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.
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.pykhô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 libusbKHID (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.txtLư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.binOutput 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) và
("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 cdcLogInEpMagic "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
| Milestone | Evidence |
|---|---|
lab-13 | vendor_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-14 | Screenshot GUI 2 panel; cdc-demo-log.txt và vendor-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_DUMPtừ đầ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ị.
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.
Composite Debug Tool: GUI test tổng hợp cho STM32 USB
GUI Python gộp CDC log, vendor command và RAM dump vào một màn hình: pattern queue-poll thống nhất trong tkinter, và vì sao viết GUI mới không đụng vào phần USB đã có.
Vendor Request và Bulk IN trên STM32G0
EP0 vendor command và Bulk IN RAM dump trên STM32G0: pending log flag, tại sao không dùng ZLP, Zadig cho Windows, và output thật từ vendor_test.py.
USB Bulk Transfer cho debug dump là gì?
Bulk IN cho RAM dump trên STM32G0: state machine chunk 64 byte, DataIn callback pipeline, tại sao không dùng ZLP, Zadig cho Windows và throughput thực đo.
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.