STM32G0 USB Lab: Host Debug Tool

Python tool giao tiếp với USB composite device STM32G0: CLI smoke test 4 vendor commands qua EP0 và GUI test tổng hợp HID, CDC log, vendor bulk dump trên một màn hình.

11 phút đọc
STM32 cover

Kết quả đạt được

  • vendor_usb.py: wrapper pyusb cho 4 EP0 vendor commands + bulk RAM dump
  • vendor_test.py: CLI smoke test tuần tự tự động
  • GUI tool: test tự do kiểu all-in-one - HID, CDC log, vendor command cùng lúc
  • Protocol document làm nguồn sự thật duy nhất cho cả firmware và tool
  • GUI tự viết mới ở lab-14, tận dụng lại vendor_usb.py đã ổn định từ lab-13

Vai trò trong series

Tầng host-side của stm32g0-usb-device-lab. Không có tool này, không có cách nào verify firmware vendor command hoạt động đúng ngoài việc đọc CDC log thủ công.

Tool được phát triển song song với firmware, dựa trên docs/vendor-protocol.md làm nguồn sự thật duy nhất. Lịch sử git công khai đặt milestone này sau firmware để dễ đọc, nhưng thực tế cả hai được viết cùng lúc.

Cấu trúc thư mục

tools/
├─ vendor_usb.py           - wrapper pyusb, dùng chung cho CLI và GUI (lab-13)
├─ vendor_test.py          - CLI smoke test tuần tự tự động (lab-13)
├─ requirements.txt        - pyusb, libusb-package, pyserial>=3.5
├─ docs/
│  └─ vendor-protocol.md   - nguồn sự thật duy nhất cho firmware và tool
└─ composite_debug_tool/
   ├─ main.py              - entry point, trỏ về vendor_usb.py ở thư mục cha
   ├─ gui.py               - layout 2 panel, event handler (lab-14)
   ├─ cdc_logger.py        - thread đọc COM port, đẩy vào queue
   └─ hex_view.py          - hex preview cho RAM dump

main.py trỏ về tools/vendor_usb.py ở thư mục cha thay vì giữ bản copy riêng trong composite_debug_tool/. Một bản duy nhất - nếu protocol đổi chỉ sửa một nơi.

cdc_logger.py đọc đúng log format mà firmware gửi ra qua CDC - xem bài 6 CDC Debug Channel để biết firmware phía bên kia tạo ra dòng log đó như thế nào.

Quá trình triển khai: CLI trước, GUI sau

Tool được xây theo 2 giai đoạn tách biệt, không viết GUI ngay từ đầu.

Giai đoạn 1 (lab-13): CLI smoke test. Mục tiêu duy nhất là xác nhận vendor_usb.py giao tiếp đúng với 4 vendor command và Bulk IN - trước khi có bất kỳ giao diện nào. vendor_test.py chạy tuần tự, một lần, in kết quả ra console rồi thoát. Không có state phức tạp, không có thread, không có UI - chỉ có đúng một câu hỏi cần trả lời: protocol tầng thấp có đúng không.

Hạn chế khi chỉ có CLI: một khi vendor_usb.py đã đúng, CLI dạng script tuần tự lộ ra giới hạn rõ ràng khi cần debug thật:

  • Chạy một lần, in kết quả, rồi thoát - muốn thử lại một lệnh đơn lẻ (ví dụ chỉ SET_LED_MODE) phải chạy lại toàn bộ script hoặc tự sửa code.
  • Không thể vừa xem CDC log vừa gửi vendor command cùng lúc - vendor_test.py không mở kết nối CDC, phải chạy riêng một cửa sổ Tera Term khác để đối chiếu.
  • Không có cách nhấn phím vật lý (HID) và quan sát ảnh hưởng tới CDC log hay vendor state trong cùng một phiên làm việc.
  • RAM dump chỉ có thanh tiến trình dạng text in ra console, không có phản hồi trực quan khi debug tương tác.

Đây chính là động lực cho giai đoạn 2, không phải vì CLI có bug.

Giai đoạn 2 (lab-14): GUI test tự do kiểu all-in-one. Gộp CDC log, vendor command, và bulk dump vào một cửa sổ duy nhất, cho phép quan sát và điều khiển đồng thời thay vì chạy từng công cụ riêng lẻ. Chi tiết thiết kế ở phần “GUI tool” bên dưới.

Setup

Windows yêu cầu Zadig để host app truy cập Interface 3 (Vendor Specific):

1. Cắm thiết bị2. Mở Zadig → Options → List All Devices3. Chọn đúng Interface 3 (Vendor Data) - không phải Interface 0 (HID)4. Install libusbK

HID (Interface 0) và CDC (Interface 1/2) vẫn dùng inbox driver - không cần Zadig cho hai interface này. Nếu cài nhầm driver cho Interface 0, bàn phím sẽ ngừng hoạt động.

