From 5dbc15370c8257fe3c9e8b3ded8c5223cf08a19e Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 9 Jul 2026 23:38:48 +0700 Subject: example(usbtest): device-side peer for the Linux kernel usbtest battery Gadget-Zero style source/sink on a vendor interface (alt0 empty, alt1 bulk+int+iso) plus EP0 ctrl_out; tier advertised in bcdDevice. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg --- examples/device/usbtest/CMakeLists.txt | 30 +++ examples/device/usbtest/CMakePresets.json | 6 + examples/device/usbtest/Makefile | 11 + examples/device/usbtest/README.md | 94 +++++++ examples/device/usbtest/skip.txt | 15 ++ examples/device/usbtest/src/CMakeLists.txt | 4 + examples/device/usbtest/src/main.c | 336 ++++++++++++++++++++++++++ examples/device/usbtest/src/tusb_config.h | 151 ++++++++++++ examples/device/usbtest/src/usb_descriptors.c | 271 +++++++++++++++++++++ examples/device/usbtest/src/usb_descriptors.h | 69 ++++++ 10 files changed, 987 insertions(+) create mode 100644 examples/device/usbtest/CMakeLists.txt create mode 100644 examples/device/usbtest/CMakePresets.json create mode 100644 examples/device/usbtest/Makefile create mode 100644 examples/device/usbtest/README.md create mode 100644 examples/device/usbtest/skip.txt create mode 100644 examples/device/usbtest/src/CMakeLists.txt create mode 100644 examples/device/usbtest/src/main.c create mode 100644 examples/device/usbtest/src/tusb_config.h create mode 100644 examples/device/usbtest/src/usb_descriptors.c create mode 100644 examples/device/usbtest/src/usb_descriptors.h (limited to 'examples/device/usbtest') diff --git a/examples/device/usbtest/CMakeLists.txt b/examples/device/usbtest/CMakeLists.txt new file mode 100644 index 000000000..10414ede7 --- /dev/null +++ b/examples/device/usbtest/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(usbtest C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_device_example(${PROJECT_NAME} noos) diff --git a/examples/device/usbtest/CMakePresets.json b/examples/device/usbtest/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/device/usbtest/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/device/usbtest/Makefile b/examples/device/usbtest/Makefile new file mode 100644 index 000000000..035e90308 --- /dev/null +++ b/examples/device/usbtest/Makefile @@ -0,0 +1,11 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/device/usbtest/README.md b/examples/device/usbtest/README.md new file mode 100644 index 000000000..8fcc4ad36 --- /dev/null +++ b/examples/device/usbtest/README.md @@ -0,0 +1,94 @@ +# usbtest + +Device-side peer of the Linux kernel USB test pair: + +- `usbtest.ko` — host kernel module (`drivers/usb/misc/usbtest.c`) containing ~30 numbered + test cases over bulk/control/interrupt/isochronous transfers. +- `testusb` — userspace dispatcher (`tools/usb/testusb.c`) that tells the module which case + to run via usbfs ioctl. + +This example implements the Gadget-Zero style *source/sink* protocol on a vendor-specific +interface so the whole battery can exercise TinyUSB device controller drivers: + +- bulk IN = infinite source (usbtest pattern 0: all zeros) +- bulk OUT = infinite sink (data discarded) + +## Tiers + +The firmware advertises its capability tier in `bcdDevice` (`0x01TT`); the host script picks +the matching test battery automatically. + +| Tier | Capability | usbtest cases | +|------|------------|---------------| +| 1 | bulk source/sink | 0, 9, 10, 1–8, 11, 12, 24, 13, 29, 17–20, 27, 28 | +| 2 | + vendor control `0x5b`/`0x5c` (ctrl_out) | + 14, 21 | +| 3 | + interrupt source/sink | + 25, 26 | +| 4 | + isochronous source/sink | + 15, 16, 22, 23 | + +This example implements all four tiers using the vendor class with the interrupt +(`CFG_TUD_VENDOR_EP_INT_OUT/IN`) and isochronous (`CFG_TUD_VENDOR_EP_ISO_OUT/IN`) +endpoint pairs and altsetting support (`CFG_TUD_VENDOR_ALT_SETTINGS`): alt 0 +carries no endpoints, alt 1 the full source/sink set, per USB 2.0 5.6.3 (the host +usbtest driver selects alt 1 itself). + +## Test cases + +Directions are from the host's point of view: *write* = host→device (OUT endpoint, device +sinks and discards), *read* = device→host (IN endpoint, device sources zeros and the host +verifies every byte). All checking happens host-side in `usbtest.ko`; a case fails on a data +mismatch, an unexpected short packet/STALL, or a timeout. What each case stresses on the +device/DCD side: + +| # | Name | What it does / what it exercises | +|---|------|----------------------------------| +| 0 | NOP | ioctl round-trip sanity, no USB traffic — proves the interface bound with the right capability profile | +| 9 | ch9 subset | chapter-9 standard control requests (GET_DESCRIPTOR, GET_STATUS, SET/CLEAR_FEATURE, SET_INTERFACE, …) — the EP0 state machine, incl. status stages and ZLPs | +| 10 | queued control | many control URBs in flight at once — EP0 under sustained back-to-back SETUPs | +| 1 / 2 | bulk write / read | plain OUT sink / IN source streams of whole max-size packets — FIFO handling, multi-packet transfers | +| 3 / 4 | bulk write / read vary | same with transfer sizes varying per URB — short packets and packet-boundary edge cases | +| 5–8 | bulk sg write/read (+vary) | scatter-gather queued URBs — continuous packet pressure with no inter-URB gap; classic overflow/babble catcher | +| 11 / 12 | unlink reads / writes | URBs submitted then cancelled mid-flight — the device keeps streaming while the host aborts; DCD abort/cleanup paths | +| 24 | unlink queued writes | unlink from a deep OUT queue — same, under queue pressure | +| 13 | ep halt set/clear | SET_FEATURE(ENDPOINT_HALT), verify the endpoint really STALLs, then CLEAR_FEATURE and verify traffic resumes at DATA0 — stall must abort an armed transfer (and flush any loaded FIFO) | +| 29 | toggle clear | CLEAR_FEATURE(HALT) on a **non-halted** endpoint mid-traffic, purely to reset the data toggle — the DCD must reset DATA0 *without* disarming the queued transfer (historically the most common per-DCD bug in this battery) | +| 17 / 18 | bulk write / read unaligned | bulk streams from oddly-offset host buffers — host DMA-alignment path; the device sees normal traffic | +| 19 / 20 | bulk write / read premapped | bulk streams using host pre-mapped DMA buffers — another host memory path | +| 27 / 28 | bulk write / read perf | sustained maximum-throughput streams, reported in MB/s — real-time FIFO servicing under load | +| 14 | ctrl_out write/read | vendor EP0 request `0x5b` stores wLength bytes, `0x5c` reads them back, sizes varying — multi-packet control-OUT data stages and buffer persistence across requests | +| 21 | ctrl_out unaligned | same from odd host buffer offsets | +| 25 / 26 | int write / read | interrupt OUT sink / IN source at the descriptor's polling interval — interrupt endpoint arming and completion | +| 15 / 16 | iso write / read | isochronous OUT sink / IN source, one packet per (micro)frame with per-packet status — no handshake/retry, DATA0-only; the IN source must re-arm fast enough to make every frame deadline | +| 22 / 23 | iso write / read unaligned | same from odd host buffer offsets | + +Per-case iteration counts and sizes are chosen by `test/hil/usbtest.py` for the negotiated +speed (see its `PARAMS` table); the authoritative case implementations live in the kernel's +`drivers/usb/misc/usbtest.c`. + +## Running + +Use the host script (handles driver binding, per-case parameters, result parsing): + +```bash +python3 test/hil/usbtest.py --serial +``` + +Requirements on the host: `usbtest` kernel module (`CONFIG_USB_TEST`, `modprobe usbtest`), +the `testusb` binary built from kernel `tools/usb/testusb.c`, and sudo (usbfs ioctls + +driver bind/unbind). + +Manual runs are possible but beware `testusb` defaults: always pass explicit `-s`/`-v` +values that are multiples of 512 — the device streams whole max-size packets, so a +non-packet-aligned read length overflows (`-EOVERFLOW`), and never run bare `testusb -a` +(the default parameter set includes cases with invalid parameters and hour-long runtimes +at full speed). + +```bash +# bind: MUST use the 5-field form referencing Gadget Zero (0525:a4a0) so the +# dynamic id inherits its capability profile. A plain "cafe 4010" id leaves +# driver_info NULL, which usbtest_probe() dereferences -> kernel oops. +sudo modprobe usbtest +echo "cafe 4010 0 0525 a4a0" | sudo tee /sys/bus/usb/drivers/usbtest/new_id +# example: bulk write/read +sudo testusb -D /dev/bus/usb// -t 1 -c 128 -s 1024 -v 512 +sudo testusb -D /dev/bus/usb// -t 2 -c 128 -s 1024 -v 512 +``` diff --git a/examples/device/usbtest/skip.txt b/examples/device/usbtest/skip.txt new file mode 100644 index 000000000..b52bdbb14 --- /dev/null +++ b/examples/device/usbtest/skip.txt @@ -0,0 +1,15 @@ +mcu:MSP430x5xx +mcu:NUC121 +mcu:SAMD11 +# DCD has no isochronous support (dcd_edpt_iso_alloc refuses), tier-4 cannot enumerate: +mcu:CXD56 +mcu:FT90X +mcu:LPC175X_6X +mcu:LPC40XX +mcu:NUC100 +mcu:NUC120 +mcu:NUC505 +mcu:PIC32MZ +mcu:SAMG +mcu:SAMX7X +mcu:VALENTYUSB_EPTRI diff --git a/examples/device/usbtest/src/CMakeLists.txt b/examples/device/usbtest/src/CMakeLists.txt new file mode 100644 index 000000000..cef2b46ee --- /dev/null +++ b/examples/device/usbtest/src/CMakeLists.txt @@ -0,0 +1,4 @@ +# This file is for ESP-IDF only +idf_component_register(SRCS "main.c" "usb_descriptors.c" + INCLUDE_DIRS "." + REQUIRES boards tinyusb_src) diff --git a/examples/device/usbtest/src/main.c b/examples/device/usbtest/src/main.c new file mode 100644 index 000000000..78575ac9b --- /dev/null +++ b/examples/device/usbtest/src/main.c @@ -0,0 +1,336 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/* Device-side peer of the Linux kernel host test driver drivers/usb/misc/usbtest.c + * (driven from userspace by tools/usb/testusb.c). Implements the Gadget-Zero + * style source/sink protocol on a vendor interface (alt 0 = no endpoints, + * alt 1 = full set, selected by the host usbtest driver): + * - bulk/interrupt/isochronous IN = infinite source (pattern 0: all zeros) + * - bulk/interrupt/isochronous OUT = infinite sink (data discarded) + * - EP0 0x5b/0x5c = control write then read-back (ctrl_out tests) + * See examples/device/usbtest/README.md and test/hil/usbtest.py for usage. + */ + +#include +#include +#include + +#include "bsp/board_api.h" +#include "tusb.h" +#include "usb_descriptors.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ + +/* Blink pattern + * - 250 ms : device not mounted + * - 1000 ms : device mounted + */ +enum { + BLINK_NOT_MOUNTED = 250, + BLINK_MOUNTED = 1000, +}; + +static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED; + +// Source data, all zeros = usbtest pattern 0. Sizes are a multiple of the +// endpoint max packet size: transfers are always whole packets, never an +// unintended short packet or ZLP. +static uint8_t const tx_chunk[CFG_TUD_VENDOR_TX_EPSIZE]; +static uint8_t const int_tx_chunk[USBTEST_INT_EP_MPS]; +static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS]; + +// Interrupt/iso submit one packet per (micro)frame, sized to the NEGOTIATED speed's mps — a +// high-speed build enumerated at full speed must submit the FS length, not the HS-capacity buffer +// size (bulk is exempt: it streams multi-packet transfers). See usb_descriptors.h. +static inline uint16_t usbtest_int_len(void) { + return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS; +} +static inline uint16_t usbtest_iso_len(void) { + return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS; +} + +//------------- prototypes -------------// +void led_blinking_task(void* param); +void usbtest_task(void* param); + +#if CFG_TUSB_OS == OPT_OS_FREERTOS +void freertos_init(void); +#endif + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + // If using FreeRTOS: create blinky, tinyusb device, and usbtest source/sink tasks +#if CFG_TUSB_OS == OPT_OS_FREERTOS + freertos_init(); +#else + // init device stack on configured roothub port + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + while (1) { + tud_task(); // tinyusb device task + usbtest_task(NULL); + led_blinking_task(NULL); + } +#endif +} + +//--------------------------------------------------------------------+ +// Source/sink pumps +//--------------------------------------------------------------------+ + +// Polling keeps all four endpoints armed and self-heals after endpoint halt +// (set/clear feature tests): stall marks the endpoint busy so the calls fail +// quietly until the host clears the halt, then the next tick re-arms. +static void usbtest_pump(void) { + if (tud_vendor_mounted()) { + tud_vendor_read_xfer(); // bulk sink: arm/re-arm, quiet fail if armed or halted + if (tud_vendor_write_available()) { // 0 while bulk IN is busy or halted + tud_vendor_write(tx_chunk, sizeof(tx_chunk)); + } + + tud_vendor_int_read_xfer(); // interrupt sink + if (tud_vendor_int_write_available()) { + tud_vendor_int_write(int_tx_chunk, usbtest_int_len()); + } + + tud_vendor_iso_read_xfer(); // isochronous sink + if (tud_vendor_iso_write_available()) { + tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); + } + } +} + +void usbtest_task(void* param) { + (void) param; + #if CFG_TUSB_OS == OPT_OS_FREERTOS + while (1) { + usbtest_pump(); + vTaskDelay(1); // yield; tx/rx completion callbacks keep the pipes saturated between polls + } + #else + usbtest_pump(); // called from the main loop, one tick per call + #endif +} + +// Invoked when received data from host: discard and immediately re-arm +void tud_vendor_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_read_xfer(); +} + +// Invoked when last bulk tx transfer finished: keep the source saturated +void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_write(tx_chunk, sizeof(tx_chunk)); +} + +// Interrupt pair: same discard/refill pumps as bulk +void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_int_read_xfer(); +} + +void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_int_write(int_tx_chunk, usbtest_int_len()); +} + +// Isochronous pair: same discard/refill pumps; a completion may be a missed +// frame, re-arm regardless +void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { + (void) idx; + (void) buffer; + (void) bufsize; + tud_vendor_iso_read_xfer(); +} + +void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) { + (void) idx; + (void) sent_bytes; + tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); +} + +//--------------------------------------------------------------------+ +// Vendor control requests (EP0) +//--------------------------------------------------------------------+ + +// Control write/read-back for the ctrl_out tests (14/21), same protocol as +// Gadget Zero: 0x5b stores the host's wLength bytes, 0x5c returns them. +static uint8_t ctrl_buf[1024]; + +// Invoked on vendor control transfers, and by usbd for forwarded standard +// endpoint requests (halt set/clear) whose return value it ignores — return +// false for anything that is not a supported vendor request. +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR) { + return false; + } + + switch (request->bRequest) { + case 0x5b: // control WRITE: receive wLength bytes into ctrl_buf + TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_OUT); + TU_VERIFY(request->wValue == 0 && request->wIndex == 0); + TU_VERIFY(request->wLength <= sizeof(ctrl_buf)); + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, ctrl_buf, request->wLength); + } + return true; // DATA/ACK: payload already landed in ctrl_buf + + case 0x5c: // control READ: send back the previously written bytes + TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_IN); + TU_VERIFY(request->wValue == 0 && request->wIndex == 0); + TU_VERIFY(request->wLength <= sizeof(ctrl_buf)); + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, ctrl_buf, request->wLength); + } + return true; + + default: + return false; + } +} + +//--------------------------------------------------------------------+ +// Device callbacks +//--------------------------------------------------------------------+ + +// Invoked when device is mounted +void tud_mount_cb(void) { + blink_interval_ms = BLINK_MOUNTED; +} + +// Invoked when device is unmounted +void tud_umount_cb(void) { + blink_interval_ms = BLINK_NOT_MOUNTED; +} + +//--------------------------------------------------------------------+ +// BLINKING TASK +//--------------------------------------------------------------------+ +void led_blinking_task(void* param) { + (void) param; + static uint32_t start_ms = 0; + static bool led_state = false; + + while (1) { + #if CFG_TUSB_OS == OPT_OS_FREERTOS + vTaskDelay(blink_interval_ms / portTICK_PERIOD_MS); + #else + // Blink every interval ms + if (tusb_time_millis_api() - start_ms < blink_interval_ms) { + return; // not enough time + } + #endif + + start_ms += blink_interval_ms; + board_led_write(led_state); + led_state = 1 - led_state; // toggle + } +} + +//--------------------------------------------------------------------+ +// FreeRTOS +//--------------------------------------------------------------------+ +#if CFG_TUSB_OS == OPT_OS_FREERTOS + +#define BLINKY_STACK_SIZE configMINIMAL_STACK_SIZE +#define USBTEST_STACK_SIZE (configMINIMAL_STACK_SIZE*2) + +#ifdef ESP_PLATFORM + #define USBD_STACK_SIZE 4096 + int main(void); + void app_main(void) { + main(); + } +#else + // Increase stack size when debug log is enabled + #define USBD_STACK_SIZE (3*configMINIMAL_STACK_SIZE/2) * (CFG_TUSB_DEBUG ? 2 : 1) +#endif + +// static task allocation +#if configSUPPORT_STATIC_ALLOCATION +StackType_t blinky_stack[BLINKY_STACK_SIZE]; +StaticTask_t blinky_taskdef; + +StackType_t usb_device_stack[USBD_STACK_SIZE]; +StaticTask_t usb_device_taskdef; + +StackType_t usbtest_stack[USBTEST_STACK_SIZE]; +StaticTask_t usbtest_taskdef; +#endif + +// USB Device Driver task: processes all usb events and invokes callbacks +void usb_device_task(void* param) { + (void) param; + + // init device stack on configured roothub port. Must be called after the + // scheduler starts: the USB IRQ handler uses RTOS queue APIs. + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + // RTOS forever loop + while (1) { + tud_task(); // put thread to waiting state until there is a new event + } +} + +void freertos_init(void) { + #if configSUPPORT_STATIC_ALLOCATION + xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, blinky_stack, &blinky_taskdef); + xTaskCreateStatic(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_device_stack, &usb_device_taskdef); + xTaskCreateStatic(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, usbtest_stack, &usbtest_taskdef); + #else + xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL); + xTaskCreate(usb_device_task, "usbd", USBD_STACK_SIZE, NULL, configMAX_PRIORITIES-1, NULL); + xTaskCreate(usbtest_task, "usbtest", USBTEST_STACK_SIZE, NULL, configMAX_PRIORITIES-2, NULL); + #endif + + // only start scheduler for non-espressif mcu (espressif starts it in startup code) + #ifndef ESP_PLATFORM + vTaskStartScheduler(); + #endif +} +#endif diff --git a/examples/device/usbtest/src/tusb_config.h b/examples/device/usbtest/src/tusb_config.h new file mode 100644 index 000000000..dda131ae2 --- /dev/null +++ b/examples/device/usbtest/src/tusb_config.h @@ -0,0 +1,151 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Board Specific Configuration +//--------------------------------------------------------------------+ + +// RHPort number used for device can be defined by board.mk, default to port 0 +#ifndef BOARD_TUD_RHPORT +#define BOARD_TUD_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUD_MAX_SPEED +#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +// Espressif IDF requires "freertos/" prefix in include path +#ifdef ESP_PLATFORM +#define CFG_TUSB_OS_INC_PATH freertos/ +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +// Enable Device stack +#define CFG_TUD_ENABLED 1 + +// Default is max speed that hardware controller could support with on-chip PHY +#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 1 + +// Non-buffered mode: every transfer is submitted with an exact length so the +// host never sees an unexpected short packet or ZLP mid-transfer, which the +// usbtest data-integrity cases treat as failure. +#define CFG_TUD_VENDOR_RX_BUFSIZE 0 +#define CFG_TUD_VENDOR_TX_BUFSIZE 0 + +// App re-arms RX itself: required to recover the sink after halt tests +// (SET_FEATURE/CLEAR_FEATURE endpoint halt) where no completion ever fires. +#define CFG_TUD_VENDOR_RX_MANUAL_XFER 1 + +// Multi-packet IN transfers; must be a multiple of bulk MPS at both speeds (64/512). +// LPC11/13 (ip3511 FS) keep endpoint buffers in a dedicated 2 KB USB RAM: a 2048 B bulk epbuf +// overflows it once the int/iso buffers join, so those parts use 512 (= 8 FS packets). +#if TU_CHECK_MCU(OPT_MCU_LPC11UXX, OPT_MCU_LPC13XX) +#define CFG_TUD_VENDOR_TX_EPSIZE 512 +#else +#define CFG_TUD_VENDOR_TX_EPSIZE 2048 +#endif + +// Interrupt IN/OUT source/sink pair (usbtest cases 25/26). Buffer sizes track the +// per-speed endpoint max packet size (see usb_descriptors.c) so full-speed builds +// don't over-allocate the scarce USB DMA section. +#define CFG_TUD_VENDOR_EP_INT_OUT 1 +#define CFG_TUD_VENDOR_EP_INT_IN 1 +#define CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// Isochronous IN/OUT source/sink pair (usbtest cases 15/16/22/23), placed in +// altsetting 1: alt 0 has no endpoints so no iso bandwidth is claimed by default +#define CFG_TUD_VENDOR_EP_ISO_OUT 1 +#define CFG_TUD_VENDOR_EP_ISO_IN 1 +#define CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128) +#define CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 128) +#define CFG_TUD_VENDOR_ALT_SETTINGS 1 + +// CH32V20X fsdev port has only 512 B PMA and single-buffered iso can't keep the iso IN endpoint +// fed under load. Double-buffer iso; the descriptor drops iso mps to 32 there so 2x32 = 64 B/ep +// keeps the same PMA budget (see usb_descriptors.h). Other fsdev parts have room and stay single. +#if CFG_TUSB_MCU == OPT_MCU_CH32V20X +#define CFG_TUD_FSDEV_DOUBLE_BUFFERED_ISO_EP 1 +#endif + +#ifdef __cplusplus + } +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/examples/device/usbtest/src/usb_descriptors.c b/examples/device/usbtest/src/usb_descriptors.c new file mode 100644 index 000000000..24efef453 --- /dev/null +++ b/examples/device/usbtest/src/usb_descriptors.c @@ -0,0 +1,271 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "bsp/board_api.h" +#include "tusb.h" +#include "usb_descriptors.h" + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +static tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, // per-interface class + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xCafe, + .idProduct = 0x4010, + .bcdDevice = 0x0100 | USBTEST_TIER, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +// Invoked when received GET DEVICE DESCRIPTOR +uint8_t const* tud_descriptor_device_cb(void) { + return (uint8_t const*) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + +// Interface must be number 0: testusb -D issues its ioctls against interface 0 +enum { + ITF_NUM_VENDOR = 0, + ITF_NUM_TOTAL +}; + +// Vendor interface, Gadget-Zero style altsettings: alt 0 carries no endpoints (an +// isochronous endpoint must not claim bandwidth in the default altsetting, USB 2.0 +// 5.6.3), alt 1 carries bulk + interrupt + isochronous IN/OUT. The host usbtest +// driver skips altsettings without pipes and selects alt 1 itself. No TUD_ macro +// covers this layout, hand-rolled. +#define USBTEST_DESC_LEN (9 + 9 + 6*7) +#define USBTEST_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _bulk_mps, _intout, _intin, _int_mps, _int_interval, _isoout, _isoin, _iso_mps, _iso_interval) \ + /* alt 0: zero bandwidth, no endpoints */\ + 9, TUSB_DESC_INTERFACE, _itfnum, 0, 0, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ + /* alt 1: full source/sink set */\ + 9, TUSB_DESC_INTERFACE, _itfnum, 1, 6, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ + 7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ + 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ + 7, TUSB_DESC_ENDPOINT, _intout, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\ + 7, TUSB_DESC_ENDPOINT, _intin, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\ + 7, TUSB_DESC_ENDPOINT, _isoout, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval,\ + 7, TUSB_DESC_ENDPOINT, _isoin, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + USBTEST_DESC_LEN) + +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX + // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number + // 0 control, 1 Interrupt, 2 Bulk, 3 Iso, 4 Interrupt etc ... + #define EPNUM_BULK_OUT 0x02 + #define EPNUM_BULK_IN 0x85 + #define EPNUM_INT_OUT 0x01 + #define EPNUM_INT_IN 0x84 + #define EPNUM_ISO_OUT 0x03 + #define EPNUM_ISO_IN 0x86 + +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + // MCUs that don't support a same endpoint number with different direction IN and OUT + // e.g EP1 OUT & EP1 IN cannot exist together + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_BULK_OUT 0x08 + #define EPNUM_BULK_IN 0x89 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x83 + #define EPNUM_ISO_OUT 0x04 + #define EPNUM_ISO_IN 0x85 + #else + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x82 + #define EPNUM_INT_OUT 0x03 + #define EPNUM_INT_IN 0x84 + #define EPNUM_ISO_OUT 0x05 + #define EPNUM_ISO_IN 0x86 + #endif + +#elif CFG_TUSB_MCU == OPT_MCU_NRF5X + // nRF5x: ISO endpoints are hardware-fixed to EP8 (ISOOUT/ISOIN) + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x81 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x82 + #define EPNUM_ISO_OUT 0x08 + #define EPNUM_ISO_IN 0x88 + +#else + #define EPNUM_BULK_OUT 0x01 + #define EPNUM_BULK_IN 0x81 + #define EPNUM_INT_OUT 0x02 + #define EPNUM_INT_IN 0x82 + #define EPNUM_ISO_OUT 0x03 + #define EPNUM_ISO_IN 0x83 +#endif + +static uint8_t const desc_fs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval + USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 64, + EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_FS, 1, + EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_FS, 1) +}; + +#if TUD_OPT_HIGH_SPEED +// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration + +static uint8_t const desc_hs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, bulk out/in + mps, int out/in + mps + interval, iso out/in + mps + interval (1 ms) + USBTEST_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_BULK_OUT, 0x80 | EPNUM_BULK_IN, 512, + EPNUM_INT_OUT, 0x80 | EPNUM_INT_IN, USBTEST_INT_EP_MPS_HS, 4, + EPNUM_ISO_OUT, 0x80 | EPNUM_ISO_IN, USBTEST_ISO_EP_MPS_HS, 4) +}; + +// other speed configuration +static uint8_t desc_other_speed_config[CONFIG_TOTAL_LEN]; + +// device qualifier is mostly similar to device descriptor since we don't change configuration based on speed +static tusb_desc_device_qualifier_t const desc_device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0200, + + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0x00 +}; + +// Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request +uint8_t const* tud_descriptor_device_qualifier_cb(void) { + return (uint8_t const*) &desc_device_qualifier; +} + +// Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request +// Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa +uint8_t const* tud_descriptor_other_speed_configuration_cb(uint8_t index) { + (void) index; // for multiple configurations + + // if link speed is high return fullspeed config, and vice versa + // Note: the descriptor type is OTHER_SPEED_CONFIG instead of CONFIG + memcpy(desc_other_speed_config, + (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration, + CONFIG_TOTAL_LEN); + + desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + + return desc_other_speed_config; +} +#endif // highspeed + +// Invoked when received GET CONFIGURATION DESCRIPTOR +uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { + (void) index; // for multiple configurations + +#if TUD_OPT_HIGH_SPEED + // Although we are highspeed, host may be fullspeed. + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// String Descriptor Index +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER, + STRID_PRODUCT, + STRID_SERIAL, +}; + +// array of pointer to string descriptors +static char const* string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "TinyUSB", // 1: Manufacturer + "TinyUSB usbtest", // 2: Product + NULL, // 3: Serials will use unique ID if possible + "TinyUSB usbtest source/sink" // 4: Vendor Interface +}; + +static uint16_t _desc_str[32 + 1]; + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void) langid; + size_t chr_count; + + switch (index) { + case STRID_LANGID: + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + + case STRID_SERIAL: + chr_count = board_usb_get_serial(_desc_str + 1, 32); + break; + + default: + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) return NULL; + + const char* str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type + if (chr_count > max_count) chr_count = max_count; + + // Convert ASCII string into UTF-16 + for (size_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + break; + } + + // first byte is length (including header), second byte is string type + _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); + + return _desc_str; +} diff --git a/examples/device/usbtest/src/usb_descriptors.h b/examples/device/usbtest/src/usb_descriptors.h new file mode 100644 index 000000000..8a033eca9 --- /dev/null +++ b/examples/device/usbtest/src/usb_descriptors.h @@ -0,0 +1,69 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef USB_DESCRIPTORS_H_ +#define USB_DESCRIPTORS_H_ + +// Device-capability tier advertised in bcdDevice low byte (0x01TT), read by the +// host script to select which usbtest cases to run: +// 1: bulk source/sink +// 2: + vendor control 0x5b/0x5c (ctrl_out) +// 3: + interrupt source/sink +// 4: + isochronous source/sink +#define USBTEST_TIER 4 + +// Interrupt/isochronous endpoint max packet sizes, must match the configuration descriptor. +// TUD_OPT_HIGH_SPEED is a compile-time capability flag, NOT the live bus speed, so the full-speed +// config descriptor (and the OTHER_SPEED descriptor served to a HS host) must use full-speed-legal +// sizes regardless of it: interrupt <= 64 B, isochronous <= 1023 B (and both iso EPs must fit the +// 1023 B/frame FS periodic budget). Hence separate _FS / _HS descriptor sizes; the plain macro +// below is the compile-time capability maximum that sizes the source buffers (runtime write +// lengths follow the negotiated speed via tud_speed_get(), see main.c). +// +// The CH32 USB IPs have tiny per-endpoint buffers so tier-4's six endpoints don't fit at the usual +// FS sizes: usbfs gives 64 B/ep (iso must drop to 64), and the CH32V20X fsdev port shares one 512 B +// PMA across every endpoint (needs iso 32 AND a small interrupt mps to fit alongside EP0+bulk+iso). +#if CFG_TUSB_MCU == OPT_MCU_CH32V20X && defined(CFG_TUD_WCH_USBIP_FSDEV) && CFG_TUD_WCH_USBIP_FSDEV + #define USBTEST_INT_EP_MPS_FS 16 + #define USBTEST_ISO_EP_MPS_FS 32 // double-buffered on fsdev: 2x32=64/ep, same 512 B PMA budget +#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V103, OPT_MCU_CH32F20X, OPT_MCU_CH32V307, OPT_MCU_CH583) + // WCH USBFS parts cap every endpoint (except EP3 IN) at 64 B. For the CH32V307 this applies to its + // full-speed (usbfs) port; its high-speed (usbhs) port uses the _HS sizes below via desc_hs. + #define USBTEST_INT_EP_MPS_FS 64 + #define USBTEST_ISO_EP_MPS_FS 64 +#else + #define USBTEST_INT_EP_MPS_FS 64 + #define USBTEST_ISO_EP_MPS_FS 128 +#endif +#define USBTEST_INT_EP_MPS_HS 512 +#define USBTEST_ISO_EP_MPS_HS 512 + +// Compile-time capability maximum: sizes the source buffers / vendor epbufs for the largest +// packet the build can negotiate. Runtime write lengths follow tud_speed_get() (see main.c) — +// a high-speed build enumerated at full speed submits the _FS lengths. +#define USBTEST_INT_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS) +#define USBTEST_ISO_EP_MPS (TUD_OPT_HIGH_SPEED ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS) + +#endif /* USB_DESCRIPTORS_H_ */ -- cgit v1.3.1 From 24f8bce0bc4a07a69f242ff1e790da90719e984d Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 13 Jul 2026 15:30:44 +0700 Subject: rusb2: EP0 OUT reliability, HS UTMI PHY power-up, FS-only build support - EP0 OUT: park a back-to-back data-stage packet the DCP accepted before PID could go NAK and deliver it into the next armed chunk; flow-control the single-buffer control pipe between chunks (usbtest ctrl_out corruption); discard a packet parked while an OUT pipe was halted so BOT reset recovery's fresh CBW read can't receive stale WRITE data - HS UTMI PHY power-up per the FSP sequence, shared by dcd/hcd: CLKSEL programmed from the board XTAL (EK-RA8M1 runs 20 MHz; the 24 MHz reset default never locks) while DIRPD holds the PHY down, then timed release - hw/bsp(ra8m1_ek): fix U60CK divider macro - BSP_CFG_U60CK_DIV used the generic USB_CLOCK_DIV_8 encoding (7), which USB60CKDIVCR rejects, leaving the USBHS link domain at 480 MHz; the USB60-specific BSP_CLOCKS_USB60_CLOCK_DIV_8 (4) sticks and yields the required 60 MHz from PLL1P - support FS-only builds on the high-speed port: gate SYSCFG.HSE on TUD_OPT_HIGH_SPEED (RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED was a silent no-op) and always compile both hwfifo access widths - the FIFO width belongs to the module, not the link speed (FS builds corrupted odd-length tails: 16-bit access against MBW-32) - iso activate: reset stale pipe bookkeeping so a BRDY firing before the class re-arms can't replay a pre-SET_INTERFACE transfer; write PIPEBUF after PIPESEL selects the pipe (PIPESEL-windowed register) - clear-halt: re-assert BUF on a still-armed OUT pipe (usbtest case 29) - bound the D0FIFO ready spin so an undrained double-buffered IN pipe can't freeze the stack with the IRQ masked - usbtest example: cap interrupt mps at 64 on RUSB2 high speed (pipes 6-9 have a fixed 64-byte buffer, RA6M5 UM 29.1) Verified: usbtest 30/30 on ra6m5_ek (HS), ra4m1_ek (FS) and ra8m1_ek (FS-forced build on the HS port). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg --- examples/device/usbtest/src/usb_descriptors.h | 8 ++- hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h | 5 +- src/portable/renesas/rusb2/dcd_rusb2.c | 90 ++++++++++++++++-------- src/portable/renesas/rusb2/hcd_rusb2.c | 8 +-- src/portable/renesas/rusb2/rusb2_ra.h | 42 +++++++++++ src/tusb_option.h | 2 +- 6 files changed, 116 insertions(+), 39 deletions(-) (limited to 'examples/device/usbtest') diff --git a/examples/device/usbtest/src/usb_descriptors.h b/examples/device/usbtest/src/usb_descriptors.h index 8a033eca9..61b931bd0 100644 --- a/examples/device/usbtest/src/usb_descriptors.h +++ b/examples/device/usbtest/src/usb_descriptors.h @@ -57,7 +57,13 @@ #define USBTEST_INT_EP_MPS_FS 64 #define USBTEST_ISO_EP_MPS_FS 128 #endif -#define USBTEST_INT_EP_MPS_HS 512 +// RUSB2 (Renesas RA) interrupt pipes 6-9 have a fixed 64-byte single buffer at any speed +// (RA6M5 UM R01UH0891 sec 29.1: "Pipes 6 to 9: Interrupt transfer with 64-byte single buffer"). +#if TU_CHECK_MCU(OPT_MCU_RAXXX) + #define USBTEST_INT_EP_MPS_HS 64 +#else + #define USBTEST_INT_EP_MPS_HS 512 +#endif #define USBTEST_ISO_EP_MPS_HS 512 // Compile-time capability maximum: sizes the source buffers / vendor epbufs for the largest diff --git a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h index f2f1ae0c9..25638f02b 100644 --- a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h +++ b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h @@ -51,6 +51,9 @@ #define BSP_CFG_CANFDCLK_DIV (BSP_CLOCKS_CANFD_CLOCK_DIV_8) /* CANFDCLK Div /8 */ #define BSP_CFG_I3CCLK_DIV (BSP_CLOCKS_I3C_CLOCK_DIV_3) /* I3CCLK Div /3 */ #define BSP_CFG_UCK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_5) /* UCK Div /5 */ -#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_8) /* U60CK Div /8 */ +/* U60CK Div /8: PLL1P 480 MHz -> 60 MHz. Hand-fixed: Smart Configurator emitted the USB_ macro + * namespace (BSP_CLOCKS_USB_CLOCK_DIV_8 = 7, rejected by USB60CKDIVCR -> link clock ran at + * 480 MHz); configuration.xml already says u60ck.div.8, so keep the USB60_ macro if regenerating. */ +#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB60_CLOCK_DIV_8) #define BSP_CFG_OCTA_DIV (BSP_CLOCKS_OCTA_CLOCK_DIV_4) /* OCTASPICLK Div /4 */ #endif /* BSP_CLOCK_CFG_H_ */ diff --git a/src/portable/renesas/rusb2/dcd_rusb2.c b/src/portable/renesas/rusb2/dcd_rusb2.c index 5e42f63f5..c93bab05e 100644 --- a/src/portable/renesas/rusb2/dcd_rusb2.c +++ b/src/portable/renesas/rusb2/dcd_rusb2.c @@ -43,6 +43,7 @@ typedef struct static dcd_data_t _dcd; + //--------------------------------------------------------------------+ // INTERNAL OBJECT & FUNCTION DECLARATION //--------------------------------------------------------------------+ @@ -190,6 +191,15 @@ static bool pipe0_xfer_out(rusb2_reg_t *rusb) { pipe_state_t *pipe = &_dcd.pipe[0]; const unsigned rem = pipe->remaining; + // BRDY with no armed transfer: a back-to-back data-stage packet beat the PID=NAK below (the + // host has already ACKed it). Park it in the DCP buffer — an unread buffer NAKs further OUTs — + // and let process_pipe0_xfer deliver it when usbd arms the next chunk. BCLR here would silently + // drop the packet and shift every later chunk by one (usbtest ctrl_out corruption at ra4m1). + if (pipe->buf == NULL && rem == 0) { + rusb->DCPCTR = RUSB2_PIPE_CTR_PID_NAK; + return false; + } + const uint16_t mps = edpt0_max_packet_size(rusb); const uint16_t vld = rusb->CFIFOCTR_b.DTLN; const uint16_t len = tu_min16(tu_min16(rem, mps), vld); @@ -360,7 +370,16 @@ static void process_status_completion(uint8_t rhport) dcd_event_xfer_complete(rhport, ep_addr, 0, XFER_RESULT_SUCCESS, true); } -static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer, +// Report a completed transfer on `num` and reset its bookkeeping. Single completion path for the +// BRDY handler and the EP0 parked-packet drain, so they can't diverge (e.g. on clearing `queued`). +static void pipe_xfer_complete(uint8_t rhport, unsigned num, bool in_isr) { + pipe_state_t *pipe = &_dcd.pipe[num]; + pipe->queued = false; + dcd_event_xfer_complete(rhport, pipe->ep, pipe->length - pipe->remaining, + XFER_RESULT_SUCCESS, in_isr); +} + +static bool process_pipe0_xfer(uint8_t rhport, rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer, uint16_t total_bytes) { uint16_t fifo_sel = (rusb2_is_highspeed_reg(rusb) ? RUSB2_FIFOSEL_MBW_32BIT : RUSB2_FIFOSEL_MBW_16BIT) | FIFOSEL_BIGEND; @@ -386,6 +405,15 @@ static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_ad /* IN */ TU_ASSERT(rusb->DCPCTR_b.BSTS && (rusb->USBREQ & 0x80)); pipe0_xfer_in(rusb); + } else if (rusb->CFIFOCTR_b.DTLN > 0) { + /* OUT: a back-to-back packet parked by pipe0_xfer_out already sits in the DCP buffer (its + BRDY has fired and been cleared) — deliver it into this chunk now; no new BRDY will come + for it. Runs with the USB IRQ masked (dcd_edpt_xfer). Detected via the hardware DTLN + rather than a driver flag: the BCLR at SETUP/bus-reset then self-heals any parked state. */ + if (pipe0_xfer_out(rusb)) { + pipe_xfer_complete(rhport, 0, false); + return true; // PID stays NAK (set by pipe0_xfer_out) until the next chunk is armed + } } rusb->DCPCTR = RUSB2_PIPE_CTR_PID_BUF; } else { @@ -460,11 +488,11 @@ static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_add return true; } -static bool process_edpt_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes) +static bool process_edpt_xfer(uint8_t rhport, rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes) { const unsigned epn = tu_edpt_number(ep_addr); if (0 == epn) { - return process_pipe0_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes); + return process_pipe0_xfer(rhport, rusb, buffer_type, ep_addr, buffer, total_bytes); } else { return process_pipe_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes); } @@ -508,10 +536,7 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num) } } if (completed) { - pipe->queued = false; - dcd_event_xfer_complete(rhport, pipe->ep, - pipe->length - pipe->remaining, - XFER_RESULT_SUCCESS, true); + pipe_xfer_complete(rhport, num, true); // TU_LOG1("C %d %d\r\n", num, pipe->length - pipe->remaining); } } @@ -636,19 +661,8 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { #ifdef RUSB2_SUPPORT_HIGHSPEED if ( rusb2_is_highspeed_rhport(rhport) ) { - rusb->SYSCFG_b.HSE = 1; - - // leave CLKSEL as default (0x11) 24Mhz - - // Power and reset UTMI Phy - uint16_t physet = (rusb->PHYSET | RUSB2_PHYSET_PLLRESET_Msk) & ~RUSB2_PHYSET_DIRPD_Msk; - rusb->PHYSET = physet; - R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); - rusb->PHYSET_b.PLLRESET = 0; - - // set UTMI to operating mode and wait for PLL lock confirmation - rusb->LPSTS_b.SUSPENDM = 1; - while (!rusb->PLLSTA_b.PLLLOCK) {} + rusb->SYSCFG_b.HSE = TUD_OPT_HIGH_SPEED ? 1 : 0; // FS-only build: no HS chirp + rusb2_utmi_phy_powerup(rusb); rusb->SYSCFG_b.DRPD = 0; rusb->SYSCFG_b.USBE = 1; @@ -753,6 +767,10 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) if ( !rusb2_is_highspeed_rhport(rhport) && mps > 256) { return false; } + } else if (xfer == TUSB_XFER_INTERRUPT) { + // Interrupt pipes (6-9) have a fixed 64-byte buffer even in high speed (RA6M5 UM 29.1); + // a larger PIPEMAXP would enumerate, then silently truncate every transfer + TU_ASSERT(mps <= 64); } // Re-opening an endpoint must reuse its pipe: usbd_edpt_close() is a no-op on ISO_ALLOC ports, @@ -770,13 +788,12 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) /* setup pipe */ dcd_int_disable(rhport); + rusb->PIPESEL = num; if ( rusb2_is_highspeed_rhport(rhport) ) { - // FIXME shouldn't be after pipe selection and config, also the BUFNMB should be changed - // depending on the allocation scheme + // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe. + // FIXME BUFNMB is a fixed 0x08 for every pipe; a real per-pipe allocation scheme is needed. rusb->PIPEBUF = 0x7C08; } - - rusb->PIPESEL = num; rusb->PIPEMAXP = mps; volatile uint16_t *ctr = get_pipectr(rusb, num); *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; @@ -860,14 +877,12 @@ bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet _dcd.ep[dir][epn] = num; dcd_int_disable(rhport); + rusb->PIPESEL = (uint16_t) num; if (rusb2_is_highspeed_rhport(rhport)) { - // FIXME (as in dcd_edpt_open): PIPEBUF is a PIPESEL-windowed register (RA6M5 UM §29.2.35) so it - // must be written AFTER PIPESEL selects this pipe, and the fixed BUFNMB=0x08 overlaps every - // HS pipe — a real per-pipe buffer allocator is needed. Left as-is: no RA6M5 HS board on - // the HIL rig to validate a change, and the current mis-ordered write is inert on FS/RA4M1. + // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe. + // FIXME (as in dcd_edpt_open): BUFNMB is a fixed 0x08 for every pipe; a real allocator is needed. rusb->PIPEBUF = 0x7C08; } - rusb->PIPESEL = (uint16_t) num; rusb->PIPEMAXP = largest_packet_size; volatile uint16_t *ctr = get_pipectr(rusb, num); *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; @@ -893,6 +908,13 @@ bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) volatile uint16_t *ctr = get_pipectr(rusb, num); *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; // abort in-flight + reset data toggle *ctr = 0; + // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the + // stale bookkeeping so a BRDY firing before the class re-arms can't replay it + pipe_state_t *pipe = &_dcd.pipe[num]; + pipe->buf = NULL; + pipe->remaining = 0; + pipe->queued = false; + pipe->zlp_pending = false; *ctr = RUSB2_PIPE_CTR_PID_BUF; // enable dcd_int_enable(rhport); return true; @@ -904,7 +926,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t rusb2_reg_t* rusb = RUSB2_REG(rhport); dcd_int_disable(rhport); - bool r = process_edpt_xfer(rusb, 0, ep_addr, buffer, total_bytes); + bool r = process_edpt_xfer(rhport, rusb, 0, ep_addr, buffer, total_bytes); dcd_int_enable(rhport); return r; @@ -917,7 +939,7 @@ bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t * ff, uint16_ rusb2_reg_t* rusb = RUSB2_REG(rhport); dcd_int_disable(rhport); - bool r = process_edpt_xfer(rusb, 1, ep_addr, ff, total_bytes); + bool r = process_edpt_xfer(rhport, rusb, 1, ep_addr, ff, total_bytes); dcd_int_enable(rhport); return r; @@ -952,6 +974,12 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) } else { const unsigned num = _dcd.ep[0][tu_edpt_number(ep_addr)]; rusb->PIPESEL = (uint16_t)num; + // Drop any packet parked in the buffer while halted: a data-OUT packet the host sent before + // aborting its transfer would otherwise be delivered into the next read after recovery + // (BOT reset + clear-halt re-arms a 31-byte CBW read which then receives stale WRITE data, + // "SCSI CBW is not valid" -> stall -> reset loop; ra6m5 msc write wedge). + *ctr = RUSB2_PIPE_CTR_ACLRM_Msk; + *ctr = 0; // Non-bulk OUT re-enables straight away. Bulk OUT is normally armed together with its transaction // counter (TRE) by process_pipe_xfer(), so we don't blindly re-enable it here — but if a receive // was already armed (still queued), SQCLR above just left it NAKing. Re-assert BUF so it keeps diff --git a/src/portable/renesas/rusb2/hcd_rusb2.c b/src/portable/renesas/rusb2/hcd_rusb2.c index 849551d27..489162e54 100644 --- a/src/portable/renesas/rusb2/hcd_rusb2.c +++ b/src/portable/renesas/rusb2/hcd_rusb2.c @@ -454,11 +454,9 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { if (rusb2_is_highspeed_rhport(rhport) ) { rusb->SYSCFG_b.HSE = 1; rusb->PHYSET_b.HSEB = 0; - rusb->PHYSET_b.DIRPD = 0; - R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); - rusb->PHYSET_b.PLLRESET = 0; - rusb->LPSTS_b.SUSPENDM = 1; - while ( !rusb->PLLSTA_b.PLLLOCK ); + // same PHY reference-clock + power-up requirements as dcd_init: without CLKSEL matching the + // board XTAL the PLL never locks and the wait below would spin forever (e.g. EK-RA8M1, 20 MHz) + rusb2_utmi_phy_powerup(rusb); rusb->SYSCFG_b.DRPD = 1; rusb->SYSCFG_b.DCFM = 1; rusb->SYSCFG_b.DPRPU = 0; diff --git a/src/portable/renesas/rusb2/rusb2_ra.h b/src/portable/renesas/rusb2/rusb2_ra.h index e5945ffe2..0954d0d25 100644 --- a/src/portable/renesas/rusb2/rusb2_ra.h +++ b/src/portable/renesas/rusb2/rusb2_ra.h @@ -49,6 +49,23 @@ typedef struct { #define rusb2_is_highspeed_rhport(_p) (_p == 1) #define rusb2_is_highspeed_reg(_reg) (_reg == RUSB2_REG(1)) + + // UTMI PHY reference clock is the main oscillator: PHYSET.CLKSEL must match the board XTAL + // before the PHY PLL is released (RA6M5 UM R01UH0891 29.2.17: 00=12 MHz, 10=20 MHz, + // 11=24 MHz reset default). EK-RA6M5 runs 24 MHz (default works); EK-RA8M1 runs 20 MHz and + // never locks/chirps on the default. A board with a non-standard USB clocking scheme can + // pre-define RUSB2_PHYSET_CLKSEL_VALUE to override this selection. + #ifndef RUSB2_PHYSET_CLKSEL_VALUE + #if BSP_CFG_XTAL_HZ == 12000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 0u + #elif BSP_CFG_XTAL_HZ == 20000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 2u + #elif BSP_CFG_XTAL_HZ == 24000000 + #define RUSB2_PHYSET_CLKSEL_VALUE 3u + #else + #error "USBHS UTMI PHY: no PHYSET.CLKSEL encoding for this BSP_CFG_XTAL_HZ; define RUSB2_PHYSET_CLKSEL_VALUE" + #endif + #endif #else #define RUSB2_CONTROLLER_COUNT 1 @@ -84,6 +101,31 @@ TU_ATTR_ALWAYS_INLINE static inline void rusb2_int_disable(uint8_t rhport) { TU_ATTR_ALWAYS_INLINE static inline void rusb2_phy_init(void) { } +#ifdef RUSB2_SUPPORT_HIGHSPEED +// UTMI PHY power-up per the FSP reference sequence (r_usb_preg_access.c), shared by dcd_init and +// hcd_init: program CLKSEL to the board XTAL while the PHY is powered down (DIRPD=1), 1 us, +// release DIRPD, 1 ms, release PLLRESET, then wait for PLL lock. Changing CLKSEL as the PHY +// powers up gets mis-sampled (EK-RA8M1, 20 MHz). +static inline void rusb2_utmi_phy_powerup(rusb2_reg_t* rusb) { + uint16_t physet = rusb->PHYSET | RUSB2_PHYSET_DIRPD_Msk; + rusb->PHYSET = physet; + #ifdef RUSB2_PHYSET_CLKSEL_VALUE + physet = (uint16_t) ((physet & ~RUSB2_PHYSET_CLKSEL_Msk) | + (RUSB2_PHYSET_CLKSEL_VALUE << RUSB2_PHYSET_CLKSEL_Pos)); + rusb->PHYSET = physet; + #endif + R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MICROSECONDS); + physet &= (uint16_t) ~RUSB2_PHYSET_DIRPD_Msk; + rusb->PHYSET = physet; + R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS); + rusb->PHYSET_b.PLLRESET = 0; + + // set UTMI to operating mode and wait for PLL lock confirmation + rusb->LPSTS_b.SUSPENDM = 1; + while (!rusb->PLLSTA_b.PLLLOCK) {} +} +#endif + #ifdef __cplusplus } #endif diff --git a/src/tusb_option.h b/src/tusb_option.h index e19ee1629..65cf747e2 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -376,7 +376,7 @@ //------------ RUSB2 --------------// #if defined(TUP_USBIP_RUSB2) #define CFG_TUD_EDPT_DEDICATED_HWFIFO 1 - #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | (TUD_OPT_HIGH_SPEED ? 4 : 0)) // 16 bit and 32 bit if highspeed + #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | 4) // HS module uses 32-bit access at any link speed (e.g. FS-forced build) #define CFG_TUSB_FIFO_HWFIFO_ADDR_STRIDE 0 #define CFG_TUSB_FIFO_HWFIFO_CUSTOM_WRITE // custom write since rusb2 can change access width 32 -> 16 and can write // odd byte with byte access -- cgit v1.3.1 From e02f93158cc602ba6f20945a187172abf2546718 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 13 Jul 2026 17:53:34 +0700 Subject: test/hil: usbtest fleet enablement, shuffled scheduling, unique PIDs Pool/config: - record real uids (ra8m1_ek), enable usbtest for espressif s3/p4, then park ra6m5_ek and ra8m1_ek in boards-skip (ra6m5's usbtest/MSC traffic can kill the uPD720201 host on its ROM firmware; ra8m1 USBHS bring-up pending); max32666/nrf54lm20 stay enabled - their MosChip flakiness never wedges - re-enable device/usbtest on HS boards (mimxrt1064, ch32v307) now that uPD720201 firmware 2.0.2.6 fixes the command-ring death; mimxrt1015 stays skipped - its HS battery killed the controller on both ROM and 2.0.2.6 firmware (board-specific); match the moved host-test bundles (f723 <-> rt1064); skip never-passing tests on the new nrf5340dk/nrf54lm20dk boards and the detached pico host bundle, each documented with a comment Host-controller quirk gating in usbtest.py (auto-skip, self-heals on a healthy xHCI): - MosChip MCS9990 EHCI: case 25 (int-OUT never scheduled, FRINDEX bug) and case 11 (unlinked reads complete short/EREMOTEIO) - Renesas uPD720201 xHCI: firmware-gated. The card must run firmware >= 2.0.2.6 (RAM-uploaded - it reverts to ROM on every power cycle): on older firmware the command ring dies under unlink stress (a Configure Endpoint command stops completing; the hub worker deadlocks holding the device lock; only a host power cycle recovers; three boards reproduced it). usbtest.py reads the FW version register (PCI config 0x6c) and refuses to run at all on older firmware - hil_test surfaces that as a failed test with the reason. On current firmware the full 30-case battery runs (validated FS+HS: metro_m4, f723, f723-DMA all 30/30). Scheduling (hil_test.py): - Shuffle each (board, variant)'s test order with a seeded RNG (HIL_SHUFFLE_SEED to replay) so usbtest batteries and flash churn spread across the timeline instead of convoying on one controller. - Per-controller usbtest + flash semaphores: HIL_USBTEST_PARALLEL (default 4) concurrent usbtest batteries and HIL_FLASH_PARALLEL (default 8) concurrent flashes per host controller. Profiled on uPD720201 firmware 2.0.2.6 across 8/1..12/8: wall time falls 22.2/14.3/12.5/10.8 min at usbtest width 1/2/3/4 and plateaus there; zero controller errors everywhere; first battery case failures (leaf-hub bandwidth stretch) appear at 12/8, and flash width 12 only amplifies flasher-hub contention flakes - so 8/4 is the optimum. A separate battery-window flash throttle was profiled and dropped. - Give every example a unique hardcoded USB PID (0x4001-0x4022, usbtest keeps 0x4010) instead of the PID_MAP interface bitmap: different examples now always re-enumerate back-to-back, even on boards whose CPU reset does not drop D+ (WCH CH58x), so the EXAMPLE_PID table and same-PID adjacency reordering in hil_test.py are gone; only the variant-boundary same-example repeat needs a swap. - Report matrix: stable columns with the metric-bearing tests pinned first (usbtest, cdc_msc_throughput, msc_file_explorer[_freertos]), the rest alphabetical. Fail fast: - enum wait budget 8 s on the first attempt, 4 s on retries; dfu waits are deadline-based so dfu-util's own runtime counts against the budget. A device-absent failure now costs ~3-5x a passing test (20-30 s) instead of 10-30x (47-150 s). - CI runs hil_test with --retry 1 and no in-run second pass: a broken fixture fails the job fast instead of holding the self-hosted runner for hours and blocking other PRs' HIL jobs. hil_test still writes the .skip sidecar, so a manual re-run attempt only retests what failed. Review fixes (multi-agent adversarial review of this commit): - tinyusb_win_usbser.inf: the PID rework moved five CDC examples onto even PIDs the INF's odd-only DeviceList never matched (legacy-Windows usbser binding) - appended 0x4006/4008/400a/4020/4022 to both lists. - usbtest example: USBTEST_TIER is now overridable and the descriptors and pumps are tier-conditional, so a board whose DCD cannot serve a tier lowers it instead of skipping the whole example - RA2A1 (RUSB2 with no isochronous pipe) builds at tier 3 via its BOARD_ define; the host battery follows the tier advertised in bcdDevice. Tier-4 output verified byte-identical after the refactor. - dynamic_configuration's second config derived USB_PID + 11 = 0x4018, colliding with net_lwip_webserver - now USB_PID + 0x0100, outside the per-example space. tools/check_example_pids.py (pre-commit hook) enforces PID uniqueness incl. derived and literal idProduct values. - usbtest.py firmware gate: matched by device ID (uPD720201/720202, both use the 0x6c FW register), and an unreadable version (setpci missing/denied) now refuses with its own message instead of masquerading as "firmware 0x00000000"; noted the gate is necessary but not sufficient (board-specific kills stay per-board skips). - hil_test: deadline waits use time.monotonic(); multiprocessing context pinned to fork (raw semaphores in Pool initargs); flash and usbtest permits unified into one fail-closed, exception-safe ctrl_permit (unknown controller takes every slot and logs a warning instead of silently borrowing slot 0); an all-skipped battery reports as skip, not "0/0" failure; slow-body polls (mtp, printer, disk read) go through a shared deadline-based wait_until so their bodies count against the enum budget; throughput's FS detection compares serials case-insensitively like every other walk; a missing MSC read-speed line now fails the host msc_file_explorer test instead of passing with an empty metric. Hardening: - fail fast (15 s) when a driver-registry sysfs write blocks: a wedged device otherwise turns every subsequent battery into an unkillable D-state writer and silently hangs the whole run - usb-recover skill: a VM reboot is not a reliable cure (MosChip hubs latch up across the PCIe reset); full host power cycle is Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg --- .claude/skills/usb-recover/SKILL.md | 16 +- .github/workflows/build.yml | 14 +- .pre-commit-config.yaml | 7 + .../audio_4_channel_mic/src/usb_descriptors.c | 11 +- .../src/usb_descriptors.c | 11 +- examples/device/audio_test/src/usb_descriptors.c | 11 +- .../audio_test_freertos/src/usb_descriptors.c | 11 +- .../audio_test_multi_rate/src/usb_descriptors.c | 11 +- .../device/cdc_dual_ports/src/usb_descriptors.c | 11 +- examples/device/cdc_msc/src/usb_descriptors.c | 11 +- .../device/cdc_msc_freertos/src/usb_descriptors.c | 11 +- .../cdc_msc_throughput/src/usb_descriptors.c | 3 +- examples/device/cdc_uac2/src/usb_descriptors.c | 11 +- examples/device/dfu/src/usb_descriptors.c | 10 +- examples/device/dfu_runtime/src/usb_descriptors.c | 10 +- .../dynamic_configuration/src/usb_descriptors.c | 13 +- .../hid_boot_interface/src/usb_descriptors.c | 10 +- .../device/hid_composite/src/usb_descriptors.c | 11 +- .../hid_composite_freertos/src/usb_descriptors.c | 11 +- .../device/hid_generic_inout/src/usb_descriptors.c | 11 +- .../hid_multiple_interface/src/usb_descriptors.c | 11 +- examples/device/midi_test/src/usb_descriptors.c | 11 +- .../midi_test_freertos/src/usb_descriptors.c | 11 +- examples/device/msc_dual_lun/src/usb_descriptors.c | 11 +- examples/device/mtp/src/usb_descriptors.c | 11 +- .../net_lwip_webserver/src/usb_descriptors.c | 12 +- .../device/printer_to_cdc/src/usb_descriptors.c | 2 +- examples/device/uac2_headset/src/usb_descriptors.c | 11 +- .../device/uac2_speaker_fb/src/usb_descriptors.c | 11 +- examples/device/usbtest/src/main.c | 8 + examples/device/usbtest/src/usb_descriptors.c | 24 +- examples/device/usbtest/src/usb_descriptors.h | 11 +- examples/device/usbtmc/src/usb_descriptors.c | 11 +- .../device/video_capture/src/usb_descriptors.c | 11 +- .../device/video_capture_2ch/src/usb_descriptors.c | 11 +- .../device/webusb_serial/src/usb_descriptors.c | 11 +- examples/dual/dynamic_switch/src/usb_descriptors.c | 11 +- .../host_hid_to_device_cdc/src/usb_descriptors.c | 11 +- .../host_info_to_device_cdc/src/usb_descriptors.c | 11 +- test/hil/hil_test.py | 356 ++++++++++++++++----- test/hil/tinyusb.json | 83 ++++- test/hil/usbtest.py | 77 ++++- tools/check_example_pids.py | 55 ++++ tools/usb_drivers/tinyusb_win_usbser.inf | 4 +- 44 files changed, 582 insertions(+), 419 deletions(-) create mode 100644 tools/check_example_pids.py (limited to 'examples/device/usbtest') diff --git a/.claude/skills/usb-recover/SKILL.md b/.claude/skills/usb-recover/SKILL.md index 7f3e86632..3f72b7e21 100644 --- a/.claude/skills/usb-recover/SKILL.md +++ b/.claude/skills/usb-recover/SKILL.md @@ -36,7 +36,9 @@ the ioctl then returns and the convoy unwinds on its own. **Not every controller supports FLR.** The Renesas uPD720201 (`0000:01:00.0`) has no reset method — `pci-reset` fails with `Inappropriate ioctl for device` -(ENOTTY). On those, there is no clean D-state cure short of a **reboot**; do NOT +(ENOTTY). On those, there is no clean software D-state cure — a VM reboot is NOT +reliable (the MosChip downstream hubs latch up across the PCIe reset and need a +physical replug); ask the operator for a full PVE host power cycle instead. Do NOT fall through to `pci-rebind` (see next). **`pci-rebind` can strand the controller driverless.** Its unbind succeeds but, @@ -44,8 +46,8 @@ with a D-state process still holding a URB, the *re-bind* hangs — leaving the PCI device with **no driver** (`/sys/bus/pci/devices//driver` gone) and the whole controller's fixtures offline. A second `pci-rebind` then dies with "no driver bound". Recover with `pci-bind ` (re-attaches the xHCI driver); -if that also hangs because the D-state URB is unkillable, **reboot** is the only -cure. The Renesas binds via `xhci-pci-renesas` (firmware loader), others via +if that also hangs because the D-state URB is unkillable, only a full PVE host +power cycle (operator action) recovers. The Renesas binds via `xhci-pci-renesas` (firmware loader), others via `xhci_hcd` — `pci-bind` auto-tries both, or pass the driver explicitly. **Ordering is critical.** `authorized`/`rebind`/`pci-rebind` all take the @@ -53,7 +55,7 @@ per-device lock the stuck ioctl holds — they block and join the convoy, and soon every libusb tool (uhubctl, JLinkExe) hangs too. Worse, a blocked `pci-rebind` grabs the PCI device lock on its way in, which `pci-reset` also needs: once a rebind has been attempted and is stuck, even FLR deadlocks and -**only a rig reboot recovers**. pci-reset first (if supported), and never +**only a full PVE host power cycle recovers**. pci-reset first (if supported), and never `pci-rebind` a D-state wedge. **If no** (device merely dead or silent), escalate gently: @@ -82,10 +84,10 @@ port power switching — uhubctl reports "No compatible devices" there. - Command produces no output and doesn't return → it is blocked on the device lock: a D-state holder exists; see above. - Trying `pci-rebind` on a D-state hang — its re-bind hangs and strands the - controller **driverless**; recover with `pci-bind `, or reboot if the - D-state URB is unkillable. Use `pci-reset` (if supported) for D-state, never + controller **driverless**; recover with `pci-bind `, or a PVE host power + cycle if the D-state URB is unkillable. Use `pci-reset` (if supported) for D-state, never `pci-rebind`. - Running `pci-reset` on a controller without FLR support (Renesas) → ENOTTY; - no recovery but reboot. + no software recovery — needs a PVE host power cycle. - A J-Link reset (`r; go`) does not disconnect a wedged DUT from the host: the DWC2 soft-connect pullup stays up through a core halt, so stuck URBs stay stuck. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8c597e50..f24f3ae1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -333,15 +333,11 @@ jobs: merge-multiple: true - name: Test on actual hardware - run: | - python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS || \ - (if [ -f "${{ env.HIL_JSON }}.skip" ]; then - SKIP_BOARDS=$(cat "${{ env.HIL_JSON }}.skip") - echo "Re-running with SKIP_BOARDS=$SKIP_BOARDS" - python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS - else - exit 1 - fi) + # Single attempt per test (--retry 1), no in-run second pass: a broken fixture + # fails fast instead of holding the runner (and other PRs' HIL jobs) for hours. + # hil_test.py still writes ${HIL_JSON}.skip, so a manual re-run attempt only + # retests what failed (see "Get Skip Boards from previous run"). + run: python3 test/hil/hil_test.py --retry 1 ${{ env.HIL_JSON }} $SKIP_BOARDS - name: Upload HIL report if: always() && github.event_name == 'pull_request' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4a297289..e87b935dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,13 @@ repos: - repo: local hooks: + - id: unique-example-pids + name: unique example USB PIDs + files: usb_descriptors\.c$ + entry: python3 tools/check_example_pids.py + pass_filenames: false + language: system + - id: unit-test name: unit-test files: ^(src/|test/unit-test/) diff --git a/examples/device/audio_4_channel_mic/src/usb_descriptors.c b/examples/device/audio_4_channel_mic/src/usb_descriptors.c index 6b9a9bbae..42da9442c 100644 --- a/examples/device/audio_4_channel_mic/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4001 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c index 216cd062a..0afb3df0a 100644 --- a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4002 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test/src/usb_descriptors.c b/examples/device/audio_test/src/usb_descriptors.c index cea4eb8d1..8c25fc290 100644 --- a/examples/device/audio_test/src/usb_descriptors.c +++ b/examples/device/audio_test/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4003 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test_freertos/src/usb_descriptors.c b/examples/device/audio_test_freertos/src/usb_descriptors.c index 37ebf84d3..709425c49 100644 --- a/examples/device/audio_test_freertos/src/usb_descriptors.c +++ b/examples/device/audio_test_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4004 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/audio_test_multi_rate/src/usb_descriptors.c b/examples/device/audio_test_multi_rate/src/usb_descriptors.c index b1f60dd10..008c8cd76 100644 --- a/examples/device/audio_test_multi_rate/src/usb_descriptors.c +++ b/examples/device/audio_test_multi_rate/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4005 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/cdc_dual_ports/src/usb_descriptors.c b/examples/device/cdc_dual_ports/src/usb_descriptors.c index adfd8cf9d..779221c0c 100644 --- a/examples/device/cdc_dual_ports/src/usb_descriptors.c +++ b/examples/device/cdc_dual_ports/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4006 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc/src/usb_descriptors.c b/examples/device/cdc_msc/src/usb_descriptors.c index 5dc80dee3..140ef2140 100644 --- a/examples/device/cdc_msc/src/usb_descriptors.c +++ b/examples/device/cdc_msc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4007 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc_freertos/src/usb_descriptors.c b/examples/device/cdc_msc_freertos/src/usb_descriptors.c index f5b015051..8398f0365 100644 --- a/examples/device/cdc_msc_freertos/src/usb_descriptors.c +++ b/examples/device/cdc_msc_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4008 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_msc_throughput/src/usb_descriptors.c b/examples/device/cdc_msc_throughput/src/usb_descriptors.c index 3b0ff6e17..ba0b0a26f 100644 --- a/examples/device/cdc_msc_throughput/src/usb_descriptors.c +++ b/examples/device/cdc_msc_throughput/src/usb_descriptors.c @@ -26,7 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -#define USB_PID (0x4000 | ((CFG_TUD_CDC) ? (1 << 0) : 0) | ((CFG_TUD_MSC) ? (1 << 1) : 0)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4009 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/cdc_uac2/src/usb_descriptors.c b/examples/device/cdc_uac2/src/usb_descriptors.c index fdffc761e..9c1bbee47 100644 --- a/examples/device/cdc_uac2/src/usb_descriptors.c +++ b/examples/device/cdc_uac2/src/usb_descriptors.c @@ -29,15 +29,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400a //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dfu/src/usb_descriptors.c b/examples/device/dfu/src/usb_descriptors.c index e4291be2d..5bfb32b2e 100644 --- a/examples/device/dfu/src/usb_descriptors.c +++ b/examples/device/dfu/src/usb_descriptors.c @@ -26,14 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400b //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dfu_runtime/src/usb_descriptors.c b/examples/device/dfu_runtime/src/usb_descriptors.c index 8fa078da2..273566414 100644 --- a/examples/device/dfu_runtime/src/usb_descriptors.c +++ b/examples/device/dfu_runtime/src/usb_descriptors.c @@ -26,14 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400c //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/dynamic_configuration/src/usb_descriptors.c b/examples/device/dynamic_configuration/src/usb_descriptors.c index c4049414f..838052ea1 100644 --- a/examples/device/dynamic_configuration/src/usb_descriptors.c +++ b/examples/device/dynamic_configuration/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400d // Configuration mode // 0 : enumerated as CDC/MIDI. Board button is not pressed when enumerating @@ -79,7 +72,7 @@ tusb_desc_device_t const desc_device_1 = .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = 0xCafe, - .idProduct = USB_PID + 11, // should be different PID than desc0 + .idProduct = USB_PID + 0x0100, // must differ from desc0's PID and stay outside the 0x40xx per-example space .bcdDevice = 0x0100, .iManufacturer = 0x01, diff --git a/examples/device/hid_boot_interface/src/usb_descriptors.c b/examples/device/hid_boot_interface/src/usb_descriptors.c index b5c31a94a..4d5caa835 100644 --- a/examples/device/hid_boot_interface/src/usb_descriptors.c +++ b/examples/device/hid_boot_interface/src/usb_descriptors.c @@ -27,14 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400e //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/hid_composite/src/usb_descriptors.c b/examples/device/hid_composite/src/usb_descriptors.c index 46e4b63f9..7f5f74c70 100644 --- a/examples/device/hid_composite/src/usb_descriptors.c +++ b/examples/device/hid_composite/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x400f #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/hid_composite_freertos/src/usb_descriptors.c b/examples/device/hid_composite_freertos/src/usb_descriptors.c index a745c17b5..a0464afae 100644 --- a/examples/device/hid_composite_freertos/src/usb_descriptors.c +++ b/examples/device/hid_composite_freertos/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4011 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/hid_generic_inout/src/usb_descriptors.c b/examples/device/hid_generic_inout/src/usb_descriptors.c index 93e718461..f179b74f7 100644 --- a/examples/device/hid_generic_inout/src/usb_descriptors.c +++ b/examples/device/hid_generic_inout/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4012 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/hid_multiple_interface/src/usb_descriptors.c b/examples/device/hid_multiple_interface/src/usb_descriptors.c index cd2d93c44..90aef6dd7 100644 --- a/examples/device/hid_multiple_interface/src/usb_descriptors.c +++ b/examples/device/hid_multiple_interface/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4013 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/midi_test/src/usb_descriptors.c b/examples/device/midi_test/src/usb_descriptors.c index 99c798ce1..fc7228c35 100644 --- a/examples/device/midi_test/src/usb_descriptors.c +++ b/examples/device/midi_test/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4014 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/midi_test_freertos/src/usb_descriptors.c b/examples/device/midi_test_freertos/src/usb_descriptors.c index 99c798ce1..bfdbc555e 100644 --- a/examples/device/midi_test_freertos/src/usb_descriptors.c +++ b/examples/device/midi_test_freertos/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4015 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/msc_dual_lun/src/usb_descriptors.c b/examples/device/msc_dual_lun/src/usb_descriptors.c index b328cf17f..5e036a8c4 100644 --- a/examples/device/msc_dual_lun/src/usb_descriptors.c +++ b/examples/device/msc_dual_lun/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4016 //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/mtp/src/usb_descriptors.c b/examples/device/mtp/src/usb_descriptors.c index 4c840560e..fefa8a239 100644 --- a/examples/device/mtp/src/usb_descriptors.c +++ b/examples/device/mtp/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] MTP | VENDOR | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) | PID_MAP(MTP, 5)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4017 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/net_lwip_webserver/src/usb_descriptors.c b/examples/device/net_lwip_webserver/src/usb_descriptors.c index 09090bb92..85a6a420c 100644 --- a/examples/device/net_lwip_webserver/src/usb_descriptors.c +++ b/examples/device/net_lwip_webserver/src/usb_descriptors.c @@ -27,16 +27,8 @@ #include "class/net/net_device.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] NET | VENDOR | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID \ - (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) | \ - PID_MAP(ECM_RNDIS, 5) | PID_MAP(NCM, 5)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4018 // String Descriptor Index enum { diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c index db7bfe97a..b9450c87e 100644 --- a/examples/device/printer_to_cdc/src/usb_descriptors.c +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -29,7 +29,7 @@ #include "usb_descriptors.h" #define USB_VID 0xCafe -#define USB_PID 0x4005 +#define USB_PID 0x4019 #define USB_BCD 0x0200 //--------------------------------------------------------------------+ diff --git a/examples/device/uac2_headset/src/usb_descriptors.c b/examples/device/uac2_headset/src/usb_descriptors.c index b554e7195..1615b92ec 100644 --- a/examples/device/uac2_headset/src/usb_descriptors.c +++ b/examples/device/uac2_headset/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401a //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/uac2_speaker_fb/src/usb_descriptors.c b/examples/device/uac2_speaker_fb/src/usb_descriptors.c index f0c780e38..40a36cbf4 100644 --- a/examples/device/uac2_speaker_fb/src/usb_descriptors.c +++ b/examples/device/uac2_speaker_fb/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "usb_descriptors.h" #include "common_types.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VENDOR, 5) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401b //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/device/usbtest/src/main.c b/examples/device/usbtest/src/main.c index 78575ac9b..e57a90161 100644 --- a/examples/device/usbtest/src/main.c +++ b/examples/device/usbtest/src/main.c @@ -61,7 +61,9 @@ static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED; // unintended short packet or ZLP. static uint8_t const tx_chunk[CFG_TUD_VENDOR_TX_EPSIZE]; static uint8_t const int_tx_chunk[USBTEST_INT_EP_MPS]; +#if USBTEST_TIER >= 4 static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS]; +#endif // Interrupt/iso submit one packet per (micro)frame, sized to the NEGOTIATED speed's mps — a // high-speed build enumerated at full speed must submit the FS length, not the HS-capacity buffer @@ -69,9 +71,11 @@ static uint8_t const iso_tx_chunk[USBTEST_ISO_EP_MPS]; static inline uint16_t usbtest_int_len(void) { return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_INT_EP_MPS_HS : USBTEST_INT_EP_MPS_FS; } +#if USBTEST_TIER >= 4 static inline uint16_t usbtest_iso_len(void) { return (tud_speed_get() == TUSB_SPEED_HIGH) ? USBTEST_ISO_EP_MPS_HS : USBTEST_ISO_EP_MPS_FS; } +#endif //------------- prototypes -------------// void led_blinking_task(void* param); @@ -125,10 +129,12 @@ static void usbtest_pump(void) { tud_vendor_int_write(int_tx_chunk, usbtest_int_len()); } +#if USBTEST_TIER >= 4 tud_vendor_iso_read_xfer(); // isochronous sink if (tud_vendor_iso_write_available()) { tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); } +#endif } } @@ -175,6 +181,7 @@ void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) { // Isochronous pair: same discard/refill pumps; a completion may be a missed // frame, re-arm regardless +#if USBTEST_TIER >= 4 void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t* buffer, uint32_t bufsize) { (void) idx; (void) buffer; @@ -187,6 +194,7 @@ void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) { (void) sent_bytes; tud_vendor_iso_write(iso_tx_chunk, usbtest_iso_len()); } +#endif //--------------------------------------------------------------------+ // Vendor control requests (EP0) diff --git a/examples/device/usbtest/src/usb_descriptors.c b/examples/device/usbtest/src/usb_descriptors.c index 24efef453..b4f46adb8 100644 --- a/examples/device/usbtest/src/usb_descriptors.c +++ b/examples/device/usbtest/src/usb_descriptors.c @@ -67,21 +67,29 @@ enum { // Vendor interface, Gadget-Zero style altsettings: alt 0 carries no endpoints (an // isochronous endpoint must not claim bandwidth in the default altsetting, USB 2.0 -// 5.6.3), alt 1 carries bulk + interrupt + isochronous IN/OUT. The host usbtest -// driver skips altsettings without pipes and selects alt 1 itself. No TUD_ macro -// covers this layout, hand-rolled. -#define USBTEST_DESC_LEN (9 + 9 + 6*7) +// 5.6.3), alt 1 carries bulk + interrupt (+ isochronous IN/OUT at tier 4). The host +// usbtest driver skips altsettings without pipes and selects alt 1 itself. No TUD_ +// macro covers this layout, hand-rolled. +#if USBTEST_TIER >= 4 + #define USBTEST_EP_COUNT 6 + #define USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) \ + ,7, TUSB_DESC_ENDPOINT, _isoout, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval,\ + 7, TUSB_DESC_ENDPOINT, _isoin, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval +#else + #define USBTEST_EP_COUNT 4 + #define USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) +#endif +#define USBTEST_DESC_LEN (9 + 9 + USBTEST_EP_COUNT*7) #define USBTEST_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _bulk_mps, _intout, _intin, _int_mps, _int_interval, _isoout, _isoin, _iso_mps, _iso_interval) \ /* alt 0: zero bandwidth, no endpoints */\ 9, TUSB_DESC_INTERFACE, _itfnum, 0, 0, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ /* alt 1: full source/sink set */\ - 9, TUSB_DESC_INTERFACE, _itfnum, 1, 6, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ + 9, TUSB_DESC_INTERFACE, _itfnum, 1, USBTEST_EP_COUNT, TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, _stridx,\ 7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_bulk_mps), 0,\ 7, TUSB_DESC_ENDPOINT, _intout, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\ - 7, TUSB_DESC_ENDPOINT, _intin, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval,\ - 7, TUSB_DESC_ENDPOINT, _isoout, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval,\ - 7, TUSB_DESC_ENDPOINT, _isoin, (uint8_t)(TUSB_XFER_ISOCHRONOUS | (uint8_t)(TUSB_ISO_EP_ATT_ASYNCHRONOUS)), U16_TO_U8S_LE(_iso_mps), _iso_interval + 7, TUSB_DESC_ENDPOINT, _intin, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_int_mps), _int_interval\ + USBTEST_ISO_EPS(_isoout, _isoin, _iso_mps, _iso_interval) #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + USBTEST_DESC_LEN) diff --git a/examples/device/usbtest/src/usb_descriptors.h b/examples/device/usbtest/src/usb_descriptors.h index 61b931bd0..bcf8b5ec4 100644 --- a/examples/device/usbtest/src/usb_descriptors.h +++ b/examples/device/usbtest/src/usb_descriptors.h @@ -32,7 +32,16 @@ // 2: + vendor control 0x5b/0x5c (ctrl_out) // 3: + interrupt source/sink // 4: + isochronous source/sink -#define USBTEST_TIER 4 +// Default is the full tier 4; a board whose DCD cannot serve a tier lowers it here +// (BOARD_ is defined by both build systems) and the host battery follows. +#ifndef USBTEST_TIER + #if defined(BOARD_RA2A1_EK) + // RA2A1's RUSB2 instance has no isochronous pipe (other RA parts have pipes 1-2) + #define USBTEST_TIER 3 + #else + #define USBTEST_TIER 4 + #endif +#endif // Interrupt/isochronous endpoint max packet sizes, must match the configuration descriptor. // TUD_OPT_HIGH_SPEED is a compile-time capability flag, NOT the live bus speed, so the full-speed diff --git a/examples/device/usbtmc/src/usb_descriptors.c b/examples/device/usbtmc/src/usb_descriptors.c index ecdcef834..5ba5d9367 100644 --- a/examples/device/usbtmc/src/usb_descriptors.c +++ b/examples/device/usbtmc/src/usb_descriptors.c @@ -28,15 +28,8 @@ #include "class/usbtmc/usbtmc.h" #include "class/usbtmc/usbtmc_device.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401c #define USB_VID 0xcafe #define USB_BCD 0x0200 diff --git a/examples/device/video_capture/src/usb_descriptors.c b/examples/device/video_capture/src/usb_descriptors.c index b3382c82d..d5d805f0b 100644 --- a/examples/device/video_capture/src/usb_descriptors.c +++ b/examples/device/video_capture/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] VIDEO | AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VIDEO, 5) | PID_MAP(VENDOR, 6) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401d #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/video_capture_2ch/src/usb_descriptors.c b/examples/device/video_capture_2ch/src/usb_descriptors.c index 8dc986da6..ad65cc019 100644 --- a/examples/device/video_capture_2ch/src/usb_descriptors.c +++ b/examples/device/video_capture_2ch/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] VIDEO | AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(AUDIO, 4) | PID_MAP(VIDEO, 5) | PID_MAP(VENDOR, 6) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401e #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/device/webusb_serial/src/usb_descriptors.c b/examples/device/webusb_serial/src/usb_descriptors.c index 527837161..5986bffdf 100644 --- a/examples/device/webusb_serial/src/usb_descriptors.c +++ b/examples/device/webusb_serial/src/usb_descriptors.c @@ -27,15 +27,8 @@ #include "tusb.h" #include "usb_descriptors.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x401f //--------------------------------------------------------------------+ // Device Descriptors diff --git a/examples/dual/dynamic_switch/src/usb_descriptors.c b/examples/dual/dynamic_switch/src/usb_descriptors.c index ef6d795b7..c6d80e2ab 100644 --- a/examples/dual/dynamic_switch/src/usb_descriptors.c +++ b/examples/dual/dynamic_switch/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4)) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4020 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c b/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c index 3efa30e20..3dc32e3f4 100644 --- a/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c +++ b/examples/dual/host_hid_to_device_cdc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4021 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c b/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c index 3efa30e20..4fa3bcbc7 100644 --- a/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c +++ b/examples/dual/host_info_to_device_cdc/src/usb_descriptors.c @@ -26,15 +26,8 @@ #include "bsp/board_api.h" #include "tusb.h" -/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. - * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. - * - * Auto ProductID layout's Bitmap: - * [MSB] HID | MSC | CDC [LSB] - */ -#define PID_MAP(itf, n) ((CFG_TUD_##itf) ? (1 << (n)) : 0) -#define USB_PID (0x4000 | PID_MAP(CDC, 0) | PID_MAP(MSC, 1) | PID_MAP(HID, 2) | \ - PID_MAP(MIDI, 3) | PID_MAP(VENDOR, 4) ) +// Unique PID per example: guarantees re-enumeration on re-flash and a fresh host driver match. +#define USB_PID 0x4022 #define USB_VID 0xCafe #define USB_BCD 0x0200 diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index 862d2b5bd..5f4cef7a6 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -53,14 +53,46 @@ import serial import subprocess import json import glob -from multiprocessing import Pool, Lock +import multiprocessing from multiprocessing import TimeoutError as MpTimeoutError + +# Raw Lock/Semaphore objects passed via Pool initargs are inheritable only under the fork +# start method (spawn/forkserver pickle them and fail at Pool creation) — pin it so a +# future interpreter default change cannot break the run at startup. +_mp = multiprocessing.get_context('fork') +Pool, Lock, Semaphore, Manager = _mp.Pool, _mp.Lock, _mp.Semaphore, _mp.Manager import hashlib import ctypes from pymtp import MTP import string -ENUM_TIMEOUT = 15 +# Enumeration wait budget. The first attempt gets ENUM_TIMEOUT; retry attempts get the +# shorter ENUM_TIMEOUT_RETRY - the board was just re-flashed again, and a device that is +# going to enumerate shows up within a few seconds, so a failing test costs ~3-5x a +# passing one instead of 10-30x. Per-attempt value is set by test_example(); each pool +# worker is its own process, so a module global is safe. +ENUM_TIMEOUT = 8 +ENUM_TIMEOUT_RETRY = 4 +_enum_timeout = ENUM_TIMEOUT + + +def enum_timeout_s() -> int: + """Enumeration wait budget for the current test attempt.""" + return _enum_timeout + + +def wait_until(predicate, step: float = 1.0): + """Poll predicate under the per-attempt enum budget. Deadline-based so a slow predicate + body (subprocess, libmtp scan) counts against the budget. Returns the first truthy + predicate value, or None on timeout.""" + deadline = time.monotonic() + enum_timeout_s() + while True: + r = predicate() + if r: + return r + if time.monotonic() >= deadline: + return None + time.sleep(step) STATUS_OK = "\033[32mOK\033[0m" STATUS_FAILED = "\033[31mFailed\033[0m" @@ -85,13 +117,34 @@ board_test = {} build_dir = 'cmake-build' skip_flash = False print_lock = None -usbtest_lock = None # serializes the usbtest batteries across the board worker pool - - -def init_worker(lock, ut_lock): - global print_lock, usbtest_lock +shuffle_seed = None # per-run seed for the per-board test-order shuffle (HIL_SHUFFLE_SEED to replay) + +# Per-host-controller concurrency (see controller_of/ctrl_slot below): a usbtest battery +# saturates its DUT's host controller, so batteries and flashes are budgeted per controller. +# NOTE: a Renesas uPD720201 host card must run its latest firmware (>= 2.0.2.6; RAM-uploaded, +# so it must be re-loaded every power cycle) - its ROM firmware dies under battery + +# flash/re-enumeration churn, and usbtest.py refuses the unlink-stress cases on old firmware. +# Widths profiled 2026-07-13/14 on fw 2.0.2.6 (8/1 through 12/8): wall time falls +# 22.2/14.3/12.5/10.8 min at usbtest width 1/2/3/4 and plateaus there; flash width beyond 8 +# buys nothing and only amplifies flasher-hub contention; the first battery case failures +# (bandwidth stretch on shared leaf-hub uplinks) appear at 12/8. Hence the 8/4 defaults. +FLASH_PARALLEL = max(1, int(os.getenv('HIL_FLASH_PARALLEL', '8'))) +USBTEST_PARALLEL = max(1, int(os.getenv('HIL_USBTEST_PARALLEL', '4'))) +CTRL_SLOTS = 12 # lock slots; controllers are assigned to slots on first sight +usbtest_sems = None # CTRL_SLOTS semaphores: up to USBTEST_PARALLEL batteries per controller +flash_sems = None # CTRL_SLOTS semaphores(FLASH_PARALLEL): flash permits per controller +ctrl_map = None # shared dict: 'pci:' -> slot, 'uid:' -> pci addr cache +ctrl_meta = None # guards slot assignment in ctrl_map + + +def init_worker(lock, seed, b_mutexes, f_sems, cmap, cmeta): + global print_lock, shuffle_seed, usbtest_sems, flash_sems, ctrl_map, ctrl_meta print_lock = lock - usbtest_lock = ut_lock + shuffle_seed = seed + usbtest_sems = b_mutexes + flash_sems = f_sems + ctrl_map = cmap + ctrl_meta = cmeta def log_line(msg: str) -> None: @@ -103,6 +156,95 @@ def log_line(msg: str) -> None: print(msg, file=out, flush=True) +# ------------------------------------------------------------- +# Per-controller scheduling +# ------------------------------------------------------------- +def controller_of(uid: str): + """Resolve a DUT uid to its root host controller's PCI address, or None if the device + is not enumerated (e.g. parked in board_test firmware with USB off). Successful + resolutions are cached — cabling does not change mid-run. Dual-port parts (e.g. + CH32V307 usbhs/usbfs variants) share one uid and one cache entry: budgeting is only + exact when both ports sit on the same controller (true on this rig).""" + if ctrl_map is None: + return None + cached = ctrl_map.get(f'uid:{uid}') + if cached: + return cached + for f in glob.glob('/sys/bus/usb/devices/*/serial'): + d = os.path.dirname(f) + try: + if open(f).read().strip().lower() != uid.lower(): + continue + bus = int(open(os.path.join(d, 'busnum')).read()) + root = os.path.realpath(f'/sys/bus/usb/devices/usb{bus}') + m = re.findall(r'[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]', root) + if m: + ctrl_map[f'uid:{uid}'] = m[-1] + return m[-1] + except (OSError, ValueError): + continue + return None + + +def ctrl_slot(pci: str) -> int: + """Map a controller PCI address to a lock slot (assigned on first sight).""" + key = f'pci:{pci}' + with ctrl_meta: + slot = ctrl_map.get(key) + if slot is None: + slot = ctrl_map.get('nslots', 0) + if slot >= CTRL_SLOTS: + slot = 0 # more controllers than slots: overflow shares slot 0 (safe, over-serialized) + else: + ctrl_map['nslots'] = slot + 1 + ctrl_map[key] = slot + return slot + + +class ctrl_permit: + """Context manager: one permit from `sems` on the board's controller slot. If the + controller is unknown, fail closed: take one permit from EVERY slot, in order, so the + operation respects the budget wherever it might land. `warn_unknown` logs that fallback + (used by usbtest, where the device is expected to be enumerated by the caller).""" + def __init__(self, sems, uid: str, warn_unknown: bool = False): + self.sems = sems + self.slots = None + if sems is None: + return + pci = controller_of(uid) + if pci is None and warn_unknown: + log_line(f'warning: cannot resolve {uid} to a host controller; ' + 'taking a permit on every slot (over-serialized)') + self.slots = [ctrl_slot(pci)] if pci else list(range(CTRL_SLOTS)) + + def __enter__(self): + if self.slots: + taken = [] + try: + for s in self.slots: + self.sems[s].acquire() + taken.append(s) + except BaseException: + for s in reversed(taken): + self.sems[s].release() + raise + return self + + def __exit__(self, *exc): + if self.slots: + for s in reversed(self.slots): + self.sems[s].release() + return False + + +def flash_permit(uid: str) -> ctrl_permit: + return ctrl_permit(flash_sems, uid) + + +def usbtest_permit(uid: str) -> ctrl_permit: + return ctrl_permit(usbtest_sems, uid, warn_unknown=True) + + def compact_output(raw: str) -> str: if not raw: return '' @@ -239,7 +381,7 @@ def get_alsa_capture_dev(id): def open_serial_dev(port: str): - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() ser = None while timeout > 0: if os.path.exists(port): @@ -274,27 +416,31 @@ def read_disk_file(uid: str, lun: int, fname: str) -> bytes: # Reads a file from a FAT volume on a block device without mounting it. # Requires mtools: `apt install mtools` (no pip dependency). dev = get_disk_dev(uid, 'TinyUSB', lun) - timeout = ENUM_TIMEOUT last_err = None - while timeout > 0: - if os.path.exists(dev): - try: - data = subprocess.check_output( - ['mtype', '-i', dev, f'::/{fname}'], stderr=subprocess.PIPE) - assert data, f'Cannot read file {fname} from {dev}' - return data - except subprocess.CalledProcessError as e: - last_err = e.stderr.decode(errors='replace').strip() - time.sleep(1) - timeout -= 1 - raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed') + def try_read(): + nonlocal last_err + if not os.path.exists(dev): + return None + try: + data = subprocess.check_output( + ['mtype', '-i', dev, f'::/{fname}'], stderr=subprocess.PIPE) + assert data, f'Cannot read file {fname} from {dev}' + return data + except subprocess.CalledProcessError as e: + last_err = e.stderr.decode(errors='replace').strip() + return None + + data = wait_until(try_read) + if data is None: + raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed') + return data def open_mtp_dev(uid): mtp = MTP() - timeout = ENUM_TIMEOUT - while timeout > 0: + + def try_open(): # unmount gio/gvfs MTP mount which blocks libmtp from accessing the device subprocess.run(f"gio mount -u mtp://TinyUsb_TinyUsb_Device_{uid}/", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -305,9 +451,9 @@ def open_mtp_dev(uid): if sn == uid: return mtp mtp.disconnect() - time.sleep(1) - timeout -= 1 - return None + return None + + return wait_until(try_open) def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): @@ -326,14 +472,13 @@ def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): def open_printer_dev(id: str, vendor_str, product_str, ifnum: int) -> str: """Wait for printer device to enumerate and return its path""" - timeout = ENUM_TIMEOUT - while timeout > 0: + def try_find(): lp_dev = get_printer_dev(id, vendor_str, product_str, ifnum) - if lp_dev and os.path.exists(lp_dev): - return lp_dev - time.sleep(1) - timeout -= 1 - assert False, f'Printer device not found for {id} if{ifnum:02d}' + return lp_dev if lp_dev and os.path.exists(lp_dev) else None + + lp_dev = wait_until(try_find) + assert lp_dev, f'Printer device not found for {id} if{ifnum:02d}' + return lp_dev # ------------------------------------------------------------- @@ -559,7 +704,7 @@ def test_dual_host_info_to_device_cdc(board): # read until all expected devices are enumerated data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -611,7 +756,7 @@ def test_host_device_info(board): # read until all expected devices are enumerated data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -690,7 +835,7 @@ def test_host_cdc_msc_hid(board): # Wait for all expected mount messages data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() wait_cdc = len(cdc_devs) > 0 wait_msc = len(msc_devs) > 0 while timeout > 0: @@ -783,7 +928,7 @@ def test_host_msc_file_explorer(board): # Wait for MSC mount (Disk Size message) data = b'' - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: new_data = ser.read(ser.in_waiting or 1) if new_data: @@ -848,6 +993,7 @@ def test_host_msc_file_explorer(board): break ser.close() + assert speed is not None, 'MSC read produced no speed report (dd stalled or failed)' return speed @@ -947,7 +1093,7 @@ def test_device_cdc_msc_throughput(board): # Wait for MSC disk enumeration dev = get_disk_dev(uid, 'TinyUSB', 0) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(dev): break @@ -956,7 +1102,7 @@ def test_device_cdc_msc_throughput(board): # Wait for CDC tty enumeration tty = get_serial_dev(uid, 'TinyUSB', 'Throughput', 0) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(tty): break @@ -967,7 +1113,7 @@ def test_device_cdc_msc_throughput(board): is_fs = False for f in glob.glob('/sys/bus/usb/devices/*/serial'): try: - if open(f).read().strip() == uid: + if open(f).read().strip().lower() == uid.lower(): is_fs = (open(os.path.join(os.path.dirname(f), 'speed')).read().strip() == '12') break except (OSError, ValueError): @@ -1013,17 +1159,19 @@ def test_device_cdc_msc_throughput(board): def test_device_dfu(board): uid = board['uid'] - # Wait device enum - timeout = ENUM_TIMEOUT - while timeout > 0: + # Wait device enum. Deadline-based: dfu-util -l itself takes ~1 s per call, which a + # per-iteration countdown would not charge against the budget. + deadline = time.monotonic() + enum_timeout_s() + found = False + while time.monotonic() < deadline: ret = run_cmd(f'dfu-util -l') stdout = cmd_stdout_text(ret.stdout) - if f'serial="{uid}"' in stdout and 'Found DFU: [cafe:4000]' in stdout: + if f'serial="{uid}"' in stdout and 'Found DFU: [cafe:400b]' in stdout: + found = True break time.sleep(1) - timeout = timeout - 1 - assert timeout > 0, 'Device not available' + assert found, 'Device not available' f_dfu0 = f'dfu0_{uid}' f_dfu1 = f'dfu1_{uid}' @@ -1053,17 +1201,18 @@ def test_device_dfu(board): def test_device_dfu_runtime(board): uid = board['uid'] - # Wait device enum - timeout = ENUM_TIMEOUT - while timeout > 0: + # Wait device enum (deadline-based, see test_device_dfu) + deadline = time.monotonic() + enum_timeout_s() + found = False + while time.monotonic() < deadline: ret = run_cmd(f'dfu-util -l') stdout = cmd_stdout_text(ret.stdout) - if f'serial="{uid}"' in stdout and 'Found Runtime: [cafe:4000]' in stdout: + if f'serial="{uid}"' in stdout and 'Found Runtime: [cafe:400c]' in stdout: + found = True break time.sleep(1) - timeout = timeout - 1 - assert timeout > 0, 'Device not available' + assert found, 'Device not available' def test_device_hid_boot_interface(board): @@ -1072,7 +1221,7 @@ def test_device_hid_boot_interface(board): mouse1 = get_hid_dev(uid, 'TinyUSB', 'TinyUSB_Device', 'if01-event-mouse') mouse2 = get_hid_dev(uid, 'TinyUSB', 'TinyUSB_Device', 'if01-mouse') # Wait device enum - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: if os.path.exists(kbd) and os.path.exists(mouse1) and os.path.exists(mouse2): break @@ -1270,9 +1419,9 @@ def test_device_net_lwip_webserver(board): # Wait for the host to get an IPv4 address in the device's subnet (DHCP served by the device). # USB enum + DHCP serve can take longer on the CI HIL hardware than on local — give it 30s. iface_timeout = 30 - deadline = time.time() + iface_timeout + deadline = time.monotonic() + iface_timeout host_ip = None - while time.time() < deadline: + while time.monotonic() < deadline: ret = subprocess.run(['ip', '-o', '-4', 'addr', 'show', iface], capture_output=True, text=True, timeout=2) m = re.search(r'inet (192\.168\.7\.\d+)/', ret.stdout) if ret.returncode == 0 else None @@ -1284,9 +1433,9 @@ def test_device_net_lwip_webserver(board): # Poll the iperf TCP port until the device is accepting. The net stack comes up a bit # after DHCP completes; iperf server binding isn't instantaneous after reflash. - deadline = time.time() + ENUM_TIMEOUT + deadline = time.monotonic() + enum_timeout_s() last_err = None - while time.time() < deadline: + while time.monotonic() < deadline: try: with socket.create_connection((device_ip, iperf_port), timeout=1): last_err = None @@ -1294,7 +1443,7 @@ def test_device_net_lwip_webserver(board): except OSError as e: last_err = e time.sleep(0.3) - assert last_err is None, f'iperf TCP {device_ip}:{iperf_port} not accepting within {ENUM_TIMEOUT}s: {last_err}' + assert last_err is None, f'iperf TCP {device_ip}:{iperf_port} not accepting within {enum_timeout_s()}s: {last_err}' # Throughput: 5-second iperf2 TCP test, CSV output for stable parsing. # iperf2 CSV final summary line: timestamp,src_ip,src_port,dst_ip,dst_port,id,interval,bytes,bps @@ -1334,7 +1483,7 @@ def test_device_midi_test(board): uid = board['uid'] # Find MIDI device via /dev/snd/by-id using board UID - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() midi_port = None while timeout > 0: pattern = f'/dev/snd/by-id/usb-*_{uid}-*' @@ -1356,8 +1505,8 @@ def test_device_midi_test(board): with open(midi_port, 'rb') as f: notes = [] # Read for up to 3 seconds to capture a few notes (286ms interval) - end_time = time.time() + 3 - while time.time() < end_time: + end_time = time.monotonic() + 3 + while time.monotonic() < end_time: ready, _, _ = select.select([f], [], [], 0.5) if ready: data = f.read(64) @@ -1393,7 +1542,7 @@ def test_device_audio_test_freertos(board): return 'skipped' pcm = None - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() while timeout > 0: pcm = get_alsa_capture_dev(uid) if pcm: @@ -1460,7 +1609,7 @@ def test_device_hid_generic_inout(board): import hid # cython-hidapi (pip: hidapi, apt: python3-hid) # Find HID device by UID (VID=0xCafe) - timeout = ENUM_TIMEOUT + timeout = enum_timeout_s() dev = None while timeout > 0: for d in hid.enumerate(0xCafe): @@ -1511,25 +1660,26 @@ def test_device_usbtest(board): pass return False - end = time.time() + ENUM_TIMEOUT - while time.time() < end and not usbtest_enumerated(): + end = time.monotonic() + enum_timeout_s() + while time.monotonic() < end and not usbtest_enumerated(): time.sleep(0.2) + # fail before usbtest_permit: an absent device would otherwise queue on the battery + # mutex for minutes behind real batteries just to have usbtest.py report "no device" + assert usbtest_enumerated(), f'no cafe:4010 device with serial {uid}' # settle: right after flashing the enumeration can bounce once (and on dual-port parts like # CH32V307 the other port's stale usbtest node — same serial and PID — lingers a moment); # running testusb into that gap sees the device drop mid-case time.sleep(3) - # --keep-binding leaves the usbtest dynamic id registered: the cleanup path unbinds every - # claimed interface, which has wedged the host xHCI (usb_hcd_alloc_bandwidth) on this rig. - # Boards test in a worker pool, but the batteries must run one at a time: each one saturates - # the host controller (bulk perf, iso streams, unlink storms), and several at once have - # hard-frozen the CI rig (fatal PCIe error on its VFIO-passed xHCI). + # --keep-binding is required for concurrent batteries: usbtest.py's cleanup unbinds + # EVERY usbtest-bound interface (releasing stale same-PID grabs), which would kill a + # peer battery mid-run under USBTEST_PARALLEL > 1; the unbind path has also wedged a + # host xHCI (usb_hcd_alloc_bandwidth) on this rig. Leaving bindings is harmless with + # unique example PIDs - the next example re-enumerates under a different PID and binds + # its normal driver. usbtest_permit budgets USBTEST_PARALLEL batteries per controller. script = Path(__file__).resolve().parent / 'usbtest.py' cmd = f'python3 "{script}" --serial "{uid}" --json --keep-binding --timeout 60' - if usbtest_lock is not None: - with usbtest_lock: - r = run_cmd(cmd, timeout=200) - else: + with usbtest_permit(uid): r = run_cmd(cmd, timeout=200) out = cmd_stdout_text(r.stdout) brace = out.find('{') @@ -1541,6 +1691,8 @@ def test_device_usbtest(board): skipped = int(data.get('skipped', 0)) # host-controller limitation (see usbtest.py host_broken_cases) total = passed + failed + if total == 0 and skipped > 0: + return 'skipped' # every case host-skipped: a skip, not a 0/0 failure if failed == 0 and total > 0: return f'{REPORT_CELL["pass"]} {passed}/{total}' + (f' +{skipped}skip' if skipped else '') bad = [c.get('num') for c in data.get('cases', []) if c.get('status') not in ('PASS', 'SKIP')] @@ -1552,12 +1704,11 @@ def test_device_usbtest(board): # Main # ------------------------------------------------------------- # device tests -# note don't test 2 examples with cdc or 2 msc next to each other device_tests = [ - # Order matters: cdc_msc and cdc_msc_throughput share the same VID:PID (cafe:4003), so keep a - # differently-PID'd example (dfu, cafe:4000) between them. Boards whose CPU-reset does not drop - # D+ (e.g. WCH CH58x via openocd) only re-enumerate when the PID changes; back-to-back same-PID - # firmware would otherwise leave the host on the previous example's cached descriptors. + # The per-board run order is shuffled (see test_board). Every example carries a unique + # hardcoded idProduct (see its usb_descriptors.c), so any two different examples always + # re-enumerate back-to-back — even on boards whose CPU-reset does not drop D+ (e.g. WCH + # CH58x via openocd), which only re-enumerate when the PID changes. 'device/cdc_dual_ports', 'device/cdc_msc', 'device/dfu', @@ -1629,15 +1780,18 @@ def test_example(board: Board, variant: str, example: str) -> tuple[int, str, st # flash firmware (unless --skip-flash), then run the test. Both may fail randomly, # retry a few times. + global _enum_timeout start_s = time.time() flash_ok = True last_err = '' last_detail = '' for i in range(max_retry): + _enum_timeout = ENUM_TIMEOUT if i == 0 else ENUM_TIMEOUT_RETRY attempt_out = io.StringIO() with redirect_stdout(attempt_out): if not skip_flash: - ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) + with flash_permit(board['uid']): + ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) flash_ok = (ret.returncode == 0) if flash_ok: try: @@ -1771,10 +1925,24 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]: rows = [] # list of (row_label, {example: status}) — one row per build variant variants = board.get('variant') or [{'name': name, 'flags': ''}] + prev_last = None # last test of the previous variant: the variant boundary is an adjacency too for v in variants: vname = v['name'] + # Shuffle each (board, variant)'s run order — de-synchronizes the worker pool so + # usbtest batteries and flash churn spread across the timeline instead of convoying, + # and surfaces order-dependent bugs. Seeded for replay (HIL_SHUFFLE_SEED, logged by + # main). Unique per-example PIDs make any two different examples re-enumerate; only + # the variant boundary can repeat the same example (same PID) — swap it away. + run_list = list(test_list) + if shuffle_seed is not None and len(run_list) > 1: + random.Random(f'{shuffle_seed}:{name}:{vname}').shuffle(run_list) + if run_list[0] == prev_last: + run_list[0], run_list[-1] = run_list[-1], run_list[0] + log_line(f'{vname:40} test order: {", ".join(t.rsplit("/", 1)[-1] for t in run_list)}') + if run_list: + prev_last = run_list[-1] cells = {} - for test in test_list: + for test in run_list: ec, status, metric = test_example(board, vname, test) err_count += ec cells[test] = metric if metric else status @@ -1797,16 +1965,21 @@ REPORT_JSON = 'hil_report.json' def render_matrix(rows_all: list) -> str: """Render rows (list of (row_label, {example: status})) as an aligned markdown matrix: columns = tests (bare names) centered, boards left-aligned.""" - canonical = device_tests + dual_tests + host_test seen = set() for _, cells in rows_all: seen.update(cells) if not seen: return 'No tests were run.' - # columns: canonical order first, then any extras (e.g. from -t) alphabetically - columns = [t for t in canonical if t in seen] - columns += [t for t in sorted(seen) if t not in canonical] + # metric-bearing columns pinned first (usbtest score, throughput, explorer read speed), + # the rest alphabetical by bare test name: stable regardless of the (shuffled) execution order + pinned = ['usbtest', 'cdc_msc_throughput', 'msc_file_explorer', 'msc_file_explorer_freertos'] + + def col_key(t): + name = t.rsplit('/', 1)[-1] + return (pinned.index(name) if name in pinned else len(pinned), name, t) + + columns = sorted(seen, key=col_key) headers = [c.rsplit('/', 1)[-1] for c in columns] # bare example name def cell(cells, col): @@ -1953,7 +2126,16 @@ def main() -> None: for f in (REPORT_JSON, REPORT_MD): (report_dir / f).unlink(missing_ok=True) - with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=(Lock(), Lock())) as pool: + seed = os.getenv('HIL_SHUFFLE_SEED') or str(int(time.time())) + log_line(f'test-order shuffle seed: {seed} (HIL_SHUFFLE_SEED={seed} to replay); ' + f'flash/usbtest parallel per controller: {FLASH_PARALLEL}/{USBTEST_PARALLEL}; ' + f'enum timeout first/retry: {ENUM_TIMEOUT}/{ENUM_TIMEOUT_RETRY}s') + mgr = Manager() + initargs = (Lock(), seed, + [Semaphore(USBTEST_PARALLEL) for _ in range(CTRL_SLOTS)], + [Semaphore(FLASH_PARALLEL) for _ in range(CTRL_SLOTS)], + mgr.dict(), Lock()) + with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=initargs) as pool: async_ret = pool.map_async(test_board, config_boards) try: mret = async_ret.get(timeout=POOL_TIMEOUT) diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index be39c9eb5..8f121b6f8 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -22,6 +22,7 @@ { "name": "espressif_p4_function_ev-DMA", "flags": "-DCFG_TUD_DWC2_DMA_ENABLE=1 -DCFG_TUH_DWC2_DMA_ENABLE=1" } ], "tests": { + "comment": "only IDF/FreeRTOS examples are part of the espressif fleet build; device/usbtest builds under IDF but is not built/flashed by the fleet, so it is not listed", "only": [ "device/cdc_msc_freertos", "device/hid_composite_freertos", @@ -152,6 +153,8 @@ "name": "mimxrt1015_evk", "uid": "DC28F865D2111D228D00B0543A70463C", "tests": { + "skip": ["device/usbtest"], + "comment": "this board's HS battery killed the uPD720201 twice (2026-07-11 on ROM fw, 2026-07-13 case 27 on fw 2.0.2.6 - stop-endpoint timeout, HC died); mimxrt1064/ch32v307 batteries pass, so it is board-specific - keep skipped", "device": true, "host": false, "dual": false @@ -166,19 +169,20 @@ "name": "mimxrt1064_evk", "uid": "BAE96FB95AFA6DBB8F00005002001200", "tests": { + "skip": ["host/cdc_msc_hid"], + "comment-cdc-echo": "CH9102+Lexar bundle (moved here from stm32f723disco) mounts fine but echo returns nothing - TX-RX loopback jumper likely lost in the move; re-check wiring then re-enable", "device": true, "host": true, "dual": true, "dev_attached": [ { - "vid_pid": "10c4_ea60", - "serial": "0001", - "is_cdc": true, - "comment": "cp2102" + "vid_pid": "1a86_55d4", + "serial": "52D2003414", + "is_cdc": true }, { "vid_pid": "21c4_0cc7", - "serial": "900058874D871F66", + "serial": "90005893730A1A63", "is_msc": true, "block_size": 512, "block_count": 60620800, @@ -230,6 +234,8 @@ "device": true, "host": true, "dual": true, + "skip": ["host/cdc_msc_hid", "host/device_info", "host/msc_file_explorer", "host/msc_file_explorer_freertos", "dual/host_info_to_device_cdc"], + "comment-skip": "PIO-USB host port enumerates nothing since the board moves (CH340+UDisk bundle unplugged or unpowered) - re-attach the bundle then drop these skips", "dev_attached": [ { "vid_pid": "1a86_7523", @@ -379,13 +385,14 @@ "dual": false, "dev_attached": [ { - "vid_pid": "1a86_55d4", - "serial": "52D2003414", - "is_cdc": true + "vid_pid": "10c4_ea60", + "serial": "0001", + "is_cdc": true, + "comment": "cp2102" }, { "vid_pid": "21c4_0cc7", - "serial": "90005893730A1A63", + "serial": "900058874D871F66", "is_msc": true, "block_size": 512, "block_count": 60620800, @@ -512,14 +519,11 @@ "uid": "BC5DA47360D0", "args": "" } - } - ], - "boards-skip": [ + }, { "name": "ch582m_evt", "uid": "D443627B5450", "toolchain": "riscv-gcc", - "comment": "unplugged: fixture (board + WCH-Link) failed to re-enumerate after the 2026-07-06 rig reboot; replug to re-enable", "tests": { "device": true, "host": false, @@ -531,19 +535,70 @@ "args": "" } }, + { + "name": "nrf5340dk", + "uid": "78E60E166B5F88BE", + "tests": { + "device": true, + "host": false, + "dual": false, + "skip": ["device/cdc_msc_freertos", "device/audio_test_freertos"], + "comment": "board new to HIL: FreeRTOS examples hardfault (UFSR=INVPC) at first task launch on the CM33_NTZ port - pre-existing upstream issue, non-FreeRTOS examples and usbtest pass; fix separately" + }, + "flasher": { + "name": "jlink", + "uid": "001050076405", + "args": "-device NRF5340_XXAA_APP" + } + }, { "name": "nrf54lm20dk", "uid": "899C3DE5B0F4D5CA", "tests": { "device": true, "host": false, - "dual": false + "dual": false, + "skip": ["device/audio_test_freertos"], + "comment": "board new to HIL: audio_test_freertos never reaches dcd_init (FreeRTOS itself runs; cdc_msc_freertos and usbtest pass) - example-level issue on nRF54L, fix separately" }, "flasher": { "name": "jlink", "uid": "1051856258", "args": "-device NRF54LM20A_M33" } + } + ], + "boards-skip": [ + { + "name": "ra6m5_ek", + "uid": "8419032D32363657364EF4622D294B4E", + "tests": { + "device": true, + "host": false, + "dual": false, + "skip": ["device/cdc_msc_throughput", "device/msc_dual_lun"], + "comment": "MSC writes wedge the uPD720201 host (URBs queued, zero wire activity, bus-15 ctrl xfers time out until the device's URBs are killed); reproduced identically with master firmware - device side armed+BUF and exonerated. MSC reads and usbtest bulk (15.8 MB/s) are fine" + }, + "flasher": { + "name": "jlink", + "uid": "000831915224", + "args": "-device R7FA6M5BH" + } + }, + { + "name": "ra8m1_ek", + "uid": "797D142D36345030364E1737922E4B4E", + "comment": "USBHS bring-up pending (HS chirp completes digitally but terminations never switch; FS-forced build passed usbtest 30/30). Parked until fixed", + "tests": { + "device": true, + "host": false, + "dual": false + }, + "flasher": { + "name": "jlink", + "uid": "001083115236", + "args": "-device R7FA8M1AH" + } }, { "name": "stm32f769disco", diff --git a/test/hil/usbtest.py b/test/hil/usbtest.py index 3a174a510..9aec8ac0a 100755 --- a/test/hil/usbtest.py +++ b/test/hil/usbtest.py @@ -104,7 +104,15 @@ def sudo(cmd, **kw): def sysfs_write(path, data, check=True): - r = sudo(['tee', str(path)], input=data) + # A driver-registry write (new_id/remove_id/bind) blocks in D state when a wedged device + # holds its lock (driver_attach walks the bus): fail fast and loud instead of piling up + # unkillable writers and hanging the whole run -- the rig needs USB recovery first. + try: + r = sudo(['tee', str(path)], input=data, timeout=15) + except subprocess.TimeoutExpired: + sys.exit(f'write "{data}" > {path} blocked >15s: USB subsystem is wedged ' + '(a D-state device lock exists). Recover the rig (usb_recover.sh) ' + 'before running batteries.') if check and r.returncode != 0: sys.exit(f'write "{data}" > {path} failed: {r.stderr.strip()}') return r.returncode == 0 @@ -145,7 +153,8 @@ def find_device(serial, first=False): def host_broken_cases(dev): - """Cases the DUT's upstream host controller cannot run: {case: reason}. + """Cases the DUT's upstream host controller cannot run: {case: reason}. Exits the + whole run instead if the host is a uPD720201 on pre-2.0.2.6 firmware (see below). The MosChip MCS9990 (9710:9990) EHCI cannot run interrupt-OUT: its FRINDEX register is buggy silicon (the kernel probes it with "applying MosChip frame-index workaround") and ehci-hcd never keeps the int-OUT QH in the @@ -154,15 +163,58 @@ def host_broken_cases(dev): same board+hub: EHCI FAIL (QH absent from the debugfs periodic schedule the whole hang), OHCI companion PASS, xHCI fine; int-IN unaffected. Skip with a visible SKIP so the battery self-heals once the DUT tree is back on an xHCI.""" - try: - root = Path(f"/sys/bus/usb/devices/usb{int(dev['node'].split('/')[-2])}") - drv = (root / '../driver').resolve().name - pci = (root / '..').resolve() - vid_did = ((pci / 'vendor').read_text().strip(), (pci / 'device').read_text().strip()) - except (OSError, ValueError): - return {} + for attempt in range(3): + try: + root = Path(f"/sys/bus/usb/devices/usb{int(dev['node'].split('/')[-2])}") + drv = (root / '../driver').resolve().name + pci = (root / '..').resolve() + vid_did = ((pci / 'vendor').read_text().strip(), (pci / 'device').read_text().strip()) + break + except (OSError, ValueError): + # transient sysfs error (e.g. racing a re-enumeration): retry so a blip doesn't + # silently run known-broken cases; if the probe truly fails, fail open but say so + if attempt == 2: + print('warning: cannot probe the upstream host controller; ' + 'known-broken-host cases will run instead of being skipped', file=sys.stderr) + return {} + time.sleep(1) if drv.startswith('ehci') and vid_did == ('0x9710', '0x9990'): - return {25: 'host EHCI (MosChip MCS9990) loses interrupt-OUT completions'} + return { + 25: 'host EHCI (MosChip MCS9990) loses interrupt-OUT completions', + # Unlinking an in-progress read intermittently completes it as a short transfer + # (EREMOTEIO) instead of -ECONNRESET; device-side exonerated by TX counters (only + # full-mps loads, no ZLP). Passes on xHCI. Some boards dodge it by timing. + 11: 'host EHCI (MosChip MCS9990) completes unlinked reads as short (EREMOTEIO)', + } + if drv.startswith('xhci') and vid_did in (('0x1912', '0x0014'), ('0x1912', '0x0015')): + # The Renesas uPD720201/uPD720202 must run its latest firmware (>= 2.0.2.6, + # K2026090.mem; RAM-uploaded, so it reverts to ROM on every power cycle unless + # re-loaded). On the ROM firmware its command ring intermittently dies under unlink + # stress: a Configure Endpoint command stops completing, the hub worker deadlocks + # holding the device lock (needs a host power cycle). Three separate boards killed + # it this way (ch32v307 2026-07-10; ra6m5 test 24, mimxrt1015 2026-07-11). Both + # parts expose the FW version register at PCI config offset 0x6c. NOTE this check + # is necessary, not sufficient: board-specific batteries have killed the controller + # on current firmware too (mimxrt1015, stop-endpoint timeout) - those are handled + # by per-board skips in the rig config. + fw = None + try: + r = sudo(['setpci', '-s', pci.name, '0x6c.l'], capture_output=True, text=True) + if r.returncode == 0: + fw = int(r.stdout.strip(), 16) + except (OSError, ValueError): + pass + if fw is None: + sys.exit(f'REFUSING to run: cannot read host xHCI Renesas ({pci.name}) firmware ' + 'version (setpci missing or not permitted) - usbtest requires verified ' + 'firmware >= 0x00202609 (2.0.2.6); on older firmware the command ring ' + 'dies under unlink stress. Install pciutils / fix sudo, or load the ' + 'firmware and re-check.') + if fw < 0x00202609: + sys.exit(f'REFUSING to run: host xHCI Renesas ({pci.name}) firmware 0x{fw:08x} ' + '< 0x00202609 (2.0.2.6) - its command ring dies under usbtest unlink ' + 'stress. Load the latest firmware (K2026090.mem; it is RAM-uploaded and ' + 'reverts to ROM on every power cycle).') return {} @@ -327,13 +379,16 @@ def main(): if not args.json: print(info) + # probe the upstream controller before touching the device: an unsupported host + # (uPD720201 on pre-2.0.2.6 firmware) exits here, before any bind + broken = host_broken_cases(dev) + results = [] unrecovered_hang = False try: bind_usbtest(dev) set_pattern(0) # tier 1 firmware sources zeros; also required by perf cases 27/28 - broken = host_broken_cases(dev) for num in cases: if num in broken: results.append({'num': num, 'name': CASE_NAMES[num], 'status': 'SKIP', diff --git a/tools/check_example_pids.py b/tools/check_example_pids.py new file mode 100644 index 000000000..d8795af34 --- /dev/null +++ b/tools/check_example_pids.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Check that every example enumerates with a unique USB PID. + +Each example's usb_descriptors.c hardcodes its idProduct (0x40xx). Uniqueness is what +guarantees back-to-back re-enumeration on the HIL rig and a fresh host driver match, and +it is easy to break by hand: a new example copying a neighbour's PID, or an arithmetic +PID (dynamic_configuration derives a second one from USB_PID). This collects every +`#define USB_PID 0x....`, every literal `.idProduct = 0x....`, and every `USB_PID + ` +derivation across examples/, and fails on any duplicate value. +""" + +import re +import sys +from pathlib import Path + +EXAMPLES = Path(__file__).resolve().parents[1] / 'examples' + +RE_DEFINE = re.compile(r'#define\s+USB_PID\s+\(?(0x[0-9a-fA-F]+)\)?') +RE_LITERAL = re.compile(r'\.idProduct\s*=\s*(0x[0-9a-fA-F]+)') +RE_DERIVED = re.compile(r'\.idProduct\s*=\s*USB_PID\s*\+\s*(0x[0-9a-fA-F]+|\d+)') + + +def main() -> int: + pids: dict[int, list[str]] = {} + for f in sorted(EXAMPLES.glob('*/*/src/usb_descriptors.c')): + text = f.read_text(errors='replace') + rel = f.relative_to(EXAMPLES.parent) + base = None + m = RE_DEFINE.search(text) + if m: + base = int(m.group(1), 16) + for m in RE_LITERAL.finditer(text): + pids.setdefault(int(m.group(1), 16), []).append(str(rel)) + for m in RE_DERIVED.finditer(text): + if base is None: + print(f'{rel}: derived idProduct but no USB_PID define', file=sys.stderr) + return 1 + pids.setdefault(base + int(m.group(1), 0), []).append(f'{rel} (USB_PID + {m.group(1)})') + # examples whose descriptor uses .idProduct = USB_PID pick up the define itself + if base is not None and re.search(r'\.idProduct\s*=\s*USB_PID\s*[,;]', text): + pids.setdefault(base, []).append(str(rel)) + + dups = {pid: users for pid, users in pids.items() if len(users) > 1} + for pid, users in sorted(dups.items()): + print(f'duplicate USB PID 0x{pid:04x}:', file=sys.stderr) + for u in users: + print(f' {u}', file=sys.stderr) + if dups: + return 1 + print(f'{len(pids)} unique example USB PIDs') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/usb_drivers/tinyusb_win_usbser.inf b/tools/usb_drivers/tinyusb_win_usbser.inf index 659f048ae..e3875a478 100644 --- a/tools/usb_drivers/tinyusb_win_usbser.inf +++ b/tools/usb_drivers/tinyusb_win_usbser.inf @@ -88,11 +88,11 @@ ServiceBinary=%12%\%DRIVERFILENAME%.sys [SourceDisksNames] [DeviceList] -%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00 +%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00, USB\VID_CAFE&PID_4006&MI_00, USB\VID_CAFE&PID_4008&MI_00, USB\VID_CAFE&PID_400a&MI_00, USB\VID_CAFE&PID_4020&MI_00, USB\VID_CAFE&PID_4022&MI_00 [DeviceList.NTamd64] -%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00 +%DESCRIPTION%=DriverInstall, USB\VID_CAFE&PID_4001&MI_00, USB\VID_CAFE&PID_4003&MI_00, USB\VID_CAFE&PID_4005&MI_00, USB\VID_CAFE&PID_4007&MI_00, USB\VID_CAFE&PID_4009&MI_00, USB\VID_CAFE&PID_400b&MI_00, USB\VID_CAFE&PID_400d&MI_00, USB\VID_CAFE&PID_400f&MI_00, USB\VID_CAFE&PID_4011&MI_00, USB\VID_CAFE&PID_4013&MI_00, USB\VID_CAFE&PID_4015&MI_00, USB\VID_CAFE&PID_4017&MI_00, USB\VID_CAFE&PID_4019&MI_00, USB\VID_CAFE&PID_401b&MI_00, USB\VID_CAFE&PID_401d&MI_00, USB\VID_CAFE&PID_401f&MI_00, USB\VID_CAFE&PID_4021&MI_00, USB\VID_CAFE&PID_4023&MI_00, USB\VID_CAFE&PID_4025&MI_00, USB\VID_CAFE&PID_4027&MI_00, USB\VID_CAFE&PID_4029&MI_00, USB\VID_CAFE&PID_402b&MI_00, USB\VID_CAFE&PID_402d&MI_00, USB\VID_CAFE&PID_402f&MI_00, USB\VID_CAFE&PID_4031&MI_00, USB\VID_CAFE&PID_4033&MI_00, USB\VID_CAFE&PID_4035&MI_00, USB\VID_CAFE&PID_4037&MI_00, USB\VID_CAFE&PID_4039&MI_00, USB\VID_CAFE&PID_403b&MI_00, USB\VID_CAFE&PID_403d&MI_00, USB\VID_CAFE&PID_403f&MI_00, USB\VID_CAFE&PID_4006&MI_00, USB\VID_CAFE&PID_4008&MI_00, USB\VID_CAFE&PID_400a&MI_00, USB\VID_CAFE&PID_4020&MI_00, USB\VID_CAFE&PID_4022&MI_00 ;------------------------------------------------------------------------------ ; String Definitions -- cgit v1.3.1 From 36cd9f9f46ca20be907ed57b874d9d1dc7b3bf64 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 16 Jul 2026 14:11:01 +0700 Subject: dcd_lpc17_40: fix stale EP0 out_received, add isochronous support EP0 control-OUT fix (usbtest 14/21, errno 110/-74): usbd queues the status-stage OUT ZLP of every control read with buffer=NULL, so the ISR's `if (out_buffer)` check missed it and marked the arriving ZLP as out_received instead. The stale flag poisoned the next control-OUT with data: its first chunk "completed" instantly from an empty EP0 buffer and the host's real DATA NAKed forever. Track queued transfers with an explicit out_queued flag and void half-finished control state on a new SETUP. Isochronous support (UM10562 12.15.6): 5-word DMA descriptors with per-packet size memory, buflen/present_count in packets, one packet per FRAME (no DMARSet/EpIntEn involvement), completion at EOT for both directions. Details that matter: - the iso machinery (5th DD word + packet-size memory) is compiled only when an iso-capable class is enabled (CFG_TUD_AUDIO/VIDEO/VENDOR), so non-iso builds pay nothing: _dcd stays 648 B vs 1032 B with iso - ISR dispatch keys on the hardware's fixed ep-number/type map (ep_id_is_iso), never on dd fields that thread mode rebuilds - iso OUT honors Packet_valid (bit 16) and prefills the hardware writeback slots with 0, so a missed frame counts as 0 bytes instead of reading back stale buffer contents as data - packet count is validated (tu_div_ceil <= ISO_MAX_PACKETS) before the DD is touched, so an oversized transfer is refused without leaving a serviceable half-built descriptor armed for the frame engine - dcd_edpt_iso_alloc and iso_activate both enforce the fixed iso endpoint numbers (3/6/9/12); classes ignore alloc's return value, so activate must not trust it Un-skip LPC40XX in the usbtest example; tier 4 now enumerates and passes iso cases 15/16/22/23. cdc_msc_throughput and printer_to_cdc had bulk on iso-only EP3 (SET_CONFIGURATION failed with -32); add the LPC17/40 EPNUM block (bulk on EP2/EP5) like other fixed-EP examples. Verified on ea4088_quickstart: usbtest tier-4 battery 30/30 repeatedly and the full device HIL suite 14/14 (incl. audio_test iso). --- .../cdc_msc_throughput/src/usb_descriptors.c | 10 +- .../device/printer_to_cdc/src/usb_descriptors.c | 10 +- examples/device/usbtest/skip.txt | 1 - src/portable/nxp/lpc17_40/dcd_lpc17_40.c | 211 ++++++++++++++++++--- 4 files changed, 202 insertions(+), 30 deletions(-) (limited to 'examples/device/usbtest') diff --git a/examples/device/cdc_msc_throughput/src/usb_descriptors.c b/examples/device/cdc_msc_throughput/src/usb_descriptors.c index ba0b0a26f..dca5a65cf 100644 --- a/examples/device/cdc_msc_throughput/src/usb_descriptors.c +++ b/examples/device/cdc_msc_throughput/src/usb_descriptors.c @@ -65,7 +65,15 @@ enum { }; // Place bulk endpoints on EP>=8 for MAX32690 class parts (bigger FIFO, DPB-capable). -#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX + // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number + // 0 control, 1 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ... + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_MSC_OUT 0x05 + #define EPNUM_MSC_IN 0x85 +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering #define EPNUM_CDC_NOTIF 0x81 diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c index b9450c87e..92cd2b6be 100644 --- a/examples/device/printer_to_cdc/src/usb_descriptors.c +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -67,7 +67,15 @@ uint8_t const *tud_descriptor_device_cb(void) { //--------------------------------------------------------------------+ // Endpoint numbers -#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX + // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number + // 0 control, 1 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ... + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_PRINTER_OUT 0x05 + #define EPNUM_PRINTER_IN 0x85 +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering #define EPNUM_CDC_NOTIF 0x81 diff --git a/examples/device/usbtest/skip.txt b/examples/device/usbtest/skip.txt index b52bdbb14..e789c4b91 100644 --- a/examples/device/usbtest/skip.txt +++ b/examples/device/usbtest/skip.txt @@ -5,7 +5,6 @@ mcu:SAMD11 mcu:CXD56 mcu:FT90X mcu:LPC175X_6X -mcu:LPC40XX mcu:NUC100 mcu:NUC120 mcu:NUC505 diff --git a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c index 182710016..a1a44e9ae 100644 --- a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c +++ b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c @@ -19,6 +19,10 @@ //--------------------------------------------------------------------+ #define DCD_ENDPOINT_MAX 32 +// The iso machinery (5th DD word + packet-size memory) costs USB RAM on every build; +// compile it only when a class that can open an iso endpoint is enabled. +#define DCD_ISO_ENABLED (CFG_TUD_AUDIO || CFG_TUD_VIDEO || CFG_TUD_VENDOR) + typedef struct TU_ATTR_ALIGNED(4) { //------------- Word 0 -------------// @@ -48,11 +52,35 @@ typedef struct TU_ATTR_ALIGNED(4) volatile uint16_t present_count; // For non-iso : The number of bytes transferred by the DMA engine // For iso : number of packets +#if DCD_ISO_ENABLED //------------- Word 4 -------------// - // uint32_t iso_packet_size_addr; // iso only, can be omitted for non-iso + volatile uint32_t iso_packet_size_addr; // iso only: pointer into iso packet-size memory, + // advanced by hardware after each packet +#endif }dma_desc_t; -TU_VERIFY_STATIC( sizeof(dma_desc_t) == 16, "size is not correct"); // TODO not support ISO for now +TU_VERIFY_STATIC( sizeof(dma_desc_t) == (DCD_ISO_ENABLED ? 20 : 16), "size is not correct"); + +// Hardware fixes endpoint type by number: 3, 6, 9, 12 are the iso-capable ones. +// Constant per ep_id (= 2*epnum + dir) — unlike dd->isochronous, which dcd_edpt_xfer +// transiently zeroes while rebuilding the DD, this is safe to dispatch on from the ISR. +TU_ATTR_ALWAYS_INLINE static inline bool ep_id_is_iso(uint8_t ep_id) { + uint8_t const epnum = (uint8_t)(ep_id >> 1); + return (epnum % 3) == 0 && (epnum != 0) && (epnum != 15); +} + +#if DCD_ISO_ENABLED +// Isochronous packet-size memory (UM10562 12.15.6.3): one word per packet. +// IN : software fills Packet_length (bits 15:0), 0 = ZLP +// OUT: hardware writes Frame_number (31:17) | Packet_valid (16) | Packet_length (15:0) +// Iso-capable endpoint numbers are 3, 6, 9, 12 -> 8 slots (x2 directions). +// One packet moves per FRAME, so a deep queue only adds latency: 8 frames is plenty. +#define ISO_MAX_PACKETS 8 +#define ISO_SLOT_COUNT 8 +TU_ATTR_ALWAYS_INLINE static inline uint8_t iso_slot(uint8_t ep_id) { + return (uint8_t)(((ep_id / 6) - 1) * 2 + (ep_id & 1)); // ep_id = 2*epnum + dir, epnum in {3,6,9,12} +} +#endif typedef struct { @@ -66,11 +94,17 @@ typedef struct { uint8_t* out_buffer; uint8_t out_bytes; + volatile bool out_queued; // an OUT xfer is queued; out_buffer may legitimately be NULL (status ZLP) volatile bool out_received; // indicate if data is already received in endpoint uint8_t in_bytes; } control; +#if DCD_ISO_ENABLED + // iso packet-size memory, must be DMA-reachable like the DDs + volatile uint32_t iso_psize[ISO_SLOT_COUNT][ISO_MAX_PACKETS]; +#endif + } dcd_data_t; CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd; @@ -79,6 +113,7 @@ CFG_TUD_MEM_SECTION TU_ATTR_ALIGNED(128) static dcd_data_t _dcd; //--------------------------------------------------------------------+ // SIE Command //--------------------------------------------------------------------+ + static void sie_cmd_code (sie_cmdphase_t phase, uint8_t code_data) { LPC_USB->DevIntClr = (DEV_INT_COMMAND_CODE_EMPTY_MASK | DEV_INT_COMMAND_DATA_FULL_MASK); @@ -294,7 +329,8 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) break; case TUSB_XFER_ISOCHRONOUS: - TU_ASSERT((epnum % 3) == 0 && (epnum != 0) && (epnum != 15)); + // iso machinery is compiled out when no iso-capable class is enabled + TU_ASSERT(DCD_ISO_ENABLED && (epnum % 3) == 0 && (epnum != 0) && (epnum != 15)); break; default: @@ -319,16 +355,54 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) } bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { +#if DCD_ISO_ENABLED (void)rhport; - (void)ep_addr; - (void)largest_packet_size; + uint8_t const ep_id = ep_addr2idx(ep_addr); + + // hardware fixes iso to endpoint numbers 3, 6, 9, 12 + TU_ASSERT(ep_id_is_iso(ep_id)); + TU_ASSERT(largest_packet_size > 0); + + set_ep_size(ep_id, largest_packet_size); + + dma_desc_t* const dd = &_dcd.dd[ep_id]; + tu_memclr(dd, sizeof(dma_desc_t)); + dd->isochronous = 1; + dd->max_packet_size = largest_packet_size; + dd->retired = 1; // invalid at first + + sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0); + return true; +#else + (void)rhport; (void)ep_addr; (void)largest_packet_size; return false; +#endif } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { +#if DCD_ISO_ENABLED (void)rhport; - (void)desc_ep; + uint8_t const ep_id = ep_addr2idx(desc_ep->bEndpointAddress); + dma_desc_t* const dd = &_dcd.dd[ep_id]; + + // same fixed-number rule as alloc: without it a rejected-but-ignored alloc (classes + // discard that return) would set isochronous on a non-iso ep_id and underflow iso_slot() + TU_ASSERT(ep_id_is_iso(ep_id)); + + // kill any armed transfer from a previous alternate setting + LPC_USB->EpDMADis = TU_BIT(ep_id); + _dcd.udca[ep_id] = NULL; + + dd->isochronous = 1; + dd->max_packet_size = tu_edpt_packet_size(desc_ep); + dd->retired = 1; + + sie_write(SIE_CMDCODE_ENDPOINT_SET_STATUS + ep_id, 1, 0); + return true; +#else + (void)rhport; (void)desc_ep; return false; +#endif } void dcd_edpt_close_all (uint8_t rhport) @@ -373,15 +447,17 @@ static bool control_xact(uint8_t rhport, uint8_t dir, uint8_t * buffer, uint8_t { // Already received the DATA OUT packet _dcd.control.out_received = false; - _dcd.control.out_buffer = NULL; - _dcd.control.out_bytes = 0; uint8_t received = control_ep_read(buffer, len); dcd_event_xfer_complete(0, 0, received, XFER_RESULT_SUCCESS, true); }else { + // buffer is NULL for a status-stage ZLP: signal the pending xfer explicitly, + // NOT via out_buffer != NULL — a NULL-buffer queue mistaken for "nothing queued" + // leaves out_received stale and poisons the next control OUT data stage. _dcd.control.out_buffer = buffer; _dcd.control.out_bytes = len; + _dcd.control.out_queued = true; } } @@ -406,26 +482,65 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t uint16_t const ep_size = dd->max_packet_size; uint8_t is_iso = dd->isochronous; - tu_memclr(dd, sizeof(dma_desc_t)); - dd->isochronous = is_iso; - dd->max_packet_size = ep_size; - dd->buffer = (uint32_t) buffer; - dd->buflen = total_bytes; +#if DCD_ISO_ENABLED + if ( is_iso ) + { + // iso: buflen counts packets; per-packet sizes live in the packet-size memory. + // One packet moves per frame (UM10562 12.15.6: DMA request is raised for + // DMA-enabled iso endpoints on every FRAME interrupt, both directions). + // Validate BEFORE touching the DD: bailing out mid-rebuild would leave a + // zeroed (retired=0 -> serviceable) descriptor armed for the frame engine. + TU_ASSERT(ep_size > 0); + uint16_t const packets = (total_bytes > 0) ? (uint16_t) tu_div_ceil(total_bytes, ep_size) : 1; + TU_ASSERT(packets <= ISO_MAX_PACKETS); + + uint8_t const slot = iso_slot(ep_id); + uint16_t remain = total_bytes; + for ( uint16_t i = 0; i < packets; i++ ) + { + uint16_t const pkt_len = tu_min16(remain, ep_size); + // IN: length to send (0 = ZLP). OUT: hardware writes back + // Frame_number|Packet_valid|Packet_length -- prefill 0 so a frame the + // hardware never wrote (missed/invalid) cannot read back as data. + _dcd.iso_psize[slot][i] = (ep_id & 1) ? pkt_len : 0; + remain = (uint16_t)(remain - pkt_len); + } - _dcd.udca[ep_id] = dd; + tu_memclr(dd, sizeof(dma_desc_t)); + dd->isochronous = 1; + dd->max_packet_size = ep_size; + dd->buffer = (uint32_t) buffer; + dd->buflen = packets; + dd->iso_packet_size_addr = (uint32_t) &_dcd.iso_psize[slot][0]; - if ( ep_id % 2 ) + _dcd.udca[ep_id] = dd; + LPC_USB->EpDMAEn = TU_BIT(ep_id); // frame-triggered: no DMARSet, no EpIntEn + } + else +#else + (void) is_iso; +#endif { - // Clear EP interrupt before Enable DMA - LPC_USB->EpIntEn &= ~TU_BIT(ep_id); - LPC_USB->EpDMAEn = TU_BIT(ep_id); + tu_memclr(dd, sizeof(dma_desc_t)); + dd->max_packet_size = ep_size; + dd->buffer = (uint32_t) buffer; + dd->buflen = total_bytes; - // endpoint IN need to actively raise DMA request - LPC_USB->DMARSet = TU_BIT(ep_id); - }else - { - // Enable DMA - LPC_USB->EpDMAEn = TU_BIT(ep_id); + _dcd.udca[ep_id] = dd; + + if ( ep_id % 2 ) + { + // Clear EP interrupt before Enable DMA + LPC_USB->EpIntEn &= ~TU_BIT(ep_id); + LPC_USB->EpDMAEn = TU_BIT(ep_id); + + // endpoint IN need to actively raise DMA request + LPC_USB->DMARSet = TU_BIT(ep_id); + }else + { + // Enable DMA + LPC_USB->EpDMAEn = TU_BIT(ep_id); + } } return true; @@ -451,13 +566,20 @@ static void control_xfer_isr(uint8_t rhport, uint32_t ep_int_status) uint8_t setup_packet[8]; control_ep_read(setup_packet, 8); // TODO read before clear setup above + // a new SETUP voids any half-finished control state + _dcd.control.out_queued = false; + _dcd.control.out_received = false; + _dcd.control.out_buffer = NULL; + _dcd.control.out_bytes = 0; + dcd_event_setup_received(rhport, setup_packet, true); } - else if ( _dcd.control.out_buffer ) + else if ( _dcd.control.out_queued ) { - // software queued transfer previously + // software queued transfer previously (out_buffer NULL = status ZLP) uint8_t received = control_ep_read(_dcd.control.out_buffer, _dcd.control.out_bytes); + _dcd.control.out_queued = false; _dcd.control.out_buffer = NULL; _dcd.control.out_bytes = 0; @@ -513,7 +635,32 @@ static void dd_complete_isr(uint8_t rhport, uint8_t ep_id) uint8_t result = (dd->status == DD_STATUS_NORMAL || dd->status == DD_STATUS_DATA_UNDERUN) ? XFER_RESULT_SUCCESS : XFER_RESULT_FAILED; uint8_t const ep_addr = (ep_id / 2) | ((ep_id & 0x01) ? TUSB_DIR_IN_MASK : 0); - dcd_event_xfer_complete(rhport, ep_addr, dd->present_count, result, true); + uint32_t xferred_bytes; +#if DCD_ISO_ENABLED + if ( ep_id_is_iso(ep_id) ) + { + // present_count is in packets; actual byte counts are in the packet-size memory + // (IN: as programmed by us, OUT: Packet_length written back by hardware, + // guarded by Packet_valid -- a frame with no packet must count as 0) + uint8_t const slot = iso_slot(ep_id); + uint16_t const packets = tu_min16(dd->present_count, ISO_MAX_PACKETS); + xferred_bytes = 0; + for (uint16_t i = 0; i < packets; i++) + { + uint32_t const psize = _dcd.iso_psize[slot][i]; + if ( (ep_id & 1) || (psize & TU_BIT(16)) ) + { + xferred_bytes += (psize & 0xFFFFu); + } + } + } + else +#endif + { + xferred_bytes = dd->present_count; + } + + dcd_event_xfer_complete(rhport, ep_addr, (uint16_t) xferred_bytes, result, true); } // main USB IRQ handler @@ -569,6 +716,16 @@ void dcd_int_handler(uint8_t rhport) { if ( tu_bit_test(eot, ep_id) ) { + // dispatch on the hardware's fixed ep-number/type map, NOT dd->isochronous: + // thread-mode dcd_edpt_xfer transiently zeroes the DD while rebuilding it +#if DCD_ISO_ENABLED + if ( ep_id_is_iso(ep_id) ) + { + // iso: last packet already left with its frame; complete both directions here + dd_complete_isr(rhport, ep_id); + } + else +#endif if ( ep_id & 0x01 ) { // IN enable EpInt for end of usb transfer -- cgit v1.3.1 From cb224400931b7fbc3477a87a258c0602092abe6b Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 17 Jul 2026 17:58:25 +0700 Subject: dcd_lpc17_40: address review findings in the iso paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a second max-effort review of the branch: - Drop the dead TUSB_XFER_ISOCHRONOUS case in dcd_edpt_open: iso endpoints are armed via dcd_edpt_iso_alloc/activate (TUP_DCD_EDPT_ISO_ALLOC is defined for this IP), never through dcd_edpt_open, so the case and its dd->isochronous assignment were unreachable and asserted a false invariant. Only bulk/interrupt reach the switch now. - Extend the iso compile gate to the classes that actually arm an iso endpoint: DCD_ISO_ENABLED now includes CFG_TUD_BTH (bth_device.c opens an iso voice endpoint). Without it a BTH build would compile the iso machinery out and fail SET_INTERFACE at runtime. - Un-skip LPC175X_6X in the usbtest example: it shares dcd_lpc17_40.c with LPC40XX verbatim, so the "DCD has no isochronous support" skip reason no longer holds. Build-verified for lpcxpresso1769 (previously blocked by the skip). - TU_ATTR_UNUSED on the ep_id_is_iso helper: every caller is under #if DCD_ISO_ENABLED, so non-iso builds don't reference it and clang's -Wunused-function (fatal in CI) rejected the build — gcc stays quiet. Verified with the full lpc17 and lpc40 example sets under arm-clang. A fifth finding — bounding control_ep_read's PACKET_READY spin with a timeout — was implemented and REVERTED: a naive 100k-iteration bound fires on legitimately-slow control reads and intermittently drops the device (hardware-proven by interleaved A/B testing against the pre-fix binary). The infinite wait is retained; the read is only reached once out_received/ out_queued signal data is present, so the theoretical IRQ-off hang is not reachable in practice. Re-verified on ea4088_quickstart: usbtest 30/30 (repeated) + HIL 14/14. --- examples/device/usbtest/skip.txt | 1 - src/portable/nxp/lpc17_40/dcd_lpc17_40.c | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) (limited to 'examples/device/usbtest') diff --git a/examples/device/usbtest/skip.txt b/examples/device/usbtest/skip.txt index e789c4b91..792404fe4 100644 --- a/examples/device/usbtest/skip.txt +++ b/examples/device/usbtest/skip.txt @@ -4,7 +4,6 @@ mcu:SAMD11 # DCD has no isochronous support (dcd_edpt_iso_alloc refuses), tier-4 cannot enumerate: mcu:CXD56 mcu:FT90X -mcu:LPC175X_6X mcu:NUC100 mcu:NUC120 mcu:NUC505 diff --git a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c index 6dc2b017c..b577d0e9f 100644 --- a/src/portable/nxp/lpc17_40/dcd_lpc17_40.c +++ b/src/portable/nxp/lpc17_40/dcd_lpc17_40.c @@ -20,8 +20,10 @@ #define DCD_ENDPOINT_MAX 32 // The iso machinery (5th DD word + packet-size memory) costs USB RAM on every build; -// compile it only when a class that can open an iso endpoint is enabled. -#define DCD_ISO_ENABLED (CFG_TUD_AUDIO || CFG_TUD_VIDEO || CFG_TUD_VENDOR) +// compile it only when a class that can open an iso endpoint is enabled. Keep this in +// sync with the classes that actually arm an iso endpoint: audio, video, BTH (voice), +// and vendor (its optional CFG_TUD_VENDOR_EP_ISO_* endpoints, exercised by usbtest). +#define DCD_ISO_ENABLED (CFG_TUD_AUDIO || CFG_TUD_VIDEO || CFG_TUD_VENDOR || CFG_TUD_BTH) typedef struct TU_ATTR_ALIGNED(4) { @@ -64,7 +66,9 @@ TU_VERIFY_STATIC( sizeof(dma_desc_t) == (DCD_ISO_ENABLED ? 20 : 16), "size is no // Hardware fixes endpoint type by number: 3, 6, 9, 12 are the iso-capable ones. // Constant per ep_id (= 2*epnum + dir) — unlike dd->isochronous, which dcd_edpt_xfer // transiently zeroes while rebuilding the DD, this is safe to dispatch on from the ISR. -TU_ATTR_ALWAYS_INLINE static inline bool ep_id_is_iso(uint8_t ep_id) { +// TU_ATTR_UNUSED: every caller is under #if DCD_ISO_ENABLED, so non-iso builds don't +// reference it and clang -Wunused-function (fatal) would otherwise reject the build. +TU_ATTR_UNUSED TU_ATTR_ALWAYS_INLINE static inline bool ep_id_is_iso(uint8_t ep_id) { uint8_t const epnum = (uint8_t)(ep_id >> 1); return (epnum % 3) == 0 && (epnum != 0) && (epnum != 15); } @@ -360,8 +364,9 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) uint8_t const epnum = tu_edpt_number(p_endpoint_desc->bEndpointAddress); uint8_t const ep_id = ep_addr2idx(p_endpoint_desc->bEndpointAddress); - // Endpoint type is fixed to endpoint number - // 1: interrupt, 2: Bulk, 3: Iso and so on + // Endpoint type is fixed to endpoint number (1 interrupt, 2 bulk, 3 iso, ...). + // Iso endpoints are armed via dcd_edpt_iso_alloc/activate, never through here + // (TUP_DCD_EDPT_ISO_ALLOC is defined for this IP), so only bulk/interrupt land here. switch ( p_endpoint_desc->bmAttributes.xfer ) { case TUSB_XFER_INTERRUPT: @@ -372,11 +377,6 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) TU_ASSERT((epnum % 3) == 2 || (epnum == 15)); break; - case TUSB_XFER_ISOCHRONOUS: - // iso machinery is compiled out when no iso-capable class is enabled - TU_ASSERT(DCD_ISO_ENABLED && (epnum % 3) == 0 && (epnum != 0) && (epnum != 15)); - break; - default: break; } @@ -387,9 +387,7 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc) //------------- first DD prepare -------------// dma_desc_t* const dd = &_dcd.dd[ep_id]; - tu_memclr(dd, sizeof(dma_desc_t)); - - dd->isochronous = (p_endpoint_desc->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) ? 1 : 0; + tu_memclr(dd, sizeof(dma_desc_t)); // non-iso: isochronous stays 0 dd->max_packet_size = ep_size; dd->retired = 1; // invalid at first -- cgit v1.3.1 From 19ff2ed615e4a97984aab5551ac8835ead53b9e7 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 17 Aug 2026 01:02:54 +0700 Subject: examples: document and work around the i.MX RT and LPC55 USB errata ERR050101: while an isochronous IN endpoint is active, an IN token addressed to that same endpoint number on ANOTHER device sharing the host can silently unprime one of this device's OUT endpoints - control, bulk, interrupt or isochronous alike. NXP states it cannot be detected by software and raises no interrupt, so the endpoint simply stops answering and the transfer never completes. The workaround is a uniqueness requirement rather than a particular number: the isochronous IN endpoint must not share its number with any IN endpoint in use on the bus. One family-wide constant therefore defeats it, since two affected boards on the same hub then pick the same number and each becomes the other's aggressor. CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101 is set only for the parts whose errata list it - RT1015, RT1020, RT1024 and RT1050, where it is marked no fix scheduled, plus RT1060 and RT1064 rev A - so RT1010 and the RT11xx family keep the ordinary number and cannot collide with an affected board beside them. Several affected boards on one hub can still be given distinct numbers with -DEPNUM_ISO_IN. The guard covers every example that has an isochronous IN endpoint: audio_test, audio_4_channel_mic, uac2_headset, cdc_uac2, usbtest, video_capture and video_capture_2ch. The video examples move the endpoint only when streaming isochronously, since the bulk configuration is unaffected, and video_capture_2ch takes two numbers because it has two streams. The macro name follows CFG_TUSB_RP2_ERRATA_E2/E4/E15 already in tree, and its is fixed, and which cannot be told apart at compile time - a way to define it to 0. device_issues.rst records ERR050101 against every affected part with a link to each errata sheet, and adds the LPC55S2x USB.3 speed-detection and USB.5 isochronous IN entries, neither of which TinyUSB works around. The branch's design notes are included under docs/superpowers. Verified: 340 wedge-free runs on mimxrt1064_evk, which previously wedged within hours, and the macro resolving to endpoint 0x87 on mimxrt1064_evk against 0x83 on mimxrt1010_evk and stm32f407disco. --- docs/reference/device_issues.rst | 45 ++ .../plans/2026-08-15-ci-hs-reset-edges.md | 782 +++++++++++++++++++++ .../plans/2026-08-16-drop-ep0-prime-verify.md | 314 +++++++++ .../specs/2026-08-15-ci-hs-reset-edges-design.md | 162 +++++ .../2026-08-16-drop-ep0-prime-verify-design.md | 90 +++ .../audio_4_channel_mic/src/usb_descriptors.c | 4 + examples/device/audio_test/src/usb_descriptors.c | 4 + examples/device/cdc_uac2/src/usb_descriptors.c | 10 + examples/device/uac2_headset/src/usb_descriptors.c | 7 + examples/device/usbtest/src/usb_descriptors.c | 16 + .../device/video_capture/src/usb_descriptors.c | 4 + .../device/video_capture_2ch/src/usb_descriptors.c | 11 +- src/common/tusb_mcu.h | 19 + 13 files changed, 1466 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md create mode 100644 docs/superpowers/plans/2026-08-16-drop-ep0-prime-verify.md create mode 100644 docs/superpowers/specs/2026-08-15-ci-hs-reset-edges-design.md create mode 100644 docs/superpowers/specs/2026-08-16-drop-ep0-prime-verify-design.md (limited to 'examples/device/usbtest') diff --git a/docs/reference/device_issues.rst b/docs/reference/device_issues.rst index 0850409cb..b95a3fc1e 100644 --- a/docs/reference/device_issues.rst +++ b/docs/reference/device_issues.rst @@ -20,6 +20,51 @@ Most severe issues are: - USB.5: In USB full-speed host mode, linked list on done queue is broken. - USB.15: USB high-speed device in endpoint TX data corruption +NXP i.MX RT1015/RT1020/RT1024/RT1050/RT1060/RT1064 +----------------------------------------------------- +**Severity: High** when an isochronous IN endpoint is used behind a hub + +Reference: ERR050101 "USB: Endpoint conflict issue in device mode", listed in the errata sheet of +every part above - `IMXRT1015CE`_, `IMXRT1020CE`_, `IMXRT1024CE`_, `IMXRT1050CE`_, `IMXRT1060CE`_ +and `IMXRT1064CE`_. On RT1060 and RT1064 it applies to rev A silicon only and is fixed in rev B; on +RT1015, RT1020, RT1024 and RT1050 it is marked *no fix scheduled*, so all silicon is affected. +RT1010, RT116x, RT117x and RT118x do not list it. + +.. _IMXRT1015CE: https://www.nxp.com/docs/en/errata/IMXRT1015CE.pdf +.. _IMXRT1020CE: https://www.nxp.com/docs/en/errata/IMXRT1020CE.pdf +.. _IMXRT1024CE: https://www.nxp.com/docs/en/errata/IMXRT1024CE.pdf +.. _IMXRT1050CE: https://www.nxp.com/docs/en/errata/IMXRT1050CE.pdf +.. _IMXRT1060CE: https://www.nxp.com/docs/en/errata/IMXRT1060CE.pdf +.. _IMXRT1064CE: https://www.nxp.com/docs/en/errata/IMXRT1064CE.pdf + +While an isochronous IN endpoint is active, an IN token addressed to *that same endpoint number on +another device sharing the host* can silently unprime one of this device's OUT endpoints - control, +bulk, interrupt or isochronous alike. NXP states the unpriming cannot be detected by software and +raises no interrupt, so the endpoint simply stops answering OUT tokens and the transfer never +completes. Typically seen when the device is behind a hub with other devices attached. + +Workaround: give isochronous IN endpoints a number that no other device on the same host uses for +any IN endpoint - endpoints 1-3 are used by nearly every composite device, so choose a high number +(``examples/device/usbtest`` uses endpoint 7 on this family for that reason). Devices without an +isochronous IN endpoint are unaffected. + +NXP LPC55S2x/LPC552x +--------------------------------- +**Severity: Low** (both need specific conditions) + +Reference: `LPC55S2x Errata Sheet`_ USB.3, USB.5 + +.. _LPC55S2x Errata Sheet: https://www.nxp.com/docs/en/errata/ES_LPC55S2x.pdf + +USB.3: As a high-speed device behind certain full-speed hubs, the device does not correctly detect +the host's KJ chirp sequence and can behave erratically due to wrong speed detection. The documented +workaround is to set the FORCE_FS bit in DEVCMDSTAT on bus reset when the reported link speed is +full speed. TinyUSB does not implement this workaround. + +USB.5: An isochronous IN endpoint sending a 1024-byte maximum-packet-size packet raises no endpoint +interrupt and its command/status entry is not updated. Workaround: cap the isochronous IN maximum +packet size at 1023 bytes in the descriptor. + WCH CH32F20x/CH32V20x/CH32V30x --------------------------------- **Severity: Medium** diff --git a/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md b/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md new file mode 100644 index 000000000..ec0834e55 --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-ci-hs-reset-edges.md @@ -0,0 +1,782 @@ +# Bus-Reset Edge Events + Review Fix Wave Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give the device stack a "bus reset started" event so ci_hs can tell usbd to stand down at the URI interrupt instead of up to 50 ms later, and clear the ten findings agreed from the max review. + +**Architecture:** `DCD_EVENT_BUS_RESET` splits into `DCD_EVENT_BUS_RESET_START` / `_END` with a compatibility alias, so every other port stays byte-identical. `dcd_ci_hs.c`'s `bus_reset()` splits along the register/software line — registers at URI (`_START`), software structures at the port-change ending the reset (`_END`) — which eliminates the window where usbd believes it is configured over zeroed queue heads. A single bounded-flush helper absorbs the five flush sites. Seven mechanical fixes follow. + +**Tech Stack:** C99, TinyUSB device stack (`src/device/`), ChipIdea HS DCD (`src/portable/chipidea/ci_hs/`), NXP IP3511 DCD (`src/portable/nxp/lpc_ip3511/`), CMake+Ninja and Make builds, J-Link flashing, `test/hil/` HIL harness. + +## Global Constraints + +- Branch `fix-ci-hs` in worktree `/home/hathach/.herdr/worktrees/tinyusb/fix-ci-hs`. Do NOT push; the user pushes. +- C99, 2-space indent, no tabs. Match each file's surrounding style (`dcd_lpc_ip3511.c` mixes styles — follow the immediate neighbourhood). +- Commit messages: imperative mood, no `Co-Authored-By:` or `Claude-Session:` trailers (repo rule: hathach is sole author). +- The repo pre-commit hook (trailing-whitespace, end-of-file-fixer, codespell, unique-PIDs, ceedling unit tests) must pass. If it rewrites a file, re-stage and retry the commit once. +- Comments: short, only the non-obvious "why". Cite manuals as `UM10503 25.10.3` / `Errata LPC546xx USB.13` style — never `ES_` prefixes. +- Never edit anything under `hw/mcu/` or `lib/` (vendor code). +- Build commands used throughout (each ~30-60 s): + `cmake --build examples/cmake-build-` for `mimxrt1064_evk`, `lpcxpresso18s37`, `lpcxpresso11u37`, `lpcxpresso55s28`. +- Design source of truth: `docs/superpowers/specs/2026-08-15-ci-hs-reset-edges-design.md`. + +## File Structure + +| File | Responsibility in this plan | +|---|---| +| `src/device/dcd.h` | Event enum + compatibility alias + contract comment | +| `src/device/usbd.c` | Handle both reset edges; log strings; stop breakpointing on DCD refusal | +| `src/portable/chipidea/ci_hs/dcd_ci_hs.c` | Flush helper; `bus_reset()` split; setup-flush wait; `dcd_set_address`; RESUME guard | +| `src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c` | Torn-setup delivery; USB.13 TODO token | +| `hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake` | Delete dead RHPORT block | +| `hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld` | Correct stale comment; relabel ASSERT | + +Tasks 1-3 are ordered (each builds on the previous); Tasks 4-6 are independent of each other. + +--- + +### Task 1: Split the bus-reset event into START/END edges + +**Files:** +- Modify: `src/device/dcd.h` (enum at lines 23-34; contract comment above it) +- Modify: `src/device/usbd.c` (`_usbd_event_str[]` at line 457; the `DCD_EVENT_BUS_RESET` case at line 700) + +**Interfaces:** +- Produces: `DCD_EVENT_BUS_RESET_START` and `DCD_EVENT_BUS_RESET_END` enum members; `#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END`. Task 2 emits `_START` via the existing `dcd_event_bus_signal(uint8_t rhport, dcd_eventid_t eid, bool in_isr)` and `_END` via the existing `dcd_event_bus_reset(uint8_t rhport, tusb_speed_t speed, bool in_isr)`. + +- [ ] **Step 1: Replace the enum member in `src/device/dcd.h`** + +Replace: + +```c +typedef enum { + DCD_EVENT_INVALID = 0, // 0 + DCD_EVENT_BUS_RESET, // 1 + DCD_EVENT_UNPLUGGED, // 2 + DCD_EVENT_SOF, // 3 + DCD_EVENT_SUSPEND, // 4 TODO LPM Sleep L1 support + DCD_EVENT_RESUME, // 5 + DCD_EVENT_SETUP_RECEIVED, // 6 + DCD_EVENT_XFER_COMPLETE, // 7 + USBD_EVENT_FUNC_CALL, // 8 Not an DCD event, just a convenient way to defer ISR function + DCD_EVENT_COUNT +} dcd_eventid_t; +``` + +with: + +```c +// Bus reset is reported as two edges. BUS_RESET_START is optional: a controller that +// cannot tell the edges apart emits only BUS_RESET_END, which stays self-sufficient (it +// performs the full teardown with or without a preceding START). Emit START when reset +// signaling is detected - the link is unusable and the speed is not negotiated yet - so +// the stack stops using endpoints immediately instead of at the end of the reset. +typedef enum { + DCD_EVENT_INVALID = 0, // 0 + DCD_EVENT_BUS_RESET_START, // 1 + DCD_EVENT_BUS_RESET_END, // 2 with negotiated speed + DCD_EVENT_UNPLUGGED, // 3 + DCD_EVENT_SOF, // 4 + DCD_EVENT_SUSPEND, // 5 TODO LPM Sleep L1 support + DCD_EVENT_RESUME, // 6 + DCD_EVENT_SETUP_RECEIVED, // 7 + DCD_EVENT_XFER_COMPLETE, // 8 + USBD_EVENT_FUNC_CALL, // 9 Not an DCD event, just a convenient way to defer ISR function + DCD_EVENT_COUNT +} dcd_eventid_t; + +#define DCD_EVENT_BUS_RESET DCD_EVENT_BUS_RESET_END // backward compatibility +``` + +- [ ] **Step 2: Update the log-string table in `src/device/usbd.c`** + +At line 457 the table is indexed by event id and MUST stay in enum order. Replace the +`"Bus Reset",` entry (line 459) with two entries: + +```c + "Bus Reset Start", + "Bus Reset End", +``` + +- [ ] **Step 3: Handle both edges in the usbd task loop** + +Replace the case at `src/device/usbd.c:700`: + +```c + case DCD_EVENT_BUS_RESET: + TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]); + usbd_reset(event.rhport); + _usbd_dev.speed = event.bus_reset.speed; + break; +``` + +with: + +```c + case DCD_EVENT_BUS_RESET_START: + TU_LOG_USBD("\r\n"); + usbd_reset(event.rhport); + break; + + case DCD_EVENT_BUS_RESET_END: + TU_LOG_USBD(": %s Speed\r\n", tu_str_speed[event.bus_reset.speed]); + // TODO a DCD that reports both edges pays for two teardowns: track a per-rhport + // "start seen" flag and skip this reset, keeping it for the single-event DCDs. + usbd_reset(event.rhport); + _usbd_dev.speed = event.bus_reset.speed; + break; +``` + +- [ ] **Step 4: Verify legacy ports still build (the alias must carry them)** + +Run: + +```bash +cd examples && cmake -B cmake-build-stm32f407disco -DBOARD=stm32f407disco -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . && cmake --build cmake-build-stm32f407disco +``` + +Expected: builds clean. This board's DCD (dwc2) still calls `dcd_event_bus_reset()`, which +now resolves to `_END` through the unchanged helper — proving the alias works. + +- [ ] **Step 5: Verify the unit tests still build and pass** + +Run: `cd test/unit-test && ceedling test:all` +Expected: all tests pass (they reference `DCD_EVENT_BUS_RESET` via the alias). + +- [ ] **Step 6: Commit** + +```bash +git add src/device/dcd.h src/device/usbd.c +git commit -m "usbd: split bus reset into start/end edge events + +A DCD that can see reset signaling begin has no way to say so: the only +event carries the negotiated speed, which does not exist until the reset +ends. On ChipIdea that leaves the stack believing it is configured for the +whole reset window (3 ms minimum, tens of ms in practice) while the +controller has already torn its endpoints down. + +Add DCD_EVENT_BUS_RESET_START for the leading edge and rename the existing +event to DCD_EVENT_BUS_RESET_END, keeping DCD_EVENT_BUS_RESET as an alias +so every other port and the unit tests are untouched. START is optional and +END stays self-sufficient, so single-event drivers keep working unchanged." +``` + +--- + +### Task 2: Split ci_hs `bus_reset()` across the two edges, behind one flush helper + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (`bus_reset()`; `dcd_deinit()`; `dcd_edpt_iso_activate()`; the `INTR_RESET` and `INTR_PORT_CHANGE` branches of `dcd_int_handler()`) + +**Interfaces:** +- Consumes: `DCD_EVENT_BUS_RESET_START` (Task 1), `dcd_event_bus_signal()`, `dcd_event_bus_reset()`. +- Produces: `static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask)` — writes `ENDPTFLUSH = mask`, spins bounded by `CI_HS_BUSY_SPIN`, returns `true` if the bits cleared. Used by Task 3. + +- [ ] **Step 1: Add the flush helper next to `bus_reset()`** + +Insert above `bus_reset()`: + +```c +// Flush endpoint buffers and wait for the controller to acknowledge. Callers proceed +// regardless of the result; the bound only prevents an ISR-context hang on dead hardware. +static bool flush_endpoints(ci_hs_regs_t *dcd_reg, uint32_t mask) { + dcd_reg->ENDPTFLUSH = mask; + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTFLUSH & mask) { + if (!guard--) { + return false; + } + } + return true; +} +``` + +- [ ] **Step 2: Split `bus_reset()` into begin/complete** + +Replace the whole `bus_reset()` function with these two. `bus_reset_begin()` keeps only +register work; `bus_reset_complete()` owns everything that touches `_dcd_data`: + +```c +/// Register-side reset handling, must run inside the reset window (UM10503 25.10.3) +static void bus_reset_begin(uint8_t rhport) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + + // The reset value for all endpoint types is the control endpoint. If one endpoint + // direction is enabled and the paired endpoint of opposite direction is disabled, then the + // endpoint type of the unused direction must be changed from the control type to any other + // type (e.g. bulk). Leaving an un-configured endpoint control will cause undefined behavior + // for the data PID tracking on the active endpoint. + const uint8_t ep_count = ci_ep_count(dcd_reg); + for (uint8_t i = 1; i < ep_count; i++) { + dcd_reg->ENDPTCTRL[i] = ENDPTCTRL_RESET_MASK; + } + + //------------- Clear All Registers -------------// + dcd_reg->ENDPTNAK = dcd_reg->ENDPTNAK; + dcd_reg->ENDPTNAKEN = 0; + dcd_reg->ENDPTSETUPSTAT = dcd_reg->ENDPTSETUPSTAT; + dcd_reg->ENDPTCOMPLETE = dcd_reg->ENDPTCOMPLETE; + + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + flush_endpoints(dcd_reg, 0xFFFFFFFF); +} + +/// Software-side reset handling, deferred to the port change ending the reset so the queue +/// heads stay coherent until the stack is told - and so a prime issued by a task that had +/// not yet seen BUS_RESET_START is flushed here rather than surviving re-enumeration. +static void bus_reset_complete(uint8_t rhport) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + flush_endpoints(dcd_reg, 0xFFFFFFFF); + + //------------- Queue Head & Queue TD -------------// + tu_memclr(&_dcd_data, sizeof(dcd_data_t)); + + //------------- Set up Control Endpoints (0 OUT, 1 IN) -------------// + _dcd_data.qhd[0][0].zero_length_termination = _dcd_data.qhd[0][1].zero_length_termination = 1; + _dcd_data.qhd[0][0].max_packet_size = _dcd_data.qhd[0][1].max_packet_size = CFG_TUD_ENDPOINT0_SIZE; + _dcd_data.qhd[0][0].qtd_overlay.next = _dcd_data.qhd[0][1].qtd_overlay.next = QTD_NEXT_INVALID; + + _dcd_data.qhd[0][0].int_on_setup = 1; // OUT only + + dcd_dcache_clean_invalidate(&_dcd_data, sizeof(dcd_data_t)); +} +``` + +- [ ] **Step 3: Route the two ISR branches to the new functions** + +In `dcd_int_handler()`, the `INTR_RESET` branch becomes: + +```c + if (int_status & INTR_RESET) { + bus_reset_begin(rhport); + _port_change_reason[rhport] = PORT_CHANGE_REASON_RESET; + dcd_event_bus_signal(rhport, DCD_EVENT_BUS_RESET_START, true); + } +``` + +and inside the `INTR_PORT_CHANGE` branch, the reset arm (the `else` of the resume test) +becomes: + +```c + } else { + bus_reset_complete(rhport); + // PSPD: 0 full, 1 low, 2 high, 3 undefined (treated as full) + const uint32_t pspd = (dcd_reg->PORTSC1 & PORTSC1_PORT_SPEED) >> PORTSC1_PORT_SPEED_POS; + const tusb_speed_t speed = (pspd == 1) ? TUSB_SPEED_LOW : (pspd == 2) ? TUSB_SPEED_HIGH : TUSB_SPEED_FULL; + dcd_event_bus_reset(rhport, speed, true); + } +``` + +Delete the now-unused EP0 `ENDPTFLUSH` line that previously sat at the top of that arm — +`bus_reset_complete()` flushes all endpoints. + +- [ ] **Step 4: Route the remaining flush sites through the helper** + +In `dcd_deinit()`, replace the flush block with: + +```c + // flush all endpoints + uint32_t guard = CI_HS_BUSY_SPIN; + while (dcd_reg->ENDPTPRIME && guard--) {} + flush_endpoints(dcd_reg, 0xFFFFFFFF); +``` + +In `dcd_edpt_iso_activate()`, replace the flush + spin with: + +```c + // Flush EP + flush_endpoints(dcd_reg, TU_BIT(epnum + (dir ? 16 : 0))); +``` + +- [ ] **Step 5: Build both ci_hs board families** + +Run: + +```bash +cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37 +``` + +Expected: both succeed with no new warnings. + +- [ ] **Step 6: Commit** + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): report bus reset start at URI, finish at port change + +The RM wants the reset cleanup inside the reset window, but the negotiated +speed only exists once the port reaches its operational state, so the stack +was told nothing for the whole window - it kept believing it was configured +while the queue heads had been zeroed under it, and a transfer a class +driver started in that gap stayed primed across re-enumeration. + +Split the work along the register/software line: bus_reset_begin() does the +register cleanup at URI and signals BUS_RESET_START, bus_reset_complete() +re-flushes, resets the queue heads and reports BUS_RESET_END with the final +speed at the port change. Zeroing the queue heads now happens in the same +breath as telling the stack, and the second flush retires anything primed +in between. + +Fold the five hand-rolled endpoint flushes into one bounded helper while +the reset path is open." +``` + +--- + +### Task 3: Make the setup-time EP0 flush wait, and stop dropping the SET_ADDRESS status prime + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (`dcd_set_address()`; the `ENDPTSETUPSTAT` branch inside `dcd_int_handler()`) + +**Interfaces:** +- Consumes: `flush_endpoints()` (Task 2); `qhd_start_xfer()` returning `bool`, already propagated by `dcd_edpt_xfer()`. + +- [ ] **Step 1: Wait for the setup-time flush to complete** + +In the ISR's setup branch, replace the fire-and-forget flush line + +```c + dcd_reg->ENDPTFLUSH = TU_BIT(0) | TU_BIT(16); +``` + +with + +```c + // Wait it out: the flush retires a status/handshake phase left primed by the previous + // control sequence (UM10503 25.10.8.1.1), and an unfinished flush would otherwise + // still be asserted when the task primes the response to this setup and would retire + // that instead. A flush waits for any packet already in progress - microseconds at + // high speed - and the guard caps wedged hardware. + flush_endpoints(dcd_reg, TU_BIT(0) | TU_BIT(16)); +``` + +- [ ] **Step 2: Honour the status-prime result in `dcd_set_address`** + +Replace the body of `dcd_set_address()`: + +```c +void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { + // Response with status first before changing device address. A refused prime means a new + // setup superseded this transfer; staging an address whose ACK will never arrive would + // leave the device answering on it, so only arm the address when the status went out. + if (dcd_edpt_xfer(rhport, tu_edpt_addr(0, TUSB_DIR_IN), NULL, 0, false)) { + ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport); + dcd_reg->DEVICEADDR = (dev_addr << 25) | TU_BIT(24); + } +} +``` + +- [ ] **Step 3: Build and commit** + +Run: `cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37` +Expected: both succeed. + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): wait out the setup flush, honour the set-address prime + +The flush issued on every new setup was fire-and-forget. A flush waits for +a packet already in progress, so it could still be asserted when the task +primed the response to that setup and retire the fresh prime instead - +leaving EP0 silent until the host gave up. + +dcd_set_address() also armed DEVICEADDR unconditionally, but the status +prime can now be refused when a newer setup supersedes the transfer; the +address was then staged behind an ACK that never came and the device sat at +address 0. Only arm it when the status transfer actually started." +``` + +--- + +### Task 4: Emit RESUME only when the port really left suspend + +**Files:** +- Modify: `src/portable/chipidea/ci_hs/dcd_ci_hs.c` (the resume arm of the `INTR_PORT_CHANGE` branch in `dcd_int_handler()`) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Restore the hardware guard** + +In the `INTR_PORT_CHANGE` branch, the resume arm currently reads: + +```c + if (pci_reason == PORT_CHANGE_REASON_SUSPEND) { + dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); + } else { +``` + +Replace that condition with one that also consults live hardware: + +```c + if (pci_reason == PORT_CHANGE_REASON_SUSPEND) { + // Only when the port actually left suspend: a starved snapshot can hold the resume's + // port change together with a second suspend, and reporting a resume there would + // leave the stack awake on a sleeping bus with no further event to correct it. + if (!(dcd_reg->PORTSC1 & PORTSC1_SUSPEND)) { + dcd_event_bus_signal(rhport, DCD_EVENT_RESUME, true); + } + } else { +``` + +- [ ] **Step 2: Build and commit** + +Run: `cmake --build examples/cmake-build-mimxrt1064_evk && cmake --build examples/cmake-build-lpcxpresso18s37` +Expected: both succeed. + +```bash +git add src/portable/chipidea/ci_hs/dcd_ci_hs.c +git commit -m "dcd(ci_hs): only report resume when the port left suspend + +A suspend, resume and second suspend collapsed into one interrupt pass +queued suspend then resume from the recorded cause alone, so the stack +ended up awake while the bus was still suspended and nothing arrived to +correct it. Consult PORTSC1 before reporting the resume." +``` + +--- + +### Task 5: ip3511 — never deliver a knowingly-torn setup packet + +**Files:** +- Modify: `src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c` (setup branch of `dcd_int_handler()`; the `dcd_edpt_clear_stall()` comment) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Deliver only when the copy is known good** + +Replace: + +```c + // a SETUP that raced in after the acks (its bit0 consumed above, this copy possibly torn): + // its latch is visible again - re-raise the endpoint interrupt so the next pass redelivers + // the newer payload + if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) { + dcd_reg->INTSETSTAT = TU_BIT(0); + } + + dcd_event_setup_received(rhport, setup_copy, true); +``` + +with: + +```c + // a SETUP that raced in after the acks (its bit0 consumed above) makes this copy suspect: + // its latch is visible again, so re-raise the endpoint interrupt and let the next pass + // deliver the newer payload rather than passing up bytes that may be torn between the two + if (dcd_reg->DEVCMDSTAT & DEVCMDSTAT_SETUP_RECEIVED_MASK) { + dcd_reg->INTSETSTAT = TU_BIT(0); + } else { + dcd_event_setup_received(rhport, setup_copy, true); + } +``` + +- [ ] **Step 2: Add the TODO token to the USB.13 deferral** + +In `dcd_edpt_clear_stall()`, change the caveat's opening line from + +```c + // Known caveat (Errata LPC546xx USB.13, same semantics in UM11126): with RF/TV preserved at 1, TR +``` + +to + +```c + // TODO implement the Errata LPC546xx USB.13 work-around (same semantics in UM11126): with RF/TV preserved at 1, TR +``` + +- [ ] **Step 3: Build and commit** + +Run: `cmake --build examples/cmake-build-lpcxpresso11u37 && cmake --build examples/cmake-build-lpcxpresso55s28` +Expected: both succeed. + +```bash +git add src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c +git commit -m "dcd(ip3511): drop a setup packet the hardware may have overwritten + +The handler already notices when a new setup landed while it was copying +the previous one, and re-raises the endpoint interrupt so the newer payload +is delivered next pass - but it then passed the suspect copy up anyway. +Usually harmless, since the redelivery supersedes it, but if that second +event cannot be queued the torn bytes are processed as a real request. +Deliver the copy only when no newer setup is pending." +``` + +--- + +### Task 6: BSP cleanups — dead RHPORT block and the stale linker comment + +**Files:** +- Modify: `hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake` +- Modify: `hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld` + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Delete the redundant RHPORT block** + +`hw/bsp/lpc55/family.cmake` already applies the identical guarded defaults (`RHPORT_DEVICE 1`, +`RHPORT_HOST 0`) after including the board file, so remove these lines from +`board.cmake` entirely: + +```cmake +# device highspeed, host fullspeed; guarded so a -D override on the cmake command line wins +if (NOT DEFINED RHPORT_DEVICE) + set(RHPORT_DEVICE 1) +endif () +if (NOT DEFINED RHPORT_HOST) + set(RHPORT_HOST 0) +endif () +``` + +Leave `board.mk`'s `RHPORT_DEVICE ?= 1` / `RHPORT_HOST ?= 0` alone — `?=` is the idiomatic +Make form and matches sibling boards. + +- [ ] **Step 2: Prove the defaults and the override still work** + +Run: + +```bash +cd examples && rm -rf /tmp/rh-default /tmp/rh-override +cmake -B /tmp/rh-default -DBOARD=lpcxpresso55s28 -G Ninja . > /tmp/rh-default.log 2>&1 +grep -m1 "RHPORT_DEVICE" /tmp/rh-default.log || cmake -B /tmp/rh-default -DBOARD=lpcxpresso55s28 -G Ninja -LA . | grep -E "^RHPORT_(DEVICE|HOST)" +cmake -B /tmp/rh-override -DBOARD=lpcxpresso55s28 -DRHPORT_DEVICE=0 -DRHPORT_HOST=1 -G Ninja -LA . | grep -E "^RHPORT_(DEVICE|HOST)" +``` + +Expected: the default configure yields device 1 / host 0; the override configure yields +device 0 / host 1. Then rebuild the real tree: `cmake --build cmake-build-lpcxpresso55s28`. + +- [ ] **Step 3: Correct the linker-script comment and relabel the ASSERT** + +In `lpc11u37.ld`, replace the comment block above `__user_stack_top` and the ASSERT with: + +```text + /* Main (MSP/ISR) stack lives at the top of the USB SRAM bank: the 8K main bank is packed so + tight that only ~280 B remained above .bss, and ISR frames overflowed into the topmost task + stack (cdc_msc_freertos hard fault). Nothing else is placed in this bank in either build + system, so the stack owns all 2 KB; the ASSERT is future-proofing in case USB buffers are + ever mapped here again. */ + __user_stack_top = ORIGIN(RamUsb2) + LENGTH(RamUsb2); + ASSERT(__user_stack_top - (ADDR(.noinit_RAM2) + SIZEOF(.noinit_RAM2)) >= 0x200, + "main stack headroom in RamUsb2 below 512 bytes") +``` + +- [ ] **Step 4: Build both build systems for lpc11u37** + +Run: + +```bash +cmake --build examples/cmake-build-lpcxpresso11u37 +cd examples/device/cdc_msc_freertos && make -j8 BOARD=lpcxpresso11u37 all && cd ../../.. +``` + +Expected: both succeed. + +- [ ] **Step 5: Commit** + +```bash +git add hw/bsp/lpc55/boards/lpcxpresso55s28/board.cmake hw/bsp/lpc11/boards/lpcxpresso11u37/lpc11u37.ld +git commit -m "bsp: drop duplicated lpc55s28 rhport defaults, fix lpc11u37 comment + +hw/bsp/lpc55/family.cmake already applies the same guarded rhport defaults +after including the board file, so the board-level copy only added a second +place to keep in sync. + +The lpc11u37 linker comment still described USB buffers living in RamUsb2, +a placement the same branch removed; nothing lands there now, so say so and +label the headroom assert as future-proofing." +``` + +--- + +### Task 7: Stop halting the target when a DCD legitimately refuses a transfer + +**Files:** +- Modify: `src/device/usbd.c` (`usbd_edpt_xfer()` failure arm) + +**Interfaces:** none consumed or produced. + +- [ ] **Step 1: Remove the breakpoint from the DCD-refusal path** + +Replace the failure arm of `usbd_edpt_xfer()`: + +```c + } else { + // DCD error, mark endpoint as ready to allow next transfer + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + TU_LOG_USBD("FAILED\r\n"); + TU_BREAKPOINT(); + return false; + } +``` + +with: + +```c + } else { + // Driver refused the transfer, mark endpoint as ready to allow next transfer. This is a + // recoverable condition (e.g. a new setup superseding a control response), not a bug, so + // do not break into the debugger - TU_BREAKPOINT() halts the CPU whenever a probe is + // attached, which on a test rig is always. + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + TU_LOG_USBD("FAILED\r\n"); + return false; + } +``` + +- [ ] **Step 2: Confirm no other stack path relies on that breakpoint** + +Run: `grep -n "TU_BREAKPOINT" src/device/*.c src/device/*.h` +Expected: no remaining hits inside `usbd_edpt_xfer`; other occurrences (if any) are in +unrelated assert macros and stay as they are. + +- [ ] **Step 3: Build and run unit tests** + +Run: + +```bash +cmake --build examples/cmake-build-mimxrt1064_evk +cd test/unit-test && ceedling test:all && cd ../.. +``` + +Expected: build succeeds, all unit tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add src/device/usbd.c +git commit -m "usbd: do not breakpoint when a driver refuses a transfer + +TU_BREAKPOINT() is not gated on CFG_TUSB_DEBUG - it halts the CPU whenever +a debugger is attached, which on a test rig is always. A driver declining a +transfer is recoverable (a new setup superseding a control response, for +one) and the endpoint is already released for the retry, so a halted target +turns a self-healing case into a dead board." +``` + +--- + +### Task 8: Full validation on hardware + +**Files:** none modified — this task produces the evidence for the PR description. + +**Interfaces:** consumes the firmware built by Tasks 1-7. + +- [ ] **Step 1: Software gate** + +Run: + +```bash +pre-commit run --all-files +cd examples +for b in mimxrt1064_evk lpcxpresso18s37 lpcxpresso11u37 lpcxpresso55s28; do + rm -rf cmake-build-$b && cmake -B cmake-build-$b -DBOARD=$b -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . && cmake --build cmake-build-$b || echo "FAILED $b" +done +cd .. +``` + +Expected: pre-commit all green; all four boards build every example. + +- [ ] **Step 2: Make-build regression checks** + +Run: + +```bash +cd examples/host/cdc_msc_hid && make -j8 BOARD=lpcxpresso55s28 all && cd ../../.. +cd examples/device/cdc_msc_throughput && make -j8 BOARD=lpcxpresso11u37 all && cd ../../.. +``` + +Expected: both link (these two were broken earlier in the branch and are the regression +canaries for the BSP changes). + +- [ ] **Step 3: Flash with verification (mandatory)** + +The mimxrt1064_evk has twice accepted a flash that silently did not take, so every load in +this task uses `verifyfile`. For each board, write a J-Link script of this shape and run it: + +``` +r +h +loadfile examples/cmake-build-/device/usbtest/usbtest.elf +verifyfile examples/cmake-build-/device/usbtest/usbtest.elf +r +g +qc +``` + +Probes and devices: `mimxrt1064_evk` = `-USB 000725299165 -device MIMXRT1064xxx6A`, +`lpcxpresso55s28` = `-USB 000727031389 -device LPC55S28`, +`lpcxpresso11u37` = `-USB 000724441579 -device LPC11U37/401`. +Invoke as `JLinkExe -if swd -speed 4000 -autoconnect 1 -NoGui 1 -CommandFile