USB Device trên STM32: CDC, HID và cách dùng ST Middleware thực tế
USB Device trên STM32 với CDC và HID trong dự án thực tế.
1. Tại sao POS system cần USB Device
Trên POS system, USB thường xuất hiện ở nhiều chỗ hơn người ta nghĩ. Đầu đọc thẻ từ thường kết nối qua USB HID - máy tính host đọc nó như một bàn phím, không cần driver đặc biệt. Máy in thermal thường có cổng USB CDC hoặc custom class. Và với firmware update: thay vì kỹ thuật viên ra tận nơi cắm ST-Link, USB CDC là cách đơn giản nhất để kết nối máy tính thu ngân với STM32 rồi update firmware qua đó.
Bài này tập trung vào STM32 đóng vai trò USB Device - STM32 giả lập một thiết bị, máy tính là host. Dùng ST USB middleware (đi kèm với STM32Cube), không tự implement stack từ spec.
2. USB hoạt động như thế nào - những gì cần biết để debug
Nhiều bài tutorial bỏ qua phần này và nhảy thẳng vào CubeMX config. Nhưng khi USB không nhận, “Unknown USB Device” hay “Device Descriptor Request Failed” xuất hiện - hiểu protocol giúp debug đúng hướng thay vì mò mẫm.
Enumeration - quá trình “bắt tay”
Khi cắm USB vào máy tính, host không biết thiết bị là gì. Nó thực hiện enumeration - một chuỗi các bước để tìm hiểu:
Device cắm vào
↓
Host phát hiện qua hub (D+ hoặc D- được kéo lên)
↓
Host reset device
↓
Host gửi GET_DESCRIPTOR(Device) tới địa chỉ 0
↓
Device trả Device Descriptor (VID, PID, USB version...)
↓
Host gán địa chỉ mới (SET_ADDRESS)
↓
Host đọc Configuration Descriptor, Interface, Endpoint
↓
Host tìm driver phù hợp dựa trên VID/PID hoặc Class code
↓
Device vào trạng thái Configured - sẵn sàng truyền data
Nếu bất kỳ bước nào fail - ví dụ Device Descriptor trả sai, clock 48MHz không ổn định, hay firmware không kịp respond - host báo “Unknown USB Device” hoặc “Device Descriptor Request Failed”.
Descriptor - “hộ chiếu” của device
Descriptor là các cấu trúc dữ liệu mà device gửi cho host trong quá trình enumeration để mô tả bản thân. ST middleware generate sẵn, nhưng biết chúng có gì giúp debug và customization:
| Mục | Giá trị | Ghi chú |
|---|---|---|
| Device Descriptor | VID, PID, USB version, số configuration | VID/PID quyết định driver nào được load. 0x0483 là ST VID. |
| Configuration Descriptor | Power, số interface | Bus-powered hay self-powered, max current. |
| Interface Descriptor | Class, SubClass, Protocol | Ví dụ: CDC = class 0x02, HID = class 0x03. |
| Endpoint Descriptor | Địa chỉ endpoint, type, max packet size | IN/OUT, control/bulk/interrupt/isochronous. |
| String Descriptor | Manufacturer, Product, Serial number | Text hiển thị trong Device Manager. Optional. |
Transfer type - 4 loại khác nhau cho 4 usecase
| Mục | Giá trị | Ghi chú |
|---|---|---|
| Control | Endpoint 0, hai chiều | Dùng cho enumeration và configuration. Mọi device đều phải có. Firmware thường không cần handle trực tiếp - middleware lo. |
| Interrupt | Polling định kỳ (1ms-255ms) | Dùng cho HID (bàn phím, chuột, đầu đọc thẻ). Guaranteed latency. Max 64 bytes/packet ở Full Speed. |
| Bulk | Best-effort, không guarantee timing | Dùng cho CDC data, MSC. Lớn hơn interrupt (64 bytes/packet FS), dùng bandwidth còn lại sau interrupt/iso. |
| Isochronous | Periodic, không retransmit | Dùng cho audio, video. Không dùng trong POS thông thường. |
Điểm quan trọng: “Interrupt” trong USB không giống interrupt của MCU. Nó chỉ có nghĩa là host poll với tần suất cố định - guaranteed latency, không phải event-driven thật sự.
Clock 48MHz - điều kiện tiên quyết
USB Full Speed yêu cầu clock chính xác 48 MHz. Trên STM32, USB peripheral có clock domain riêng và phải được cấp đúng 48 MHz - không phải clock system, không phải PCLK.
Gotcha thường gặp nhất: cấu hình xong CubeMX, build, flash,
cắm USB → "Unknown USB Device (Device Descriptor Request Failed)"
→ 90% là clock 48MHz chưa đúng.
CubeMX có nút “Resolve Clock Issues” trong tab Clock Configuration - dùng nó khi bật USB để tự động tính PLL cho 48MHz.
3. STM32G0 và USB DRD_FS
STM32G0B1 dùng peripheral USB DRD_FS (Dual Role Device, Full Speed) - khác với USB_OTG_FS của STM32F4/H7. Khi tìm tài liệu hay example, cần lưu ý điều này.
Trong CubeMX:
- Peripheral: USB (không phải USB_OTG)
- Middleware: USB_Device → chọn class
Pins:
PA11= USB_DM (D-)PA12= USB_DP (D+)
Không cần external pull-up resistor - STM32G0 có internal pull-up cho D+, enable bằng cách set USB_BCDR |= USB_BCDR_DPPU.
4. USB CDC - Virtual COM Port
CDC (Communication Device Class) làm STM32 trông giống một COM port với máy tính. Không cần cài driver trên Linux, macOS, và Windows 10+. Mở bằng bất kỳ terminal nào (TeraTerm, PuTTY, minicom).
Vì sao CDC hữu ích hơn UART converter
Trên board POS, thường phải gắn thêm chip CH340/CP2102 để có cổng debug. USB CDC loại bỏ chip đó:
Với USB-UART converter: STM32 → UART → CH340 → USB → PC
Với USB CDC: STM32 → USB (D+/D-) → PC
Ít linh kiện hơn, ít điểm hỏng hơn. Và vì baud rate là “ảo” (không có UART vật lý), không có vấn đề baud rate mismatch.
Setup trong CubeMX
- Connectivity → USB → Enable Device (FS)
- Middleware → USB_DEVICE → Class: Communication Device Class (CDC)
- Clock Configuration → Ensure USB clock = 48 MHz
- Generate code
CubeMX tạo ra:
USB_DEVICE/
├── App/
│ ├── usb_device.c - init
│ └── usbd_cdc_if.c - RX callback và TX function
└── Target/
├── usbd_conf.c - HAL PCD callbacks
└── usbd_desc.c - Descriptors
Gửi data - CDC_Transmit_FS
#include "usbd_cdc_if.h"
/* Gửi string */
void USB_CDC_SendString(const char *str)
{
uint16_t len = strlen(str);
/* CDC_Transmit_FS copy data vào internal buffer rồi gửi */
/* Trả USBD_OK, USBD_BUSY, hoặc USBD_FAIL */
uint8_t result = CDC_Transmit_FS((uint8_t*)str, len);
if (result == USBD_BUSY) {
/* USB endpoint đang busy - chờ hoặc drop */
/* Trong thực tế nên có retry với timeout */
}
}
/* Gửi binary data */
void USB_CDC_SendBytes(const uint8_t *data, uint16_t len)
{
CDC_Transmit_FS((uint8_t*)data, len);
}
Nhận data - CDC_Receive_FS callback
/* Trong usbd_cdc_if.c - middleware gọi hàm này khi có data từ PC */
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* Buf: dữ liệu nhận được
* Len: số byte nhận được
* Hàm này chạy trong USB interrupt context! */
/* Option 1: Gửi vào FreeRTOS queue để task xử lý */
UsbRxPacket_t pkt;
memcpy(pkt.data, Buf, *Len);
pkt.len = *Len;
BaseType_t woken = pdFALSE;
xQueueSendFromISR(xUsbRxQueue, &pkt, &woken);
portYIELD_FROM_ISR(woken);
/* QUAN TRỌNG: re-arm để nhận tiếp */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return USBD_OK;
}
Thiếu USBD_CDC_ReceivePacket() ở cuối → nhận được packet đầu tiên rồi không nhận tiếp được nữa. Đây là lỗi rất phổ biến.
CDC với FreeRTOS - task riêng
/* Task xử lý USB CDC Rx */
void TaskUsbCdc(void *arg)
{
UsbRxPacket_t pkt;
for (;;) {
if (xQueueReceive(xUsbRxQueue, &pkt, portMAX_DELAY) == pdTRUE) {
/* Xử lý protocol từ PC */
Protocol_ProcessUsbPacket(pkt.data, pkt.len);
}
}
}
/* Khi cần gửi response */
void SendUsbResponse(const uint8_t *data, uint16_t len)
{
/* CDC_Transmit_FS không thread-safe nếu nhiều task cùng gọi */
/* Dùng mutex hoặc chỉ để một task gửi */
if (xSemaphoreTake(xUsbTxMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
CDC_Transmit_FS((uint8_t*)data, len);
/* Chờ TX complete trước khi release mutex */
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(50));
xSemaphoreGive(xUsbTxMutex);
}
}
5. USB HID - không cần driver, host đọc như bàn phím
HID (Human Interface Device) là class mà mọi OS đều hỗ trợ mà không cần cài driver. Khi STM32 enumerate là HID, Windows/Linux/macOS tự nhận.
Dùng cho:
- Đầu đọc thẻ từ gửi dữ liệu thẻ (host thấy như gõ bàn phím)
- Barcode scanner
- Custom control panel
HID Report Descriptor - mô tả format dữ liệu
Khác với CDC (dữ liệu raw bytes), HID dùng Report Descriptor để mô tả cấu trúc data. Host đọc Report Descriptor khi enumeration và biết cách interpret từng byte.
ST middleware mặc định tạo HID Mouse. Để dùng custom HID (ví dụ card reader), cần thay Report Descriptor:
/* Trong usbd_hid.c - thay thế HID_MOUSE_ReportDesc */
/* Custom HID: gửi 8 byte data tùy ý */
__ALIGN_BEGIN static uint8_t HID_CUSTOM_ReportDesc[] __ALIGN_END =
{
0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */
0x09, 0x01, /* Usage (0x01) */
0xA1, 0x01, /* Collection (Application) */
0x09, 0x01, /* Usage (0x01) */
0x15, 0x00, /* Logical Minimum (0) */
0x26, 0xFF, 0x00, /* Logical Maximum (255) */
0x75, 0x08, /* Report Size (8) - 8 bits */
0x95, 0x08, /* Report Count (8) - 8 bytes */
0x81, 0x02, /* Input (Data, Variable, Absolute) */
0x09, 0x01, /* Usage (0x01) */
0x91, 0x02, /* Output (Data, Variable, Absolute) */
0xC0 /* End Collection */
};
Với Report Descriptor này, mỗi report là 8 bytes. Không cần driver - host dùng generic HID driver.
Gửi HID report
#include "usbd_hid.h"
uint8_t report[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
/* Gửi report - polling bởi host theo interval trong descriptor */
USBD_HID_SendReport(&hUsbDeviceFS, report, sizeof(report));
HID cho card reader - gửi data như gõ phím
Cách POS hay dùng nhất: đầu đọc thẻ gửi dữ liệu qua HID Keyboard report. Host nhận như là bàn phím đang gõ ký tự. Application thu ngân đọc keyboard input bình thường - không cần biết gì về USB.
/* HID Keyboard report: modifier + reserved + 6 keycodes */
typedef struct {
uint8_t modifier; /* Shift, Ctrl, Alt... */
uint8_t reserved;
uint8_t keycode[6]; /* Tối đa 6 phím nhấn cùng lúc */
} HID_KeyboardReport_t;
void SendKeyboardChar(char c)
{
HID_KeyboardReport_t report = {0};
/* Map ASCII sang HID keycode */
if (c >= 'a' && c <= 'z') {
report.keycode[0] = 0x04 + (c - 'a');
} else if (c >= 'A' && c <= 'Z') {
report.modifier = 0x02; /* Left Shift */
report.keycode[0] = 0x04 + (c - 'A');
} else if (c >= '1' && c <= '9') {
report.keycode[0] = 0x1E + (c - '1');
}
/* ... thêm mapping khác nếu cần */
/* Gửi key press */
USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&report, sizeof(report));
/* Delay ngắn rồi gửi key release */
HAL_Delay(5);
memset(&report, 0, sizeof(report));
USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&report, sizeof(report));
}
6. So sánh CDC vs HID cho POS
| Mục | Giá trị | Ghi chú |
|---|---|---|
| Driver | CDC: không cần (Win10+) | HID: không cần | Cả hai đều no-driver trên OS hiện đại. |
| Giao tiếp | CDC: bidirectional stream | HID: report-based, polling | CDC tự nhiên hơn cho protocol 2 chiều. |
| Throughput | CDC (Bulk): ~1 MB/s | HID (Interrupt): ~64 KB/s | CDC tốt hơn cho firmware update. |
| Latency | CDC: variable | HID: guaranteed (theo interval) | HID predictable hơn cho real-time input. |
| Use case POS | CDC: debug, firmware update, host command | HID: card reader, barcode scanner, keypad | Thường cần cả hai - Composite Device. |
Composite Device - CDC + HID cùng lúc
ST middleware hỗ trợ Composite Device - một USB device có nhiều interface. POS system thường cần:
USB Composite Device
├── Interface 0: CDC (debug/update)
└── Interface 1: HID (card reader data)
Cấu hình Composite trong CubeMX phức tạp hơn một chút - cần sửa thủ công usbd_desc.c để thêm cả hai interface vào Configuration Descriptor. ST có example code tại STM32CubeG0/Projects/.../USB_Device/CDC_HID_Standalone.
7. Gotcha thực tế - những lỗi hay gặp
”Unknown USB Device” khi cắm vào Windows
Nguyên nhân 90%: clock 48MHz sai. Kiểm tra trong CubeMX Clock Configuration, tìm “USB” trong clock tree, đảm bảo = 48.000 MHz chính xác.
Nếu dùng HSI 16MHz: cần PLL × 6 = 96MHz → /2 = 48MHz
Nếu dùng HSE 8MHz: cần PLL × 12 = 96MHz → /2 = 48MHz
CDC connect được nhưng không nhận data
Nguyên nhân hay gặp: quên USBD_CDC_ReceivePacket() ở cuối CDC_Receive_FS. Sau lần nhận đầu tiên, endpoint không được re-arm → không nhận tiếp được.
HID không respond khi gửi report liên tục
USBD_HID_SendReport() trả USBD_BUSY nếu endpoint chưa sẵn sàng. Cần kiểm tra return value và retry:
uint8_t USB_HID_SendReportWithRetry(uint8_t *report, uint8_t len)
{
uint32_t timeout = HAL_GetTick() + 50; /* 50ms timeout */
while (HAL_GetTick() < timeout) {
if (USBD_HID_SendReport(&hUsbDeviceFS, report, len) == USBD_OK) {
return 0;
}
osDelay(1); /* Trong FreeRTOS */
}
return 1; /* Timeout */
}
Device disconnect khi erase Flash (single-bank)
Đây là bug thú vị đặc trưng của single-bank chip: khi erase Flash, USB interrupt bị block ~25ms. Host không nhận được SOF (Start of Frame) trong 25ms → host nghĩ device bị unplug → disconnect.
Giải pháp: execute từ RAM (đã nói trong bài Flash), hoặc dùng chip dual-bank (G0B1), hoặc tránh erase Flash khi USB đang active.
CDC bị glitch khi debug bằng ST-Link cùng lúc
ST-Link và USB CDC dùng chung nguồn 3.3V từ board. Khi ST-Link debug, đôi khi gây nhiễu trên đường USB. Nếu gặp vấn đề, thử debug qua SWO thay vì printf, hoặc dùng nguồn độc lập.
8. Checklist USB Device
Checklist USB Device STM32
9. Tóm tắt
USB Device trên STM32 với ST middleware không khó khi đã hiểu cơ bản:
- Enumeration là quá trình host tìm hiểu device qua descriptor - clock 48MHz là điều kiện tiên quyết
- CDC cho debug và firmware update: throughput tốt, bidirectional, no-driver
- HID cho input device: no-driver, guaranteed latency, không cần protocol tự thiết kế
- Callback CDC_Receive_FS chạy trong interrupt context - dùng queue để đưa sang task
- Re-arm
USBD_CDC_ReceivePacket()sau mỗi lần nhận - quên là mất data - Single-bank + erase Flash làm USB disconnect - giải pháp là execute-from-RAM hoặc dual-bank
Cho POS system: CDC làm debug/update interface, HID làm card reader interface - chạy cùng nhau trong Composite Device.
Đọc tiếp
- Flash STM32G0: Linker Script, EEPROM Emulation và Dual Bank - tại sao erase Flash làm USB disconnect
- STM32 Bootloader: Execute from RAM, UART Update và Jump to Application - USB CDC là kênh thay thế cho UART update
- FreeRTOS trên STM32: Task, Queue, Signal - task và queue cho USB CDC handling
Tài liệu tham khảo
- STM32G0B1 Reference Manual RM0444 - USB DRD_FS chapter
- STM32CubeG0 USB Examples - CDC, HID, DFU examples
- USB in a NutShell - beyondlogic.org - giải thích protocol USB rõ nhất, free
- USB HID Usage Tables - keycode mapping, usage pages
- USB Device Class Specifications - usb.org - CDC, HID spec chính thức
Thấy nội dung này hữu ích?
Lưu lại hoặc chia sẻ cho người cũng đang học firmware, BIOS/UEFI và embedded systems.
Nội dung liên quan
Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.
USB Device STM32 nâng cao: Vendor Request, Bulk, DMA và Debug với JTAG
USB Device STM32: xử lý vendor request và truyền qua DMA cho bulk và interrupt.
USB Descriptor: Phân tích từng field và HID Report Descriptor từ góc firmware
Phân tích USB Descriptor, HID Report Descriptor của thiết bị composite CDC+HID.
USB Transfer Types: Interrupt, Bulk, Control và Isochronous từ góc firmware
Giải thích về 4 loại USB transfer.
Đọc thêm về BIOS/UEFI
Khám phá các bài viết về BIOS/UEFI, embedded firmware, debugging và system-level thinking.