Driver này bind theo interface, không phải theo endpoint. Zadig chọn đúng Interface 3 (Vendor Specific) là đủ để cả EP0 vendor request lẫn EP4 Bulk IN (cả hai đều thuộc Interface 3) hoạt động - không cần bind riêng cho từng endpoint.

Linux/macOS: pyusb truy cập trực tiếp không cần Zadig.

cd tools/pip install -r requirements.txt

Lưu ý CDC: nếu đang mở CDC port bằng Tera Term hay terminal khác, phải đóng nó trước khi chạy tool hoặc mở GUI. CDC chỉ cho phép một connection tại một thời điểm.

CLI smoke test (lab-13)

vendor_test.py chạy 5 checks tuần tự tự động:

[1] GET_FIRMWARE_INFO  - verify magic "SG0L", in version + features + interface map[2] SET_LED_MODE       - ON → BLINK_SLOW → BLINK_FAST → OFF[3] SET_REPEAT_ENABLE  - tắt repeat[4] SET_REPEAT_ENABLE  - bật lại repeat[5] START_RAM_DUMP     - dump 144 KB, in throughput, lưu ram_dump.bin

Output thật từ lần chạy với firmware lab-13:

Found: 'STM32 USB HID 4x4 Macro Keypad' (0x0483:0x572b)[1] GET_FIRMWARE_INFO  [OK] firmware v0.1       features  : HID | CDC_LOG | VENDOR_BULK | REPEAT_CONTROL       interfaces: HID=0 CDC_ctrl=1 CDC_data=2[2] SET_LED_MODE: ON -> BLINK_SLOW -> BLINK_FAST -> OFF  [OK] led_mode -> ON  [OK] led_mode -> BLINK_SLOW  [OK] led_mode -> BLINK_FAST  [OK] led_mode -> OFF[3] SET_REPEAT_ENABLE = False  [OK] repeat_enable -> False[4] SET_REPEAT_ENABLE = True  [OK] repeat_enable -> True[5] START_RAM_DUMP -> ram_dump.bin  147456/147456 bytes (100%)  0.34 s  ->  418.7 KB/s  saved 147456 bytes -> 'ram_dump.bin'Done.

GUI tool (lab-14): test tự do kiểu all-in-one

GUI gộp CDC log panel và vendor command panel vào một màn hình, giải quyết đúng 4 hạn chế của CLI đã nêu ở trên: hai kết nối hoàn toàn độc lập, Connect CDC (qua COM port, pyserial) và Connect USB (qua pyusb/libusbK), mất một cái vẫn dùng được cái kia - có thể nhấn phím vật lý và xem log/gửi lệnh cùng lúc trong một phiên làm việc duy nhất, không cần đóng mở lại tool hay chạy song song nhiều cửa sổ.

┌─────────────────────────────────────────────────┐│ Device: STM32 USB HID 4x4 Macro Keypad          │├──────────────┬──────────────────────────────────┤│ CDC Log      │ Vendor Commands                  ││              │                                  ││ [CDC] log..  │ [GET_FIRMWARE_INFO]              ││ [HID] key..  │ [Disable Repeat] [Enable Repeat] ││ [VREQ] ..    │ [LED OFF][ON][SLOW][FAST]        ││ [BULK] ..    │ [Start Dump]  [Save...]           ││              │  Progress: ████████░░  80%        │└──────────────┴──────────────────────────────────┘

Mỗi nút bấm gọi trực tiếp một hàm tương ứng trong vendor_usb.py - không có nút nào tự viết lại logic giao tiếp USB. Người dùng có thể bấm GET_FIRMWARE_INFO bất kỳ lúc nào, đổi LED mode giữa chừng khi đang xem log, hoặc bắt đầu RAM dump trong khi vẫn đang gõ phím trên keypad - tất cả không cần dừng hay khởi động lại tool, đúng tinh thần “test tự do” thay vì chạy một kịch bản cố định như CLI.

Threading model:

CDC log reader chạy thread riêng đẩy từng dòng vào một queue.Queue riêng của cdc_logger.py. RAM dump chạy worker thread riêng để không treo UI, đẩy cả tiến độ lẫn kết quả cuối vào _result_queue của GUI - ("progress", received, total)("dump_done", result). Một vòng self.after(POLL_MS, self._poll) duy nhất ở main thread lấy hết item ra khỏi _result_queue mỗi tick và cập nhật đúng widget tương ứng theo tag ở phần tử đầu tiên - không phải hai cơ chế khác nhau cho hai loại dữ liệu, chỉ một pattern queue-poll dùng lại cho mọi thứ cần về main thread. Nhờ vậy có thể bấm nút khác hoặc theo dõi CDC log trong lúc dump 144 KB đang chạy, không bị đứng màn hình chờ.

threading.Lock trong vendor_usb.py bảo vệ ctrl_transfer để các nút vendor command gọi từ main thread không xung đột với dump worker thread nếu cả hai cùng cần truy cập USB một lúc.

Vì sao viết GUI mới lại không đụng vào phần USB

