STM32G0 USB Lab: Composite Device
USB composite device trên STM32G0B1RE: HID Keyboard + CDC log + EP0 Vendor Request + Vendor Bulk IN. Một codebase phát triển từ HID đơn đến debug tool hoàn chỉnh qua commit/tag.
Kết quả đạt được
- Composite class tự viết (không dùng ST HID class mặc định)
- 4 interface: HID Boot Keyboard + CDC ACM + Vendor Specific
- EP0 Vendor Request: GET_FIRMWARE_INFO, SET_REPEAT_ENABLE, SET_LED_MODE, START_RAM_DUMP
- Vendor Bulk IN: RAM dump 144 KB qua EP4, không ZLP
- USB lifecycle: reset/suspend/resume với abort dump và clear busy flag
- CDC log ring buffer 512 byte, ISR-safe pattern
Vai trò trong series
Project page này mô tả tầng USB trong
stm32g0-usb-device-lab,
từ milestone lab-08 (HID keyboard baseline) đến lab-15 (USB lifecycle).
Codebase phát triển theo commit/tag - có thể checkout từng mốc để xem hoặc build lại trạng thái firmware tại thời điểm đó:
git checkout lab-08-hid-keyboard-baseline # chỉ HID đơngit checkout lab-09-composite-skeleton # HID + CDC skeletongit checkout lab-10-cdc-debug-channel # CDC log hoạt độnggit checkout lab-11-vendor-request # EP0 vendor commandsgit checkout lab-12-vendor-bulk # Bulk IN RAM dumpgit checkout lab-15-usb-lifecycle # reset/suspend handlingHardware setup: breakout board và nguồn điện
USB Device peripheral của STM32G0B1 (PA11=D-, PA12=D+) không có connector sẵn trên NUCLEO-G0B1RE - nối ra ngoài qua một breakout board USB rời. Setup hiện tại chỉ nối D+/D-, dây VBUS của breakout không nối: nguồn cho toàn bộ board (kể cả MCU) vẫn lấy qua ST-Link, jumper chọn nguồn trên board đang ở vị trí mặc định (vị trí 1).
Để thiết bị tự cấp nguồn qua chính đường USB Device, không phụ thuộc ST-Link cắm vào PC: đổi jumper chọn nguồn từ vị trí 1 sang vị trí 4. Khi đó board nhận điện trực tiếp từ VBUS của đường USB Device - có thể cắm vào sạc pin thường, không cần máy tính, USB Device vẫn enumerate và hoạt động bình thường.
Cấu trúc descriptor tổng quan
Device Descriptor
bDeviceClass/SubClass/Protocol = 0xEF / 0x02 / 0x01 (bắt buộc khi dùng IAD)
└─ Configuration Descriptor (wTotalLength = 100 byte, bNumInterfaces = 4)
├─ Interface 0 - HID Boot Keyboard
│ ├─ HID Descriptor
│ └─ Endpoint: EP1 IN Interrupt, 8 byte, bInterval=0x0A (10ms, thực tế 8ms)
│
├─ IAD bFirstInterface=1, bInterfaceCount=2 (group IF1+IF2 thành CDC ACM)
├─ Interface 1 - CDC Communication (ACM control)
│ └─ Endpoint: EP2 IN Interrupt, 8 byte, bInterval=0xFF
├─ Interface 2 - CDC Data
│ ├─ Endpoint: EP3 OUT Bulk, 64 byte
│ └─ Endpoint: EP3 IN Bulk, 64 byte
│
└─ Interface 3 - Vendor Specific (RAM dump)
└─ Endpoint: EP4 IN Bulk, 64 byte 64 byte trên các endpoint Bulk (EP3, EP4) là max packet size của Bulk endpoint ở
Full-Speed theo USB 2.0 spec, không phải lựa chọn tùy ý. Nhiều packet 64 byte tự nối
thành một transfer dài hơn ở tầng host; nếu độ dài data đúng bằng bội số của 64, host
cần thêm một Zero-Length Packet để biết transfer đã kết thúc (không áp dụng cho RAM
dump ở Interface 3 vì host đã biết trước acceptedLength).
Mảng descriptor thật: Configuration + Interface 0
Trích từ sCompositeCfgDesc[] trong usbd_composite.c - phần Configuration
Descriptor header và Interface 0 (HID), đúng nguyên văn kèm comment gốc trong code:
__ALIGN_BEGIN static uint8_t sCompositeCfgDesc[COMP_CFG_DESC_SIZE] __ALIGN_END ={ /* ---------- Configuration Descriptor ---------- */ 0x09, /* bLength */ USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType */ COMP_CFG_DESC_SIZE, 0x00, /* wTotalLength */ 0x04, /* bNumInterfaces: HID(0) + CDC Comm(1) + CDC Data(2) + Vendor(3) */ 0x01, /* bConfigurationValue */ 0x00, /* iConfiguration */#if (USBD_SELF_POWERED == 1U) 0xC0, /* bmAttributes: Self-powered */#else 0x80, /* bmAttributes: Bus-powered */#endif 0x32, /* MaxPower: 100 mA */ /* ---------- Interface 0: HID Keyboard ---------- */ 0x09, /* bLength */ USB_DESC_TYPE_INTERFACE, /* bDescriptorType */ COMP_IF_HID, /* bInterfaceNumber */ 0x00, /* bAlternateSetting */ 0x01, /* bNumEndpoints */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: Boot */ 0x01, /* bInterfaceProtocol: Keyboard */ 0x00, /* iInterface */ /* HID Class Descriptor */ 0x09, /* bLength */ HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID */ 0x11, 0x01, /* bcdHID: 1.11 */ 0x0F, /* bCountryCode: Japan */ 0x01, /* bNumDescriptors */ HID_REPORT_DESC_TYPE, /* bDescriptorType: Report */ HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength low */ 0x00, /* wItemLength high */ /* EP1 IN: HID Keyboard Interrupt IN */ 0x07, /* bLength */ USB_DESC_TYPE_ENDPOINT, /* bDescriptorType */ COMP_HID_EPIN_ADDR, /* bEndpointAddress: IN EP1 */ 0x03, /* bmAttributes: Interrupt */ COMP_HID_EPIN_SIZE, 0x00, /* wMaxPacketSize: 8 bytes */ COMP_HID_FS_BINTERVAL, /* bInterval: 10 ms */ /* ---------- IAD: CDC ACM (Interfaces 1 and 2) ---------- */ 0x08, /* bLength */ 0x0B, /* bDescriptorType: IAD */ COMP_IF_CDC_COMM, /* bFirstInterface */ 0x02, /* bInterfaceCount */ 0x02, /* bFunctionClass: CDC */ /* ... IAD tiếp tục, IF1 CDC Comm, IF2 CDC Data, IF3 Vendor - xem đầy đủ trong repo */bCountryCode = 0x0F (Japan) là giá trị thật đang dùng trong firmware, không phải
0x00 (not localized) như ví dụ minh hoạ chung ở bài 3 - lựa chọn cụ thể của project
này, không ảnh hưởng layout ký tự vì OS/input method mới là bên quyết định layout thật.
bmAttributes có 2 nhánh biên dịch tuỳ USBD_SELF_POWERED - repo mặc định build ở
nhánh bus-powered (0x80), khớp với setup phần cứng thật hiện tại (lấy điện qua
ST-Link, xem phần “Hardware setup” phía trên).
Composite class: nền tảng chung cho cả 4 interface
Sau khi lab-08 chạy được bàn phím thật bằng usbd_hid.c chuẩn của ST, bước tiếp theo
không phải sửa tiếp file đó mà là viết hẳn một file mới, usbd_composite.c, để thay
thế hoàn toàn. Lý do: usbd_hid.c chỉ xử lý được đúng một class (HID), trong khi bước
kế tiếp cần thêm CDC log và sau đó là Vendor Bulk. Đặt nền bằng composite class ngay từ
đầu giúp CDC và Vendor chỉ là “thêm nhánh” vào file đã có, không phải viết lại lần nữa
mỗi khi thêm chức năng.
Việc này chia làm 2 bước nhỏ thay vì viết một lần: bước 1 chỉ có HID tự viết (để xác nhận cơ chế class hoạt động đúng - Init/DeInit/Setup/DataIn), bước 2 mới thêm CDC ACM. Tách nhỏ như vậy giúp nếu có bug, phạm vi nghi ngờ chỉ nằm trong đúng phần vừa thêm - cùng nguyên tắc “làm từng bước” áp dụng cho descriptor ở bài 3.
Bước 1 (chỉ HID) lộ ra 5 bug thuộc về hạ tầng composite class, ảnh hưởng chung tới mọi interface sau này chứ không riêng HID - hầu hết build vẫn OK nên không có gì báo trước:
| Bug | Triệu chứng | Fix |
|---|---|---|
Include usbd_hid.h đã xóa | Build error | Xóa include |
USBD_static_malloc sai kích thước | Crash ngầm | Sửa sizeof |
Sof_enable = DISABLE | SOF callback không bắn | ENABLE |
PMA 0x018 đè BTABLE | Transfer lỗi ngẫu nhiên | Offset từ 0x040 |
Thiếu reset hidTxBusy trong DataIn | Chỉ gửi được 1 phím (HID) | Thêm hcomp->hidTxBusy = false |
Riêng bug bDeviceClass sai khi thêm IAD cũng thuộc về bước này, ở tầng Device
Descriptor chứ không riêng interface nào:
USBView báo *!*ERROR: device class should be Multi-interface Function 0xEF.
Fix: đổi bDeviceClass/SubClass/Protocol sang 0xEF/0x02/0x01 trong usbd_desc.c.
Interface 0: HID Boot Keyboard
bInterfaceClass/SubClass/Protocol = 0x03/0x01/0x01, endpoint duy nhất EP1 IN
Interrupt 8 byte. Report descriptor cố định theo Boot Protocol, không tự viết Report
Descriptor tùy ý (xem lý do ở bài 3).
Bug thật (lab-08): CubeMX tạo HID Mouse descriptor. Chọn “HID Keyboard” trong
CubeMX nhưng template được generate là mouse (bInterfaceProtocol = 0x02, report
descriptor 74 byte, wMaxPacketSize = 4). Không có lỗi build, không cảnh báo - phát
hiện qua USBView. Fix: thay toàn bộ bằng Boot Keyboard descriptor 45 byte.
Mảng gốc CubeMX sinh (HID_MOUSE_ReportDesc, HID_MOUSE_REPORT_DESC_SIZE = 74U),
lấy nguyên văn từ lịch sử git trước khi sửa:
static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] ={ 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ 0x09, 0x02, /* Usage (Mouse) */ 0xA1, 0x01, /* Collection (Application) */ 0x09, 0x01, /* Usage (Pointer) */ 0xA1, 0x00, /* Collection (Physical) */ 0x05, 0x09, /* Usage Page (Button) */ 0x19, 0x01, /* Usage Minimum (0x01) */ 0x29, 0x03, /* Usage Maximum (0x03) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (1) */ 0x95, 0x03, /* Report Count (3) */ 0x75, 0x01, /* Report Size (1) */ 0x81, 0x02, /* Input (Data,Var,Abs) */ 0x95, 0x01, 0x75, 0x05, 0x81, 0x01, /* Input (Const,Array,Abs) - padding */ 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06, /* Input (Data,Var,Rel) - X, Y, Wheel */ 0xC0, /* End Collection */ 0x09, 0x3C, 0x05, 0xFF, 0x09, 0x01, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x02, 0xB1, 0x22, /* Feature (Data,Var,Abs,NoWrp) - Motion Wakeup */ 0x75, 0x06, 0x95, 0x01, 0xB1, 0x01, /* Feature (Const,Array,Abs,NoWrp) */ 0xC0 /* End Collection */};Thay bằng (HID_Keyboard_ReportDesc, HID_KEYBOARD_REPORT_DESC_SIZE = 45U):
static uint8_t HID_Keyboard_ReportDesc[HID_KEYBOARD_REPORT_DESC_SIZE] ={ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x06, /* Usage (Keyboard) */ 0xA1, 0x01, /* Collection (Application) */ 0x05, 0x07, /* Usage Page (Key Codes) */ 0x19, 0xE0, /* Usage Minimum (224) */ 0x29, 0xE7, /* Usage Maximum (231) */ 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, /* Input - modifier byte (8 bit) */ 0x95, 0x01, 0x75, 0x08, 0x81, 0x03, /* Input (Constant) - reserved byte */ 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, /* Input - 6 byte keycode array */ 0xC0 /* End Collection */};Cấu trúc mouse có 3 bit button, padding, rồi 3 byte X/Y/Wheel dạng delta tương đối
(Input Data,Var,Rel) - không có khái niệm “6 keycode slot” nào cả. Report bàn phím
gửi vào driver chuột không tạo ra ký tự gì vì driver chuột parse đúng theo cấu trúc
trên, không tìm thấy trường nào khớp với dữ liệu bàn phím gửi lên.
Cả hai mảng đều lấy nguyên văn từ commit 7b56832 (Add USB HID transport and fix Boot Keyboard descriptor) trong lịch sử git của repo.
Wireshark xác nhận report thật khi nhấn phím “1”: 00 00 1E 00 00 00 00 00
(0x1E = HID usage cho phím 1/!). Chi tiết cách tạo report 8 byte, tap-style null
report, và macro sequence nằm ở
bài 5 HID Keyboard Baseline.
Vì sao HidKeyboardConvert_Run() nằm ngoài scan loop
Main loop trong app_main.c gọi hai thứ tách biệt:
while (ScanScheduler_TakeRequest() != 0U){ KeyDetect_Run();}HidKeyboardConvert_Run(); /* ngoài while loop, không phụ thuộc scan tick */HidKeyboardConvert_Run() cố tình đặt ngoài vòng while, không gọi bên trong cùng
với KeyDetect_Run(). Nếu đặt bên trong, HID report chỉ được gửi khi có đúng một scan
request đang chờ xử lý - nghĩa là report phải đợi đến tick TIM6 kế tiếp (5ms) mới có cơ
hội gửi, ngay cả khi endpoint đã rảnh từ trước. Đặt ngoài loop giúp report được gửi
ngay khi endpoint idle, không phụ thuộc vào nhịp scan - tránh cộng thêm độ trễ không
cần thiết vào đường truyền phím.
Evidence đầy đủ: toàn bộ 16 phím và macro qua Wireshark
Capture USBPcap cho từng phím, mỗi phím là 2 frame (key-down rồi null ngay sau):
Phím HID bytes (8 byte)
─────────── ────────────────────────
'1' 00 00 1E 00 00 00 00 00
'2' 00 00 1F 00 00 00 00 00
'3' 00 00 20 00 00 00 00 00
'4' 00 00 21 00 00 00 00 00
'a' 00 00 04 00 00 00 00 00
'b' 00 00 05 00 00 00 00 00
'c' 00 00 06 00 00 00 00 00
'd' 00 00 07 00 00 00 00 00
Enter 00 00 28 00 00 00 00 00
Space 00 00 2C 00 00 00 00 00
Backspace 00 00 2A 00 00 00 00 00
Tab 00 00 2B 00 00 00 00 00
Macro (Ctrl+C/V/S là 2 bước, Alt+Tab là 3 bước vì phải giữ Alt trước khi thêm Tab):
Macro HID bytes (8 byte)
────────────────── ────────────────────────
Ctrl+C (bước 1) 01 00 06 00 00 00 00 00 LCtrl + c
Ctrl+V (bước 1) 01 00 19 00 00 00 00 00 LCtrl + v
Ctrl+S (bước 1) 01 00 16 00 00 00 00 00 LCtrl + s
Alt+Tab (bước 1) 04 00 00 00 00 00 00 00 LAlt giữ trước
Alt+Tab (bước 2) 04 00 2B 00 00 00 00 00 LAlt + Tab
Mỗi macro kết thúc bằng null report 00 00 00 00 00 00 00 00 sau bước cuối, giống
key thường.
Interface 1-2: CDC ACM (log qua cổng COM ảo)
Hai interface CDC (Comm + Data) được IAD nhóm thành một chức năng duy nhất - không có IAD, Windows không tạo cổng COM dù cả hai interface vẫn enumerate riêng lẻ bình thường.
CdcLog_Printf không phải ISR-safe (dùng vsnprintf, không có lock). Tất cả call site
phải là main loop context. Với EP0 vendor handler (chạy trong USB stack context), dùng
pattern pending log flag:
/* Trong VendorCmd_HandleSetup - USB stack context */sPendingLogVal = (uint32_t)someValue; /* giá trị cần in, nếu có */sPendingLog = VLOG_GET_INFO_OK; /* chỉ set cờ *//* Trong VendorCmd_FlushPendingLog - main loop */VendorLogEvent_t evt = sPendingLog;if (evt == VLOG_NONE) return;sPendingLog = VLOG_NONE; /* clear trước khi log */CdcLog_Printf("[VREQ] GET_FIRMWARE_INFO\r\n");Pattern tương tự dùng cho USB lifecycle events (reset/suspend/resume), qua module
usb_lifecycle.c riêng - sPendingLog ở đó không dùng chung biến với vendor command,
mỗi module tự có cờ riêng.
Không cần lock cho sWriteIdx/sReadIdx: producer (CdcLog_Printf qua
CdcLog_Write) và consumer (CdcLog_Run) đều chạy main loop, tuần tự, không ISR nào
chạm vào ring buffer. Ràng buộc “chỉ gọi từ main loop” ở trên chính là lý do bỏ qua
critical section, không phải thiếu sót.
Kết nối CDC còn có race condition lúc boot (driver bind vs app mở port thật), fix bằng delay 100ms sau cạnh lên kết nối - đã giải thích chi tiết trong bài 6: CDC Debug Channel.
Interface 3: Vendor Specific (EP0 command + Bulk dump)
Interface 3 phục vụ hai đường giao tiếp riêng: lệnh điều khiển qua EP0 Control, và truyền dữ liệu lớn qua EP4 Bulk IN. Thiết kế chi tiết, lý do tách control path/data path, và code thật của state machine RAM dump nằm ở bài 7 Vendor Request và Bulk - phần dưới đây chỉ tóm tắt để tra cứu nhanh.
EP0 Vendor Command Table:
| bRequest | Tên | wValue | Response |
|---|---|---|---|
0x01 | GET_FIRMWARE_INFO | - | FirmwareInfo_t 17 byte |
0x02 | SET_REPEAT_ENABLE | 0/1 | VendorResponse_t |
0x03 | SET_LED_MODE | 0-3 | VendorResponse_t |
0x04 | START_RAM_DUMP | - | VendorDumpResponse_t |
Tất cả dùng bmRequestType = 0xC0 (Device→Host, Vendor, Device). FirmwareInfo_t có
magic "SG0L" được chọn khác với magic của firmware phiên bản trước, để tránh host
tool nhầm lẫn khi phát hiện thiết bị nếu vô tình cắm nhầm firmware cũ.
Vendor Bulk IN: RAM dump. START_RAM_DUMP arm một state machine dump 144 KB SRAM
(0x20000000, khớp linker script của G0B1):
Host gửi START_RAM_DUMP qua EP0 ↓Firmware trả VendorDumpResponse_t: status=OK, acceptedLength=147456 ↓VendorDump_Run() gửi chunk đầu 64 byte từ main loop ↓VendorDump_OnTxCplt() (ISR, gọi từ Composite_DataIn) gửi chunk tiếp theo ↓Lặp cho đến khi gửi đủ acceptedLength byte, set sDumpDone (volatile) ↓VendorDump_Run() lần gọi kế tiếp thấy sDumpDone, tự gọi CdcLog_Printflog "[BULK] RAM dump complete" - không qua UsbLifecycle, vì đây đã chạysẵn trong main loop context, không cần thêm một lớp pending flag nữaKhông dùng ZLP để báo kết thúc vì host đã biết chính xác acceptedLength từ response
của START_RAM_DUMP - tự dừng đọc khi đủ byte.
Output thật từ vendor_test.py (CLI test tuần tự 5 lệnh, lab-13):
[5] START_RAM_DUMP -> ram_dump.bin 147456/147456 bytes (100%) 0.34 s -> 418.7 KB/s saved 147456 bytes -> 'ram_dump.bin'
Chi tiết Zadig/libusbK, protocol document nằm ở
trang project Host Debug Tool. Câu chuyện
viết GUI mới dựa trên vendor_usb.py đã ổn định từ lab-13, và pattern queue-poll thống
nhất trong tkinter nằm ở
bài 8 Composite Debug Tool.
USB lifecycle: ảnh hưởng chung tới cả 3 interface
usb_lifecycle.c xử lý 3 sự kiện bus, tác động tới busy flag của cả HID (Interface 0)
lẫn Vendor Bulk (Interface 3) - không phải chuyện riêng của một interface:
Reset (UsbLifecycle_OnReset, gọi từ Composite_Init):
- Abort dump đang chạy nếu có (
VendorDump_Abort) - Tăng reset counter, log
[USB] RESET #N dumpAborted=0|1
Suspend (UsbLifecycle_OnSuspend, gọi từ HAL_PCD_SuspendCallback):
- Chỉ log sự kiện, không abort dump (suspend giữ nguyên state theo chuẩn USB)
Resume (UsbLifecycle_OnResume, gọi từ HAL_PCD_ResumeCallback):
- Force-clear tất cả
*TxBusyflag trên composite handle (cả HID lẫn Vendor Bulk) - Abort dump nếu đang chạy (transfer dở dang không thể tiếp tục)
- Log
[USB] RESUME #N dumpAborted=0|1
Bug tìm thấy trong project cũ: HAL_PCD_ResumeCallback dùng sai kiểu
USBD_HID_HandleTypeDef* thay vì USBD_Composite_HandleTypeDef*. Không crash vì hai
struct có field đầu giống nhau (đủ may mắn), nhưng reset sai offset - đây chính là
nguyên nhân bug “TX kẹt sau suspend” ở
bài 5. Repo này sửa bằng cách dùng
đúng kiểu composite handle từ usb_lifecycle.c.
Evidence: bảng tra nhanh theo milestone
| Milestone | Evidence |
|---|---|
lab-08 | USBView: bInterfaceProtocol=0x01 (Boot Keyboard), wMaxPacketSize=8, Wireshark HID report 00 00 1E ... |
lab-09 | USBView: composite descriptor, wTotalLength=0x0064, 4 interface, IAD, CDC COM port xuất hiện |
lab-10 | Screenshot Tera Term: [CDC] log channel connected + 16 phím + [ERR] simultaneous |
lab-11 | build OK, test độc lập không cần lab-12 |
lab-12 | USBView: wTotalLength=0x0074, bNumInterfaces=4, EP4 0x84 Bulk IN |
lab-13 | vendor_test.py: 5 checks OK, 418.7 KB/s RAM dump |
lab-14 | Screenshot GUI + 2 log file (CDC log và vendor response khớp nhau); video demo youtu.be/AYjgSDcFNJ8 |
lab-15 | Build OK (0 errors, 0 warnings); kịch bản test reset/suspend/resume có sẵn trong DEVLOG nhưng video chưa quay |
Xem thêm: bài 3 USB Descriptor, bài 5 HID Baseline, bài 6 CDC Debug Channel, bài 7 Vendor Request và Bulk, bài 8 Composite Debug Tool
Giới hạn hiện tại
- RAM dump luôn dump toàn bộ 144 KB từ
0x20000000, host không thể chỉ định address hay size tùy ý. - Vendor interface trên Windows cần Zadig/libusbK binding trước khi Python tool có thể dùng.
- Suspend/resume không tối ưu low-power - chỉ abort dump và clear flag, không vào STOP mode.
- Bus reset hoặc suspend/resume giữa lúc dump làm abort toàn bộ transfer đang
chạy - không resume từ chỗ dở dang, host phải gửi lại
START_RAM_DUMPvà đọc lại từ đầu. - Microsoft OS 2.0 Descriptor chưa implement (cần Zadig thủ công).
Video demo
Bài viết này hữu ích với bạn?
Chia sẻ, góp ý, hoặc ủng hộ nếu bạn thấy nội dung này có giá trị.
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 Class và Composite Device là gì?
Các USB class thông dụng (HID, CDC, MSC, Vendor Specific) và composite device kết hợp nhiều class trên một thiết bị như thế nào.
USB Descriptor là gì?
Giải thích khái niệm và cấu trúc của USB Descriptor kèm ví dụ. Lưu ý về Boot Protocol trong Configuration Descriptor
USB Enumeration Process là gì?
Chuỗi hỏi–đáp qua Endpoint 0 từ lúc device cắm vào đến khi host giao address và chọn configuration. 7 bước, điểm nào dễ fail và cách debug từ dưới lên.
Tiếp tục xem các project embedded
Các project thực chiến giúp biến ghi chú kỹ thuật thành kinh nghiệm.