gui.py là code mới hoàn toàn, viết ở lab-14. Nhưng viết GUI mới không có nghĩa là viết lại từ đầu: vendor_usb.py (lớp protocol) đã tồn tại và được kiểm chứng từ lab-13, qua vendor_test.py chạy CLI thành công cả 5 lệnh trước khi GUI tồn tại.

GUI chỉ gọi lại đúng các hàm đã có trong vendor_usb.py - không viết thêm logic ctrl_transfer, không tự parse lại FirmwareInfo_t, không tự quản lý EP0/EP4 theo cách khác. Toàn bộ phần khó (định dạng request, đọc response, đọc Bulk IN theo acceptedLength) đã được giải quyết và test xong ở lab-13. Việc còn lại của lab-14 chỉ là lớp trình bày: CDC thread, queue, dump worker, progress bar, hex view - hoàn toàn không chạm vào code giao tiếp USB.

Đây là lợi ích cụ thể của việc tách vendor_usb.py (protocol layer) khỏi phần giao diện ngay từ lab-13: khi viết GUI sau đó, phần rủi ro nhất (giao tiếp USB đúng hay sai) đã được loại bỏ từ trước, chỉ còn lại công việc UI thuần tuý.

vendor_usb.py: cách giao tiếp protocol

Tất cả vendor command dùng EP0 Control IN:

bmRequestType = 0xC0   # Device→Host | Vendor | Device
# Ví dụ: GET_FIRMWARE_INFOdata = dev.ctrl_transfer(    0xC0,                    # bmRequestType    0x01,                    # bRequest = GET_FIRMWARE_INFO    wValue=0, wIndex=0,    data_or_wLength=17       # đọc 17 byte FirmwareInfo_t)

RAM dump qua Bulk IN:

# Bước 1: arm dump qua EP0resp = dev.ctrl_transfer(0xC0, 0x04, data_or_wLength=6)# → VendorDumpResponse_t: {status, request, acceptedLength=147456}# Bước 2: đọc đúng acceptedLength byte từ EP4 IN (0x84)while len(received) < accepted_len:    chunk = dev.read(0x84, 64, timeout=5000)    received.extend(chunk)

Không có ZLP để báo kết thúc. Host biết trước acceptedLength từ response của START_RAM_DUMP và tự dừng đọc khi đủ byte.

Protocol document

docs/vendor-protocol.md là nguồn sự thật duy nhất cho cả firmware và tool.

FirmwareInfo_t layout (17 byte, packed little-endian):

Offset  Size  Field0       4     magic = 0x4C304753 ("SG0L" little-endian)4       2     versionMajor6       2     versionMinor8       4     featureFlags12      1     hidInterface13      1     cdcControlInterface14      1     cdcDataInterface15      1     hidInEp16      1     cdcLogInEp

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ũ.

Nếu tool và firmware lệch cấu trúc response (không phải lệch bRequest): không có lỗi nào phát sinh - ctrl_transfer vẫn trả đủ dữ liệu, struct.unpack vẫn chạy, nhưng các field sau offset lệch bị đọc sai âm thầm. Đây chính xác là lý do docs/vendor-protocol.md cần được giữ làm nguồn sự thật, không suy diễn cấu trúc struct từ code cũ.

Bug gặp phải

ctrl_transfer lỗi ngay khi dùng WinUSB thay vì libusbK - đã giải thích chi tiết ở Callout trong phần Setup phía trên. Đây là bug thật gặp phải lúc đầu khi chọn driver qua Zadig, không phải cảnh báo phòng ngừa suông.

Lệch cấu trúc response im lặng nếu không theo vendor-protocol.md - nêu ở phần Protocol document: nếu tool tự suy diễn struct từ code cũ thay vì đọc đúng tài liệu, ctrl_transfer vẫn chạy được, dữ liệu vẫn về đủ, nhưng field sau offset lệch bị đọc sai mà không có lỗi nào báo ra - loại bug khó phát hiện nhất vì không crash, không exception.

Evidence

MilestoneEvidence
lab-13vendor_test.py output thật: 5 checks OK, 418.7 KB/s RAM dump (assets/evidence/lab-13-host-tool/vendor-test-output.txt)
lab-14Screenshot GUI 2 panel; cdc-demo-log.txtvendor-commands-log.txt khớp nhau; video demo youtu.be/AYjgSDcFNJ8

Xem thêm: bài 7 Vendor Request và Bulk, bài 8 Composite Debug Tool

Hạn chế đã biết

  • Vendor interface cần Zadig/libusbK binding thủ công trên Windows (không có Microsoft OS 2.0 Descriptor).
  • CDC chỉ cho một connection - phải đóng Tera Term trước khi mở GUI CDC panel.
  • RAM dump cố định 144 KB từ 0x20000000, host không thể chỉ định address hay size.
  • Bus reset hoặc suspend giữa lúc dump làm abort toàn bộ - host phải gửi lại START_RAM_DUMP từ đầu.

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ị.

Góp ý

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.

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.