summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-06-01 12:42:03 +0700
committerGitHub <[email protected]>2026-06-01 12:42:03 +0700
commitbbdb41995de6510b837ad239933e1823ca175314 (patch)
treeac33e616077fde67751dffe92cf68afa8671dd0d
parent2c27ec9c89f95057e30da5d08c01f01c941b8a58 (diff)
parent17185428df755d7229407e6ac87c124e522877dc (diff)
Merge pull request #3657 from hathach/usbh-add-control-queue
Add control transfer fifo for host stack
-rw-r--r--examples/device/midi2_device/CMakeLists.txt4
-rw-r--r--examples/device/midi2_device/src/main.c2
-rw-r--r--examples/device/msc_dual_lun/src/main.c2
-rw-r--r--examples/dual/dynamic_switch/src/main.c159
-rw-r--r--examples/dual/host_info_to_device_cdc/src/main.c6
-rw-r--r--examples/host/bare_api/src/main.c43
-rw-r--r--examples/host/cdc_msc_hid/src/cdc_app.c6
-rw-r--r--examples/host/device_info/src/main.c71
-rw-r--r--examples/host/midi2_host/src/main.c4
-rw-r--r--examples/host/msc_file_explorer_freertos/skip.txt1
-rw-r--r--src/common/tusb_types.h1
-rw-r--r--src/host/usbh.c334
-rw-r--r--src/osal/osal.h39
-rw-r--r--src/osal/osal_freertos.h13
-rw-r--r--src/osal/osal_mynewt.h6
-rw-r--r--src/osal/osal_none.h16
-rw-r--r--src/osal/osal_pico.h7
-rw-r--r--src/osal/osal_rtthread.h6
-rw-r--r--src/osal/osal_rtx4.h6
-rw-r--r--src/osal/osal_threadx.h5
-rw-r--r--src/osal/osal_zephyr.h6
-rw-r--r--src/tusb.c2
-rw-r--r--src/tusb_option.h12
-rw-r--r--test/hil/hil_ci.sh15
-rwxr-xr-xtest/hil/hil_test.py87
-rw-r--r--test/hil/requirements.txt11
-rw-r--r--test/hil/tinyusb.json236
27 files changed, 804 insertions, 296 deletions
diff --git a/examples/device/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt
index 295af6550..f1fe09db2 100644
--- a/examples/device/midi2_device/CMakeLists.txt
+++ b/examples/device/midi2_device/CMakeLists.txt
@@ -30,4 +30,6 @@ target_include_directories(${PROJECT_NAME} PUBLIC
family_configure_device_example(${PROJECT_NAME} noos)
# Suppress pre-existing warning in usbd.c (uint8_t comparison always true/false)
-target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits)
+if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
+ target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits)
+endif()
diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c
index 62741ac41..ce052a20b 100644
--- a/examples/device/midi2_device/src/main.c
+++ b/examples/device/midi2_device/src/main.c
@@ -715,6 +715,4 @@ int main(void) {
}
}
}
-
- return 0;
}
diff --git a/examples/device/msc_dual_lun/src/main.c b/examples/device/msc_dual_lun/src/main.c
index a4ade6f9b..1d764f12c 100644
--- a/examples/device/msc_dual_lun/src/main.c
+++ b/examples/device/msc_dual_lun/src/main.c
@@ -71,7 +71,7 @@ static void usb_device_init(void) {
board_init_after_tusb();
}
-#if CFG_TUSB_OS != OPT_OS_NONE && CFG_TUSB_OS != OPT_OS_PICO
+#if CFG_TUSB_OS_HAS_SCHEDULER
static void usb_device_task(RTOS_PARAM param) {
(void) param;
usb_device_init();
diff --git a/examples/dual/dynamic_switch/src/main.c b/examples/dual/dynamic_switch/src/main.c
index c9ad2a835..f67cd885c 100644
--- a/examples/dual/dynamic_switch/src/main.c
+++ b/examples/dual/dynamic_switch/src/main.c
@@ -79,6 +79,9 @@ StaticTask_t usb_taskdef;
StackType_t cdc_stack[CDC_STACK_SIZE];
StaticTask_t cdc_taskdef;
+
+StackType_t devinfo_stack[USBH_STACK_SIZE];
+StaticTask_t devinfo_taskdef;
#endif
#endif
@@ -87,14 +90,13 @@ static tusb_role_t current_role = TUSB_ROLE_DEVICE;
#if CFG_TUSB_OS == OPT_OS_FREERTOS
static void usb_task(void *param);
-void led_blinking_task(void *param);
-void cdc_task(void *params);
-#else
-void led_blinking_task(void);
-void cdc_task(void);
#endif
+void led_blinking_task(void *param);
+void cdc_task(void *param);
+void print_devinfo_task(void *param);
+
void usb_mode_switch(void);
-static void print_device_info(uint8_t daddr);
+static void print_one_device(uint8_t daddr);
static void print_utf16(uint16_t* temp_buf, size_t buf_len);
// Declare buffer for USB transfer
@@ -124,11 +126,13 @@ int main(void) {
xTaskCreateStatic(usb_task, "usb", USBD_STACK_SIZE > USBH_STACK_SIZE ? USBD_STACK_SIZE : USBH_STACK_SIZE,
NULL, configMAX_PRIORITIES-1, usb_stack, &usb_taskdef);
xTaskCreateStatic(cdc_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, cdc_stack, &cdc_taskdef);
+ xTaskCreateStatic(print_devinfo_task, "devinfo", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, devinfo_stack, &devinfo_taskdef);
#else
xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL);
xTaskCreate(usb_task, "usb", USBD_STACK_SIZE > USBH_STACK_SIZE ? USBD_STACK_SIZE : USBH_STACK_SIZE,
NULL, configMAX_PRIORITIES - 1, NULL);
xTaskCreate(cdc_task, "cdc", CDC_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL);
+ xTaskCreate(print_devinfo_task, "devinfo", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL);
#endif
#ifndef ESP_PLATFORM
@@ -162,12 +166,13 @@ int main(void) {
// Process USB tasks based on current mode
if (current_role == TUSB_ROLE_DEVICE) {
tud_task();
- cdc_task();
+ cdc_task(NULL);
} else {
tuh_task();
+ print_devinfo_task(NULL);
}
- led_blinking_task();
+ led_blinking_task(NULL);
}
#endif
}
@@ -227,21 +232,28 @@ static void usb_task(void *param) {
void usb_mode_switch(void) {
printf("\r\n--- Switching USB mode ---\r\n");
- // Deinitialize current mode
- if (current_role == TUSB_ROLE_DEVICE) {
+ // Snapshot then clear current_role BEFORE tusb_deinit() so concurrent
+ // tasks (cdc_task / print_devinfo_task on RTOS) see the role-change
+ // boundary and exit cleanly instead of calling host/device APIs against
+ // a deinitialised stack.
+ const tusb_role_t prev_role = current_role;
+ current_role = TUSB_ROLE_INVALID;
+
+ if (prev_role == TUSB_ROLE_DEVICE) {
printf("Stopping DEVICE mode...\r\n");
- tusb_deinit(BOARD_RHPORT);
} else {
printf("Stopping HOST mode...\r\n");
- tusb_deinit(BOARD_RHPORT);
}
+ tusb_deinit(BOARD_RHPORT);
#if CFG_TUSB_OS == OPT_OS_FREERTOS
vTaskDelay(pdMS_TO_TICKS(100)); // Small delay for clean transition
#else
- tusb_time_delay_ms_api(100); // Small delay for clean transition
-#endif // Switch to the other mode
- if (current_role == TUSB_ROLE_DEVICE) {
+ tusb_time_delay_ms_api(100);
+#endif
+
+ // Switch to the other mode
+ if (prev_role == TUSB_ROLE_DEVICE) {
printf("Starting HOST mode...\r\n");
tusb_rhport_init_t host_init = {
.role = TUSB_ROLE_HOST,
@@ -267,61 +279,29 @@ void usb_mode_switch(void) {
// Device Mode: CDC Task
//--------------------------------------------------------------------+
+void cdc_task(void *param) {
+ (void) param;
#if CFG_TUSB_OS == OPT_OS_FREERTOS
-void cdc_task(void *params) {
- (void) params;
-
- // RTOS forever loop
while (1) {
- // Only process CDC when in device mode
+#endif
+ // Only touch device-CDC APIs while we're in device mode. After
+ // usb_mode_switch() sets current_role to INVALID and tusb_deinit() runs,
+ // calling tud_cdc_write_flush() here would hit a deinit'd device stack.
if (current_role == TUSB_ROLE_DEVICE) {
- // Connected and there are data available
- while (tud_cdc_available()) {
+ if (tud_cdc_available()) {
uint8_t buf[64];
-
- // Read data
- uint32_t count = tud_cdc_read(buf, sizeof(buf));
-
- // Echo back
- tud_cdc_write(buf, count);
-
- // Add newline for carriage return
- for (uint32_t i = 0; i < count; i++) {
- if (buf[i] == '\r') {
- tud_cdc_write_char('\n');
- break;
- }
+ const uint32_t count = tud_cdc_read(buf, sizeof(buf));
+ if (count) {
+ tud_cdc_write(buf, count);
}
}
-
tud_cdc_write_flush();
}
-
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
vTaskDelay(pdMS_TO_TICKS(10));
}
-}
-#else
-void cdc_task(void) {
- // Connected and there are data available
- if (tud_cdc_available()) {
- uint8_t buf[64];
-
- // Read data
- uint32_t count = tud_cdc_read(buf, sizeof(buf));
-
- // Echo back
- for (uint32_t i = 0; i < count; i++) {
- tud_cdc_write_char(buf[i]);
-
- if (buf[i] == '\r') {
- tud_cdc_write_char('\n');
- }
- }
-
- tud_cdc_write_flush();
- }
-}
#endif
+}
//--------------------------------------------------------------------+
// Device Callbacks
@@ -356,24 +336,56 @@ void tud_resume_cb(void) {
// Host Callbacks
//--------------------------------------------------------------------+
-// Invoked when device is mounted (configured)
+// One flag per possible device address — set by tuh_mount_cb (host task) and
+// cleared by print_devinfo_task once the device's descriptors are printed.
+static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1];
+
+// Invoked when device is mounted (configured). Runs in the host task — keep
+// minimal; descriptor fetching happens in print_devinfo_task (different
+// context so sync helpers are safe).
void tuh_mount_cb(uint8_t daddr) {
printf("[HOST] Device attached, address = %d\r\n", daddr);
blink_interval_ms = BLINK_MOUNTED;
- print_device_info(daddr);
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = true;
+ }
}
// Invoked when device is unmounted (unplugged)
void tuh_umount_cb(uint8_t daddr) {
printf("[HOST] Device removed, address = %d\r\n", daddr);
blink_interval_ms = BLINK_NOT_MOUNTED;
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = false;
+ }
}
//--------------------------------------------------------------------+
-// Host Device Info
+// Host Device Info — serialises descriptor fetching across all mounted
+// devices using sync helpers. Safe to call from main loop (OS_NONE) or a
+// dedicated task (FreeRTOS) — but NOT from a host-stack callback.
//--------------------------------------------------------------------+
-static void print_device_info(uint8_t daddr) {
+void print_devinfo_task(void *param) {
+ (void) param;
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ while (1) {
+#endif
+ if (current_role == TUSB_ROLE_HOST) {
+ for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) {
+ if (need_devinfo[daddr]) {
+ need_devinfo[daddr] = false;
+ print_one_device(daddr);
+ }
+ }
+ }
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ vTaskDelay(pdMS_TO_TICKS(10));
+ }
+#endif
+}
+
+static void print_one_device(uint8_t daddr) {
// Get Device Descriptor
uint8_t xfer_result = tuh_descriptor_get_device_sync(daddr, &desc.device, 18);
if (XFER_RESULT_SUCCESS != xfer_result) {
@@ -467,30 +479,23 @@ static void print_utf16(uint16_t* temp_buf, size_t buf_len) {
// Blinking Task
//--------------------------------------------------------------------+
-#if CFG_TUSB_OS == OPT_OS_FREERTOS
void led_blinking_task(void *param) {
(void) param;
+ static uint32_t start_ms = 0;
static bool led_state = false;
-
- // RTOS forever loop
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
while (1) {
- board_led_write(led_state);
- led_state = 1 - led_state; // toggle
vTaskDelay(pdMS_TO_TICKS(blink_interval_ms));
+ start_ms += blink_interval_ms;
+ board_led_write(led_state);
+ led_state = 1 - led_state;
}
-}
#else
-void led_blinking_task(void) {
- static uint32_t start_ms = 0;
- static bool led_state = false;
-
- // Blink every interval ms
if (tusb_time_millis_api() - start_ms < blink_interval_ms) {
return; // not enough time
}
start_ms += blink_interval_ms;
-
board_led_write(led_state);
- led_state = 1 - led_state; // toggle
-}
+ led_state = 1 - led_state;
#endif
+}
diff --git a/examples/dual/host_info_to_device_cdc/src/main.c b/examples/dual/host_info_to_device_cdc/src/main.c
index cf3430464..5186f91dc 100644
--- a/examples/dual/host_info_to_device_cdc/src/main.c
+++ b/examples/dual/host_info_to_device_cdc/src/main.c
@@ -130,7 +130,7 @@ static void main_task(void* param) {
led_blinking_task();
// preempted RTOS run device/host stack in its own task
-#if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO
+#if CFG_TUSB_OS_HAS_SCHEDULER == 0
tud_task(); // tinyusb device task
tuh_task(); // tinyusb host task
#endif
@@ -140,7 +140,7 @@ static void main_task(void* param) {
int main(void) {
board_init();
-#if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO
+#if CFG_TUSB_OS_HAS_SCHEDULER == 0
printf("TinyUSB Host Information -> Device CDC Example\r\n");
usb_device_init();
@@ -156,7 +156,7 @@ int main(void) {
return 0;
}
-#if CFG_TUSB_OS != OPT_OS_NONE && CFG_TUSB_OS != OPT_OS_PICO
+#if CFG_TUSB_OS_HAS_SCHEDULER
// USB Device Driver task for RTOS
static void usb_device_task(void *param) {
(void) param;
diff --git a/examples/host/bare_api/src/main.c b/examples/host/bare_api/src/main.c
index 544f38102..679ce6f43 100644
--- a/examples/host/bare_api/src/main.c
+++ b/examples/host/bare_api/src/main.c
@@ -48,14 +48,19 @@ CFG_TUH_MEM_SECTION uint16_t temp_buf[128]; // temp buffer for string descriptor
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+
void led_blinking_task(void);
+void print_devinfo_task(void);
static void print_utf16(uint16_t *temp_buf, size_t buf_len);
-void print_device_descriptor(tuh_xfer_t* xfer);
+static void print_one_device(uint8_t daddr);
void parse_config_descriptor(uint8_t dev_addr, tusb_desc_configuration_t const* desc_cfg);
uint8_t* get_hid_buf(uint8_t daddr);
void free_hid_buf(uint8_t daddr);
+// One flag per possible device address — set in tuh_mount_cb (host task) and
+// cleared by print_devinfo_task (main loop) once the descriptors are printed.
+static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1];
+
/*------------- MAIN -------------*/
int main(void) {
board_init();
@@ -74,38 +79,53 @@ int main(void) {
while (1) {
// tinyusb host task
tuh_task();
+ print_devinfo_task();
led_blinking_task();
}
}
/*------------- TinyUSB Callbacks -------------*/
-// Invoked when device is mounted (configured)
+// Invoked when device is mounted (configured). Runs in the host task — keep
+// it minimal. The descriptor fetching/printing happens in print_devinfo_task()
+// below where the sync helpers are safe (different context).
void tuh_mount_cb(uint8_t daddr) {
printf("Device attached, address = %d\r\n", daddr);
-
- // Get Device Descriptor
- // TODO: invoking control transfer now has issue with mounting hub with multiple devices attached, fix later
- tuh_descriptor_get_device(daddr, &desc_device, 18, print_device_descriptor, 0);
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = true;
+ }
}
/// Invoked when device is unmounted (bus reset/unplugged)
void tuh_umount_cb(uint8_t daddr) {
printf("Device removed, address = %d\r\n", daddr);
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = false;
+ }
free_hid_buf(daddr);
}
//--------------------------------------------------------------------+
-// Device Descriptor
+// Print device info task — serialises descriptor fetching across all
+// mounted devices via sync helpers. Sync calls are safe here because this
+// runs in the main loop (outside any host-stack callback context).
//--------------------------------------------------------------------+
-void print_device_descriptor(tuh_xfer_t *xfer) {
- if (XFER_RESULT_SUCCESS != xfer->result) {
+void print_devinfo_task(void) {
+ for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) {
+ if (need_devinfo[daddr]) {
+ need_devinfo[daddr] = false;
+ print_one_device(daddr);
+ }
+ }
+}
+
+static void print_one_device(uint8_t daddr) {
+ // Get Device Descriptor
+ if (XFER_RESULT_SUCCESS != tuh_descriptor_get_device_sync(daddr, &desc_device, 18)) {
printf("Failed to get device descriptor\r\n");
return;
}
- uint8_t const daddr = xfer->daddr;
-
printf("Device %u: ID %04x:%04x\r\n", daddr, desc_device.idVendor, desc_device.idProduct);
printf("Device Descriptor:\r\n");
printf(" bLength %u\r\n" , desc_device.bLength);
@@ -119,7 +139,6 @@ void print_device_descriptor(tuh_xfer_t *xfer) {
printf(" idProduct 0x%04x\r\n" , desc_device.idProduct);
printf(" bcdDevice %04x\r\n" , desc_device.bcdDevice);
- // Get String descriptor using Sync API
printf(" iManufacturer %u ", desc_device.iManufacturer);
if (XFER_RESULT_SUCCESS == tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, temp_buf, sizeof(temp_buf))) {
print_utf16(temp_buf, TU_ARRAY_SIZE(temp_buf));
diff --git a/examples/host/cdc_msc_hid/src/cdc_app.c b/examples/host/cdc_msc_hid/src/cdc_app.c
index 20033981e..e6c190715 100644
--- a/examples/host/cdc_msc_hid/src/cdc_app.c
+++ b/examples/host/cdc_msc_hid/src/cdc_app.c
@@ -95,7 +95,6 @@ void tuh_cdc_mount_cb(uint8_t idx) {
printf("CDC Interface is mounted: address = %u, itf_num = %u\r\n", itf_info.daddr,
itf_info.desc.bInterfaceNumber);
-#ifdef CFG_TUH_CDC_LINE_CODING_ON_ENUM
// If CFG_TUH_CDC_LINE_CODING_ON_ENUM is defined, line coding will be set by tinyusb stack
// while eneumerating new cdc device
cdc_line_coding_t line_coding = {0};
@@ -103,11 +102,6 @@ void tuh_cdc_mount_cb(uint8_t idx) {
printf(" Baudrate: %" PRIu32 ", Stop Bits : %u\r\n", line_coding.bit_rate, line_coding.stop_bits);
printf(" Parity : %u, Data Width: %u\r\n", line_coding.parity, line_coding.data_bits);
}
-#else
- // Set Line Coding upon mounted
- cdc_line_coding_t new_line_coding = { 115200, CDC_LINE_CODING_STOP_BITS_1, CDC_LINE_CODING_PARITY_NONE, 8 };
- tuh_cdc_set_line_coding(idx, &new_line_coding, NULL, 0);
-#endif
}
// Invoked when a device with CDC interface is unmounted
diff --git a/examples/host/device_info/src/main.c b/examples/host/device_info/src/main.c
index b0e38dd6b..f32ed1a3e 100644
--- a/examples/host/device_info/src/main.c
+++ b/examples/host/device_info/src/main.c
@@ -72,8 +72,14 @@ CFG_TUH_MEM_SECTION struct {
} desc;
void led_blinking_task(void* param);
+void print_devinfo_task(void* param);
static void print_utf16(uint16_t* temp_buf, size_t buf_len);
+// One flag per possible device address — set by tuh_mount_cb (host task) and
+// cleared by print_devinfo_task (separate task / main loop) once the device's
+// descriptor info has been printed.
+static volatile bool need_devinfo[CFG_TUH_DEVICE_MAX + 1];
+
#if CFG_TUSB_OS == OPT_OS_FREERTOS
void init_freertos_task(void);
#endif
@@ -103,6 +109,7 @@ int main(void) {
init_tinyusb();
while (1) {
tuh_task(); // tinyusb host task
+ print_devinfo_task(NULL);
led_blinking_task(NULL);
}
#endif
@@ -110,10 +117,34 @@ int main(void) {
/*------------- TinyUSB Callbacks -------------*/
-// Invoked when device is mounted (configured)
+// Invoked when device is mounted (configured). Runs in the host task — keep
+// it minimal. The actual descriptor fetching/printing happens in
+// print_devinfo_task() below, which runs in a different context (main loop
+// on OS_NONE / dedicated task on RTOS) where the sync helpers are safe.
void tuh_mount_cb(uint8_t daddr) {
blink_interval_ms = BLINK_MOUNTED;
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = true;
+ }
+}
+// Invoked when device is unmounted (bus reset/unplugged)
+void tuh_umount_cb(uint8_t daddr) {
+ blink_interval_ms = BLINK_NOT_MOUNTED;
+ if (daddr < TU_ARRAY_SIZE(need_devinfo)) {
+ need_devinfo[daddr] = false;
+ }
+ printf("Device removed, address = %d\r\n", daddr);
+}
+
+//--------------------------------------------------------------------+
+// Print device info task — serialises descriptor fetching across all
+// mounted devices using the sync helpers. Sync calls are safe here because
+// this task runs outside the host-task callback context (main loop on
+// OS_NONE / dedicated FreeRTOS task on RTOS).
+//--------------------------------------------------------------------+
+
+static void print_one_device(uint8_t daddr) {
// Get Device Descriptor
uint8_t xfer_result = tuh_descriptor_get_device_sync(daddr, &desc.device, 18);
if (XFER_RESULT_SUCCESS != xfer_result) {
@@ -129,9 +160,8 @@ void tuh_mount_cb(uint8_t daddr) {
}
if (XFER_RESULT_SUCCESS != xfer_result) {
uint16_t* serial = (uint16_t*)(uintptr_t) desc.serial;
-
serial[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * 1 + 2));
- serial[1] = '0'; // simply 0
+ serial[1] = '0';
serial[2] = 0;
}
print_utf16((uint16_t*)(uintptr_t) desc.serial, sizeof(desc.serial)/2);
@@ -149,12 +179,9 @@ void tuh_mount_cb(uint8_t daddr) {
printf(" idProduct 0x%04x\r\n", desc.device.idProduct);
printf(" bcdDevice %04x\r\n", desc.device.bcdDevice);
- // Get String descriptor using Sync API
-
printf(" iManufacturer %u ", desc.device.iManufacturer);
if (desc.device.iManufacturer != 0) {
- xfer_result = tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf));
- if (XFER_RESULT_SUCCESS == xfer_result) {
+ if (XFER_RESULT_SUCCESS == tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf))) {
print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2);
}
}
@@ -162,22 +189,33 @@ void tuh_mount_cb(uint8_t daddr) {
printf(" iProduct %u ", desc.device.iProduct);
if (desc.device.iProduct != 0) {
- xfer_result = tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf));
- if (XFER_RESULT_SUCCESS == xfer_result) {
+ if (XFER_RESULT_SUCCESS == tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf))) {
print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2);
}
}
printf("\r\n");
printf(" iSerialNumber %u ", desc.device.iSerialNumber);
- printf("%s\r\n", (char*)desc.serial); // serial is already to UTF-8
+ printf("%s\r\n", (char*)desc.serial); // serial is already UTF-8
printf(" bNumConfigurations %u\r\n", desc.device.bNumConfigurations);
}
-// Invoked when device is unmounted (bus reset/unplugged)
-void tuh_umount_cb(uint8_t daddr) {
- blink_interval_ms = BLINK_NOT_MOUNTED;
- printf("Device removed, address = %d\r\n", daddr);
+void print_devinfo_task(void* param) {
+ (void) param;
+
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ while (1) {
+#endif
+ for (uint8_t daddr = 1; daddr < TU_ARRAY_SIZE(need_devinfo); daddr++) {
+ if (need_devinfo[daddr]) {
+ need_devinfo[daddr] = false;
+ print_one_device(daddr);
+ }
+ }
+#if CFG_TUSB_OS == OPT_OS_FREERTOS
+ vTaskDelay(pdMS_TO_TICKS(10));
+ }
+#endif
}
//--------------------------------------------------------------------+
@@ -278,6 +316,9 @@ StaticTask_t blinky_taskdef;
StackType_t usb_stack[USB_STACK_SIZE];
StaticTask_t usb_taskdef;
+
+StackType_t devinfo_stack[USB_STACK_SIZE];
+StaticTask_t devinfo_taskdef;
#endif
#ifdef ESP_PLATFORM
@@ -299,9 +340,11 @@ void init_freertos_task(void) {
#if configSUPPORT_STATIC_ALLOCATION
xTaskCreateStatic(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, blinky_stack, &blinky_taskdef);
xTaskCreateStatic(usb_host_task, "usbh", USB_STACK_SIZE, NULL, configMAX_PRIORITIES-1, usb_stack, &usb_taskdef);
+ xTaskCreateStatic(print_devinfo_task, "devinfo", USB_STACK_SIZE, NULL, configMAX_PRIORITIES-2, devinfo_stack, &devinfo_taskdef);
#else
xTaskCreate(led_blinking_task, "blinky", BLINKY_STACK_SIZE, NULL, 1, NULL);
xTaskCreate(usb_host_task, "usbh", USB_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL);
+ xTaskCreate(print_devinfo_task, "devinfo", USB_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL);
#endif
// only start scheduler for non-espressif mcu
diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c
index 63b08318c..d81e77a33 100644
--- a/examples/host/midi2_host/src/main.c
+++ b/examples/host/midi2_host/src/main.c
@@ -35,7 +35,7 @@
// State
//--------------------------------------------------------------------+
-static uint8_t midi2_idx = 0xFF;
+TU_ATTR_UNUSED static uint8_t midi2_idx = 0xFF;
//--------------------------------------------------------------------+
// UMP printer - shows MT and word(s) in hex; decodes Channel Voice
@@ -160,6 +160,4 @@ int main(void) {
while (1) {
tuh_task();
}
-
- return 0;
}
diff --git a/examples/host/msc_file_explorer_freertos/skip.txt b/examples/host/msc_file_explorer_freertos/skip.txt
index f0be07d25..a8c9bea2a 100644
--- a/examples/host/msc_file_explorer_freertos/skip.txt
+++ b/examples/host/msc_file_explorer_freertos/skip.txt
@@ -1,3 +1,4 @@
mcu:CH32F20X
board:lpcxpresso54114
mcu:FT90X
+board:stm32h7s3nucleo
diff --git a/src/common/tusb_types.h b/src/common/tusb_types.h
index 959fc129a..cb06b89bb 100644
--- a/src/common/tusb_types.h
+++ b/src/common/tusb_types.h
@@ -282,6 +282,7 @@ typedef enum {
XFER_RESULT_FAILED,
XFER_RESULT_STALLED,
XFER_RESULT_TIMEOUT,
+ XFER_RESULT_ABORTED,
XFER_RESULT_INVALID
} xfer_result_t;
diff --git a/src/host/usbh.c b/src/host/usbh.c
index 2e3c93c5e..9d159985e 100644
--- a/src/host/usbh.c
+++ b/src/host/usbh.c
@@ -40,6 +40,14 @@
#define CFG_TUH_TASK_QUEUE_SZ 16
#endif
+#ifndef CFG_TUH_CONTROL_PENDING_QUEUE_SZ
+ #if CFG_TUH_HUB
+ #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4
+ #else
+ #define CFG_TUH_CONTROL_PENDING_QUEUE_SZ 2
+ #endif
+#endif
+
#ifndef CFG_TUH_INTERFACE_MAX
#define CFG_TUH_INTERFACE_MAX 8
#endif
@@ -175,11 +183,11 @@ static OSAL_SPINLOCK_DEF(_usbh_spin, usbh_int_set);
OSAL_QUEUE_DEF(usbh_int_set, _usbh_qdef, CFG_TUH_TASK_QUEUE_SZ, hcd_event_t);
static osal_queue_t _usbh_q;
- #if CFG_TUH_HUB
+#if CFG_TUH_HUB
// Deferred attachment queue, only needed when using hub
OSAL_QUEUE_DEF(usbh_int_set, _usbh_daqdef, CFG_TUH_HUB, hcd_event_t);
static osal_queue_t _usbh_daq;
- #endif
+#endif
// Control transfers: since most controllers do not support multiple control transfers
// on multiple devices concurrently and control transfers are not used much except for
@@ -189,9 +197,9 @@ typedef struct {
tuh_xfer_cb_t complete_cb;
uintptr_t user_data;
+ volatile uint16_t actual_len;
volatile uint8_t stage;
uint8_t daddr;
- volatile uint16_t actual_len;
uint8_t failed_count;
} usbh_ctrl_xfer_info_t;
@@ -202,17 +210,32 @@ typedef struct {
} usbh_call_after_t;
typedef struct {
- uint8_t controller_id; // controller ID
+ tusb_control_request_t setup;
+ uint8_t* buffer;
+ tuh_xfer_cb_t complete_cb;
+ uintptr_t user_data;
+ uint8_t daddr;
+ uint8_t daddr_gen;
+} usbh_pending_ctrl_t;
+
+// FIFO for pending async control transfers since we only execute 1 control transfer at a time
+TU_FIFO_DEF(_usbh_pending_ctrl_q, CFG_TUH_CONTROL_PENDING_QUEUE_SZ * sizeof(usbh_pending_ctrl_t), false);
+
+typedef struct {
uint8_t enumerating_daddr; // device address of the device being enumerated
uint8_t attach_debouncing_bm; // bitmask for roothub port attach debouncing
tuh_bus_info_t dev0_bus; // bus info for dev0 in enumeration
usbh_ctrl_xfer_info_t ctrl_xfer_info; // control transfer
usbh_call_after_t call_after;
+ // Per-daddr generation counter — bumped on usbh_device_close() to identify stale pending control transfer
+ uint8_t daddr_gen[TOTAL_DEVICES + 1];
+#if CFG_TUSB_OS_HAS_SCHEDULER
+ osal_task_handle_t task_hdl; // host task handle, lazy-captured on first tuh_task_ext()
+#endif
} usbh_data_t;
-static usbh_data_t _usbh_data = {
- .controller_id = TUSB_INDEX_INVALID_8,
-};
+static uint8_t _usbh_controller_id = TUSB_INDEX_INVALID_8;
+static usbh_data_t _usbh_data;
typedef struct {
TUH_EPBUF_TYPE_DEF(tusb_control_request_t, request);
@@ -346,8 +369,11 @@ static void enum_new_device(hcd_event_t* event);
static void enum_delay_async(uintptr_t state);
static void process_remove_event(hcd_event_t *event);
static void remove_device_tree(uint8_t rhport, uint8_t hub_addr, uint8_t hub_port);
+
static bool usbh_edpt_control_open(uint8_t dev_addr, uint8_t max_packet_size);
static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes);
+static void control_xfer_dispatch_pending(void);
+static void control_xfer_complete(uint8_t daddr, xfer_result_t result);
TU_ATTR_ALWAYS_INLINE static inline usbh_device_t* get_device(uint8_t dev_addr) {
TU_VERIFY(dev_addr > 0 && dev_addr <= TOTAL_DEVICES, NULL);
@@ -364,7 +390,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(hcd_event_t const * event,
return true;
}
-TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage) {
+TU_ATTR_ALWAYS_INLINE static inline void control_xfer_set_stage(uint8_t stage) {
if (_usbh_data.ctrl_xfer_info.stage != stage) {
(void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
_usbh_data.ctrl_xfer_info.stage = stage;
@@ -372,15 +398,6 @@ TU_ATTR_ALWAYS_INLINE static inline void _control_set_xfer_stage(uint8_t stage)
}
}
-TU_ATTR_ALWAYS_INLINE static inline bool usbh_setup_send(uint8_t daddr, const uint8_t setup_packet[8]) {
- const uint8_t rhport = usbh_get_rhport(daddr);
- const bool ret = hcd_setup_send(rhport, daddr, setup_packet);
- if (!ret) {
- _control_set_xfer_stage(CONTROL_STAGE_IDLE);
- }
- return ret;
-}
-
bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t param) {
TU_ASSERT(_usbh_data.call_after.func == NULL);
TU_LOG_USBH("USBH schedule function after %u ms\r\n", (unsigned int)ms);
@@ -394,9 +411,16 @@ bool usbh_defer_func_ms_async(uint32_t ms, tusb_defer_func_t func, uintptr_t par
TU_ATTR_ALWAYS_INLINE static inline void usbh_device_close(uint8_t rhport, uint8_t daddr) {
hcd_device_close(rhport, daddr);
- // abort any ongoing control transfer
- if (daddr == _usbh_data.ctrl_xfer_info.daddr) {
- _control_set_xfer_stage(CONTROL_STAGE_IDLE);
+ // Bump the generation under the mutex so a concurrent producer in
+ // tuh_control_xfer stamps a value that is strictly monotonic w.r.t. close.
+ (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
+ _usbh_data.daddr_gen[daddr]++;
+ (void) osal_mutex_unlock(_usbh_mutex);
+
+ // If this device has in-flight control xfer, complete as FAILED
+ usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
+ if (daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE) {
+ control_xfer_complete(daddr, XFER_RESULT_FAILED);
}
// invalidate if enumerating
@@ -458,7 +482,7 @@ tusb_speed_t tuh_speed_get(uint8_t daddr) {
}
bool tuh_rhport_is_active(uint8_t rhport) {
- return _usbh_data.controller_id == rhport;
+ return _usbh_controller_id == rhport;
}
bool tuh_rhport_reset_bus(uint8_t rhport, bool active) {
@@ -485,7 +509,7 @@ static void clear_device(usbh_device_t* dev) {
}
bool tuh_inited(void) {
- return _usbh_data.controller_id != TUSB_INDEX_INVALID_8;
+ return _usbh_controller_id != TUSB_INDEX_INVALID_8;
}
bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
@@ -547,7 +571,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
tu_memclr(_usbh_devices, sizeof(_usbh_devices));
tu_memclr(&_usbh_data, sizeof(_usbh_data));
- _usbh_data.controller_id = TUSB_INDEX_INVALID_8;
+ _usbh_controller_id = TUSB_INDEX_INVALID_8;
_usbh_data.enumerating_daddr = TUSB_INDEX_INVALID_8;
for (uint8_t i = 0; i < TOTAL_DEVICES; i++) {
@@ -565,7 +589,7 @@ bool tuh_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
}
// Init host controller
- _usbh_data.controller_id = rhport;
+ _usbh_controller_id = rhport;
TU_ASSERT(hcd_init(rhport, rh_init));
hcd_int_enable(rhport);
@@ -580,7 +604,7 @@ bool tuh_deinit(uint8_t rhport) {
// deinit host controller
hcd_int_disable(rhport);
TU_ASSERT(hcd_deinit(rhport));
- _usbh_data.controller_id = TUSB_INDEX_INVALID_8;
+ _usbh_controller_id = TUSB_INDEX_INVALID_8;
// remove all devices on this rhport (hub_addr = 0, hub_port = 0)
remove_device_tree(rhport, 0, 0);
@@ -604,6 +628,25 @@ bool tuh_deinit(uint8_t rhport) {
_usbh_daq = NULL;
#endif
+ // Fire FAILED cb for any queued async control xfer so callers aren't stranded.
+ usbh_pending_ctrl_t pending;
+ while (tu_fifo_read_n(&_usbh_pending_ctrl_q, &pending, sizeof(pending)) == sizeof(pending)) {
+ if (pending.complete_cb) {
+ tuh_xfer_t x = {
+ .daddr = pending.daddr,
+ .ep_addr = 0,
+ .result = XFER_RESULT_FAILED,
+ .actual_len = 0,
+ .setup = &pending.setup,
+ .buffer = pending.buffer,
+ .complete_cb = pending.complete_cb,
+ .user_data = pending.user_data,
+ };
+ pending.complete_cb(&x);
+ }
+ }
+ tu_fifo_clear(&_usbh_pending_ctrl_q);
+
#if OSAL_MUTEX_REQUIRED
// TODO make sure there is no task waiting on this mutex
osal_mutex_delete(_usbh_mutex);
@@ -629,6 +672,12 @@ bool tuh_task_event_ready(void) {
}
#endif
+ // Pending control xfer waiting for an idle slot
+ if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE &&
+ !tu_fifo_empty(&_usbh_pending_ctrl_q)) {
+ return true;
+ }
+
if (_usbh_data.call_after.func) {
int32_t remain_ms = (int32_t)(_usbh_data.call_after.at_ms - tusb_time_millis_api());
if (remain_ms <= 0) {
@@ -663,6 +712,13 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) {
(void) in_isr; // not implemented yet
+#if CFG_TUSB_OS_HAS_SCHEDULER
+ // Save task handle on 1st run
+ if (_usbh_data.task_hdl == NULL) {
+ _usbh_data.task_hdl = osal_task_get_current_handle();
+ }
+#endif
+
// Loop until there are no more events in the queue or CFG_TUH_TASK_EVENTS_PER_RUN is reached
for (unsigned epr = 0;; epr++) {
#if CFG_TUH_TASK_EVENTS_PER_RUN > 0
@@ -695,6 +751,16 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) {
}
}
+ // Drain pending async control xfers. Slot transitions and dispatch are
+ // decoupled: completion / abort / device_close set stage = IDLE via
+ // control_xfer_set_stage() and the actual FIFO drain happens here in the
+ // event loop. The check is a fast non-mutex sanity gate; the dispatcher
+ // itself re-checks under the mutex.
+ if (_usbh_data.ctrl_xfer_info.stage == CONTROL_STAGE_IDLE &&
+ !tu_fifo_empty(&_usbh_pending_ctrl_q)) {
+ control_xfer_dispatch_pending();
+ }
+
hcd_event_t event;
#if CFG_TUH_HUB
@@ -818,73 +884,179 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) {
// Control transfer
//--------------------------------------------------------------------+
-static void _control_blocking_complete_cb(tuh_xfer_t* xfer) {
- // update result
- *((xfer_result_t*) xfer->user_data) = xfer->result;
+// Carries both fields the sync waiter cares about — capturing from xfer_temp
+// (snapshot taken before release_slot resets ctrl_info for the next pending
+// entry) so the waiter sees this xfer's data, not the next dispatched one's.
+typedef struct {
+ volatile xfer_result_t result;
+ volatile uint32_t actual_len;
+} control_xfer_sync_param_t;
+
+static void control_xfer_sync_complete(tuh_xfer_t* xfer) {
+ control_xfer_sync_param_t* s = (control_xfer_sync_param_t*) xfer->user_data;
+ s->actual_len = xfer->actual_len;
+ s->result = xfer->result;
}
// TODO timeout_ms is not supported yet
bool tuh_control_xfer (tuh_xfer_t* xfer) {
- TU_VERIFY(xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet
const uint8_t daddr = xfer->daddr;
- TU_VERIFY(tuh_connected(daddr));
-
+ TU_VERIFY(daddr <= TOTAL_DEVICES && xfer->ep_addr == 0 && xfer->setup); // EP0 with setup packet
usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
- TU_VERIFY(ctrl_info->stage == CONTROL_STAGE_IDLE); // pre-check to help reducing mutex lock
- (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
- bool const is_idle = (ctrl_info->stage == CONTROL_STAGE_IDLE);
- if (is_idle) {
- ctrl_info->stage = CONTROL_STAGE_SETUP;
- ctrl_info->daddr = daddr;
- ctrl_info->actual_len = 0;
- ctrl_info->failed_count = 0;
+#if CFG_TUSB_OS_HAS_SCHEDULER
+ // Sync (complete_cb == NULL) from a host-stack callback is forbidden on
+ // RTOS targets — the event-loop driver can't block on its own pending xfer
+ // (deadlock if other control xfers are queued behind). Use async with a
+ // chained cb instead. OS_NONE / OS_PICO are exempt: they have a single
+ // execution context and the recursive-drive path is the only way to wait.
+ TU_ASSERT(!(xfer->complete_cb == NULL &&
+ osal_task_get_current_handle() == _usbh_data.task_hdl));
+#endif
- ctrl_info->buffer = xfer->buffer;
- ctrl_info->complete_cb = xfer->complete_cb;
- ctrl_info->user_data = xfer->user_data;
- _usbh_epbuf.request = (*xfer->setup);
- }
- (void) osal_mutex_unlock(_usbh_mutex);
+ // Slot is single-threaded — when busy, sync callers block until it frees
+ // (blocking semantics require the result); async callers get queued in the
+ // pending FIFO and submitted by control_xfer_complete() when the slot
+ // drains. The test-and-{claim|enqueue} is one critical section so a slot
+ // that becomes IDLE between the check and the enqueue can't strand an async
+ // request in a queue nothing else drains.
+ const bool is_nonblocking = (xfer->complete_cb != NULL);
+ while (true) {
+ TU_VERIFY(tuh_connected(daddr));
+ bool claimed = false;
+ bool is_queued = false;
+ (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
+ if (ctrl_info->stage == CONTROL_STAGE_IDLE) {
+ ctrl_info->stage = CONTROL_STAGE_SETUP;
+ ctrl_info->daddr = daddr;
+ ctrl_info->actual_len = 0;
+ ctrl_info->failed_count = 0;
+
+ ctrl_info->buffer = xfer->buffer;
+ ctrl_info->complete_cb = xfer->complete_cb;
+ ctrl_info->user_data = xfer->user_data;
+ _usbh_epbuf.request = (*xfer->setup);
+ claimed = true;
+ } else if (is_nonblocking) {
+ // Async + busy: queue the transfer.
+ const usbh_pending_ctrl_t entry = {
+ .setup = *xfer->setup,
+ .buffer = xfer->buffer,
+ .complete_cb = xfer->complete_cb,
+ .user_data = xfer->user_data,
+ .daddr = daddr,
+ .daddr_gen = _usbh_data.daddr_gen[daddr]
+ };
+ is_queued = tu_fifo_write_n(&_usbh_pending_ctrl_q, &entry, sizeof(entry)) == sizeof(entry);
+ }
+
+ (void) osal_mutex_unlock(_usbh_mutex);
+
+ if (claimed) {
+ break;
+ }
+
+ if (is_nonblocking) {
+ return is_queued;
+ }
- TU_VERIFY(is_idle);
+ // - OS_HAS_SCHEDULER: delay 1 ms
+ // - Otherwise: single execution context; drive the loop ourselves to progress the in-flight transfer.
+#if CFG_TUSB_OS_HAS_SCHEDULER
+ osal_task_delay(1);
+#else
+ tuh_task_ext(0, false);
+#endif
+ }
TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(daddr), daddr,
(xfer->setup->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer->setup->bRequest <= TUSB_REQ_SYNCH_FRAME) ?
tu_str_std_request[xfer->setup->bRequest] : "Class Request");
TU_LOG_BUF_USBH(xfer->setup, 8);
- if (xfer->complete_cb != NULL) {
- TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request));
- }else {
- // blocking if complete callback is not provided
- // change callback to internal blocking, and result as user argument
- volatile xfer_result_t result = XFER_RESULT_INVALID;
-
- // use user_data to point to xfer_result_t
- ctrl_info->user_data = (uintptr_t) &result;
- ctrl_info->complete_cb = _control_blocking_complete_cb;
+ // Sync: wire control_xfer_sync_complete BEFORE submit so a fast completion
+ // event has the cb in place. control_xfer_complete() captures both result
+ // and actual_len through this cb before release_slot overwrites ctrl_info.
+ volatile control_xfer_sync_param_t sync_state;
+ if (!is_nonblocking) {
+ sync_state.result = XFER_RESULT_INVALID;
+ sync_state.actual_len = 0;
+ ctrl_info->user_data = (uintptr_t) &sync_state;
+ ctrl_info->complete_cb = control_xfer_sync_complete;
+ }
- TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) &_usbh_epbuf.request));
+ if (!hcd_setup_send(usbh_get_rhport(daddr), daddr, (uint8_t const *) &_usbh_epbuf.request)) {
+ control_xfer_set_stage(CONTROL_STAGE_IDLE);
+ return false;
+ }
- while (result == XFER_RESULT_INVALID) {
- // Note: this can be called within an callback ie. part of tuh_task()
- // therefore even with RTOS tuh_task_ext() still need to be invoked
+ if (!is_nonblocking) {
+ // No tuh_connected() escape needed: usbh_device_close() routes through
+ // control_xfer_complete(daddr, FAILED) on disconnect, which fires
+ // sync_complete and unblocks this poll.
+ while (sync_state.result == XFER_RESULT_INVALID) {
+#if CFG_TUSB_OS_HAS_SCHEDULER
+ osal_task_delay(1);
+#else
tuh_task_ext(0, false);
- // TODO probably some timeout to prevent hanged
+#endif
}
- // update transfer result, user_data is expected to point to xfer_result_t
+ // Forward to caller (xfer->user_data, if set, is a xfer_result_t pointer).
if (xfer->user_data != 0) {
- *((xfer_result_t*) xfer->user_data) = result;
+ *((xfer_result_t*) xfer->user_data) = sync_state.result;
}
- xfer->result = result;
- xfer->actual_len = ctrl_info->actual_len;
+ xfer->result = sync_state.result;
+ xfer->actual_len = sync_state.actual_len;
}
return true;
}
-static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) {
+// Start control transfer from pending fifo
+static void control_xfer_dispatch_pending(void) {
+ usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
+
+ while (true) {
+ usbh_pending_ctrl_t xfer;
+ bool has_xfer = false;
+
+ (void) osal_mutex_lock(_usbh_mutex, OSAL_TIMEOUT_WAIT_FOREVER);
+ if (ctrl_info->stage == CONTROL_STAGE_IDLE &&
+ tu_fifo_read_n(&_usbh_pending_ctrl_q, &xfer, sizeof(xfer)) == sizeof(xfer)) {
+ ctrl_info->stage = CONTROL_STAGE_SETUP;
+ ctrl_info->daddr = xfer.daddr;
+ ctrl_info->actual_len = 0;
+ ctrl_info->failed_count = 0;
+ ctrl_info->buffer = xfer.buffer;
+ ctrl_info->complete_cb = xfer.complete_cb;
+ ctrl_info->user_data = xfer.user_data;
+ _usbh_epbuf.request = xfer.setup;
+ has_xfer = true;
+ }
+ (void) osal_mutex_unlock(_usbh_mutex);
+
+ if (!has_xfer) {
+ return; // nothing to do
+ }
+
+ // mismatched daddr_gen means pending transfer is stale due to the device got disconnected while in the FIFO
+ // Note: the address can be re-allocated to another device at this point.
+ if (xfer.daddr_gen == _usbh_data.daddr_gen[xfer.daddr]) {
+ TU_LOG_USBH("[%u:%u] %s: ", usbh_get_rhport(xfer.daddr), xfer.daddr,
+ (xfer.setup.bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && xfer.setup.bRequest <= TUSB_REQ_SYNCH_FRAME) ?
+ tu_str_std_request[xfer.setup.bRequest] : "Class Request");
+ TU_LOG_BUF_USBH(&xfer.setup, 8);
+ if (hcd_setup_send(usbh_get_rhport(xfer.daddr), xfer.daddr, (uint8_t const *) &_usbh_epbuf.request)) {
+ return; // transfer kicked-off, we are done
+ }
+ }
+
+ // complete callback as FAILED and continue with next pending xfer
+ control_xfer_complete(xfer.daddr, XFER_RESULT_FAILED);
+ }
+}
+
+static void control_xfer_complete(uint8_t daddr, xfer_result_t result) {
TU_LOG_USBH("\r\n");
usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
@@ -901,7 +1073,8 @@ static void _control_xfer_complete(uint8_t daddr, xfer_result_t result) {
.user_data = ctrl_info->user_data
};
- _control_set_xfer_stage(CONTROL_STAGE_IDLE);
+ // set to IDLE before callback since cb can invoke another transfer
+ control_xfer_set_stage(CONTROL_STAGE_IDLE);
if (xfer_temp.complete_cb != NULL) {
xfer_temp.complete_cb(&xfer_temp);
@@ -915,11 +1088,17 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t
tusb_control_request_t const * request = &_usbh_epbuf.request;
usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
+ // Drop stale completions: slot already released (abort/close fired its cb)
+ // or now owns a different device's xfer (a pending entry was dispatched).
+ if (ctrl_info->stage == CONTROL_STAGE_IDLE || ctrl_info->daddr != daddr) {
+ return true;
+ }
+
switch (result) {
case XFER_RESULT_STALLED:
TU_LOG_USBH("[%u:%u] Control STALLED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes);
TU_LOG_BUF_USBH(request, 8);
- _control_xfer_complete(daddr, result);
+ control_xfer_complete(daddr, result);
break;
case XFER_RESULT_FAILED:
@@ -931,11 +1110,14 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t
ctrl_info->actual_len = 0; // reset actual_len
(void) osal_mutex_unlock(_usbh_mutex);
- TU_ASSERT(usbh_setup_send(daddr, (uint8_t const *) request));
+ if (!hcd_setup_send(rhport, daddr, (uint8_t const *) request)) {
+ control_xfer_complete(daddr, XFER_RESULT_FAILED);
+ return false;
+ }
} else {
TU_LOG_USBH("[%u:%u] Control FAILED, xferred_bytes = %" PRIu32 "\r\n", rhport, daddr, xferred_bytes);
TU_LOG_BUF_USBH(request, 8);
- _control_xfer_complete(daddr, result);
+ control_xfer_complete(daddr, result);
}
break;
@@ -944,7 +1126,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t
case CONTROL_STAGE_SETUP:
if (request->wLength > 0) {
// DATA stage: initial data toggle is always 1
- _control_set_xfer_stage(CONTROL_STAGE_DATA);
+ control_xfer_set_stage(CONTROL_STAGE_DATA);
const uint8_t ep_data = tu_edpt_addr(0, request->bmRequestType_bit.direction);
TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_data, ctrl_info->buffer, request->wLength));
return true;
@@ -959,7 +1141,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t
ctrl_info->actual_len = (uint16_t) xferred_bytes;
// ACK stage: toggle is always 1
- _control_set_xfer_stage(CONTROL_STAGE_ACK);
+ control_xfer_set_stage(CONTROL_STAGE_ACK);
const uint8_t ep_status = tu_edpt_addr(0, 1 - request->bmRequestType_bit.direction);
TU_ASSERT(hcd_edpt_xfer(rhport, daddr, ep_status, NULL, 0));
break;
@@ -976,7 +1158,7 @@ static bool usbh_control_xfer_cb (uint8_t daddr, uint8_t ep_addr, xfer_result_t
}
}
- _control_xfer_complete(daddr, result);
+ control_xfer_complete(daddr, result);
break;
}
@@ -1023,7 +1205,7 @@ bool tuh_edpt_abort_xfer(uint8_t daddr, uint8_t ep_addr) {
const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
TU_VERIFY(daddr == ctrl_info->daddr && ctrl_info->stage != CONTROL_STAGE_IDLE);
hcd_edpt_abort_xfer(rhport, daddr, ep_addr);
- _control_set_xfer_stage(CONTROL_STAGE_IDLE); // reset control transfer state to idle
+ control_xfer_complete(daddr, XFER_RESULT_ABORTED);
} else {
usbh_device_t* dev = get_device(daddr);
TU_VERIFY(dev);
@@ -1055,9 +1237,9 @@ uint8_t *usbh_get_enum_buf(void) {
void usbh_int_set(bool enabled) {
// TODO all host controller if multiple are used since they shared the same event queue
if (enabled) {
- hcd_int_enable(_usbh_data.controller_id);
+ hcd_int_enable(_usbh_controller_id);
} else {
- hcd_int_disable(_usbh_data.controller_id);
+ hcd_int_disable(_usbh_controller_id);
}
}
diff --git a/src/osal/osal.h b/src/osal/osal.h
index 4840463f3..69cb356d4 100644
--- a/src/osal/osal.h
+++ b/src/osal/osal.h
@@ -76,28 +76,31 @@ typedef void (*osal_task_func_t)(void* param);
/*--------------------------------------------------------------------
OSAL Porting API
Should be implemented as static inline function in osal_port.h header
- uint32_t osal_time_millis(void);
+ uint32_t osal_time_millis(void);
- void osal_spin_init(osal_spinlock_t *ctx);
- void osal_spin_lock(osal_spinlock_t *ctx, bool in_isr)
- void osal_spin_unlock(osal_spinlock_t *ctx, bool in_isr);
+ void osal_task_delay(uint32_t msec);
+ osal_task_handle_t osal_task_get_current_handle(void);
- osal_semaphore_t osal_semaphore_create(osal_semaphore_def_t* semdef);
- bool osal_semaphore_delete(osal_semaphore_t semd_hdl);
- bool osal_semaphore_post(osal_semaphore_t sem_hdl, bool in_isr);
- bool osal_semaphore_wait(osal_semaphore_t sem_hdl, uint32_t msec);
- void osal_semaphore_reset(osal_semaphore_t sem_hdl);
+ void osal_spin_init(osal_spinlock_t *ctx);
+ void osal_spin_lock(osal_spinlock_t *ctx, bool in_isr)
+ void osal_spin_unlock(osal_spinlock_t *ctx, bool in_isr);
- osal_mutex_t osal_mutex_create(osal_mutex_def_t* mdef);
- bool osal_mutex_delete(osal_mutex_t mutex_hdl)
- bool osal_mutex_lock (osal_mutex_t sem_hdl, uint32_t msec);
- bool osal_mutex_unlock(osal_mutex_t mutex_hdl);
+ osal_semaphore_t osal_semaphore_create(osal_semaphore_def_t* semdef);
+ bool osal_semaphore_delete(osal_semaphore_t semd_hdl);
+ bool osal_semaphore_post(osal_semaphore_t sem_hdl, bool in_isr);
+ bool osal_semaphore_wait(osal_semaphore_t sem_hdl, uint32_t msec);
+ void osal_semaphore_reset(osal_semaphore_t sem_hdl);
- osal_queue_t osal_queue_create(osal_queue_def_t* qdef);
- bool osal_queue_delete(osal_queue_t qhdl);
- bool osal_queue_receive(osal_queue_t qhdl, void* data, uint32_t msec);
- bool osal_queue_send(osal_queue_t qhdl, void const * data, bool in_isr);
- bool osal_queue_empty(osal_queue_t qhdl);
+ osal_mutex_t osal_mutex_create(osal_mutex_def_t* mdef);
+ bool osal_mutex_delete(osal_mutex_t mutex_hdl)
+ bool osal_mutex_lock (osal_mutex_t sem_hdl, uint32_t msec);
+ bool osal_mutex_unlock(osal_mutex_t mutex_hdl);
+
+ osal_queue_t osal_queue_create(osal_queue_def_t* qdef);
+ bool osal_queue_delete(osal_queue_t qhdl);
+ bool osal_queue_receive(osal_queue_t qhdl, void* data, uint32_t msec);
+ bool osal_queue_send(osal_queue_t qhdl, void const * data, bool in_isr);
+ bool osal_queue_empty(osal_queue_t qhdl);
--------------------------------------------------------------------------*/
diff --git a/src/osal/osal_freertos.h b/src/osal/osal_freertos.h
index 898edd4ed..2f36aa9e8 100644
--- a/src/osal/osal_freertos.h
+++ b/src/osal/osal_freertos.h
@@ -83,6 +83,19 @@ typedef struct {
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef TaskHandle_t osal_task_handle_t;
+
+// Requires INCLUDE_xTaskGetCurrentTaskHandle == 1 in FreeRTOSConfig.h. FreeRTOS
+// also exposes the symbol when configUSE_MUTEXES == 1, so accept either.
+#if !defined(INCLUDE_xTaskGetCurrentTaskHandle) || (INCLUDE_xTaskGetCurrentTaskHandle == 0)
+ #if !defined(configUSE_MUTEXES) || (configUSE_MUTEXES == 0)
+ #error "TinyUSB host stack requires INCLUDE_xTaskGetCurrentTaskHandle or configUSE_MUTEXES to be enabled in FreeRTOSConfig.h"
+ #endif
+#endif
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return xTaskGetCurrentTaskHandle();
+}
+
TU_ATTR_ALWAYS_INLINE static inline uint32_t _osal_ms2tick(uint32_t msec) {
if (msec == OSAL_TIMEOUT_WAIT_FOREVER) { return portMAX_DELAY; }
if (msec == 0) { return 0; }
diff --git a/src/osal/osal_mynewt.h b/src/osal/osal_mynewt.h
index 335d53491..d1fa77ecb 100644
--- a/src/osal/osal_mynewt.h
+++ b/src/osal/osal_mynewt.h
@@ -36,6 +36,12 @@
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef struct os_task* osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return os_sched_get_current_task();
+}
+
TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
os_time_delay( os_time_ms_to_ticks32(msec) );
}
diff --git a/src/osal/osal_none.h b/src/osal/osal_none.h
index 7bf6029d6..e174d3518 100644
--- a/src/osal/osal_none.h
+++ b/src/osal/osal_none.h
@@ -34,6 +34,22 @@ extern "C" {
// osal_time_millis() is not provided, tusb_time_millis_api() must be implemented by user application
//--------------------------------------------------------------------+
+// TASK API
+//--------------------------------------------------------------------+
+// Bare-metal single context: return a non-NULL sentinel so equality compares true.
+typedef void* osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return (osal_task_handle_t) 1;
+}
+
+// Bare-metal has no scheduler to yield to; this is dead code in practice because
+// callers gate it on running outside the host task, which can't happen here.
+TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
+ (void) msec;
+}
+
+//--------------------------------------------------------------------+
// Spinlock API
//--------------------------------------------------------------------+
// Note: This implementation is designed for bare-metal single-core systems without RTOS.
diff --git a/src/osal/osal_pico.h b/src/osal/osal_pico.h
index 6a0a21bb3..364c38b01 100644
--- a/src/osal/osal_pico.h
+++ b/src/osal/osal_pico.h
@@ -39,6 +39,13 @@ extern "C" {
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+// Bare-metal single context: return a non-NULL sentinel so equality compares true.
+typedef void* osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return (osal_task_handle_t) 1;
+}
+
TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
sleep_ms(msec);
}
diff --git a/src/osal/osal_rtthread.h b/src/osal/osal_rtthread.h
index f560281c5..a151a7d70 100644
--- a/src/osal/osal_rtthread.h
+++ b/src/osal/osal_rtthread.h
@@ -38,6 +38,12 @@ extern "C" {
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef rt_thread_t osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return rt_thread_self();
+}
+
TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
rt_thread_mdelay(msec);
}
diff --git a/src/osal/osal_rtx4.h b/src/osal/osal_rtx4.h
index e1930c96c..e5b708a2c 100644
--- a/src/osal/osal_rtx4.h
+++ b/src/osal/osal_rtx4.h
@@ -37,6 +37,12 @@ extern "C" {
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef OS_TID osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return os_tsk_self();
+}
+
TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
uint16_t hi = msec >> 16;
uint16_t lo = msec;
diff --git a/src/osal/osal_threadx.h b/src/osal/osal_threadx.h
index 6bcf9c5ab..cca4eb487 100644
--- a/src/osal/osal_threadx.h
+++ b/src/osal/osal_threadx.h
@@ -37,6 +37,11 @@ extern "C" {
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef TX_THREAD* osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return tx_thread_identify();
+}
TU_ATTR_ALWAYS_INLINE static inline uint32_t _osal_ms2tick(uint32_t msec) {
if ( msec == TX_WAIT_FOREVER ) {
diff --git a/src/osal/osal_zephyr.h b/src/osal/osal_zephyr.h
index 900ac786c..6ea45131e 100644
--- a/src/osal/osal_zephyr.h
+++ b/src/osal/osal_zephyr.h
@@ -31,6 +31,12 @@
//--------------------------------------------------------------------+
// TASK API
//--------------------------------------------------------------------+
+typedef k_tid_t osal_task_handle_t;
+
+TU_ATTR_ALWAYS_INLINE static inline osal_task_handle_t osal_task_get_current_handle(void) {
+ return k_current_get();
+}
+
TU_ATTR_ALWAYS_INLINE static inline void osal_task_delay(uint32_t msec) {
k_msleep(msec);
}
diff --git a/src/tusb.c b/src/tusb.c
index 5d656fb8c..634cbc10b 100644
--- a/src/tusb.c
+++ b/src/tusb.c
@@ -497,7 +497,7 @@ char const* const tu_str_std_request[] = {
};
char const* const tu_str_xfer_result[] = {
- "OK", "FAILED", "STALLED", "TIMEOUT"
+ "OK", "FAILED", "STALLED", "TIMEOUT", "ABORTED", "INVALID"
};
#endif
diff --git a/src/tusb_option.h b/src/tusb_option.h
index 74eb8cc06..dcf0646cf 100644
--- a/src/tusb_option.h
+++ b/src/tusb_option.h
@@ -535,6 +535,18 @@
#define CFG_TUSB_OS OPT_OS_NONE
#endif
+// 1 when CFG_TUSB_OS provides a preemptive scheduler with distinct tasks
+// (FreeRTOS, Zephyr, ThreadX, etc.); 0 when the application is single-context
+// (bare-metal OS_NONE or Pico SDK). Sync host control xfers from the host
+// task are forbidden when this is 1.
+#ifndef CFG_TUSB_OS_HAS_SCHEDULER
+ #if CFG_TUSB_OS == OPT_OS_NONE || CFG_TUSB_OS == OPT_OS_PICO
+ #define CFG_TUSB_OS_HAS_SCHEDULER 0
+ #else
+ #define CFG_TUSB_OS_HAS_SCHEDULER 1
+ #endif
+#endif
+
#ifndef CFG_TUSB_OS_INC_PATH
#ifndef CFG_TUSB_OS_INC_PATH_DEFAULT
#define CFG_TUSB_OS_INC_PATH_DEFAULT
diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh
index 96872e2e1..4c7ba2936 100644
--- a/test/hil/hil_ci.sh
+++ b/test/hil/hil_ci.sh
@@ -54,11 +54,14 @@ scp -q "$ROOT_DIR/test/hil/hil_test.py" \
"$CONFIG" \
"$REMOTE:$REMOTE_DIR/test/hil/"
-# Copy only firmware binaries (elf/bin/hex), preserving directory structure
+# Copy only firmware binaries (elf/bin/hex) plus esptool metadata
+# (config.env + flash_args needed by the esptool flasher), preserving structure
copy_board_binaries() {
local src="$1"
rsync -a --prune-empty-dirs \
- --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \
+ --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' \
+ --include='config.env' --include='flash_args' \
+ --exclude='*' \
"$src" "$REMOTE:$REMOTE_DIR/examples/"
}
@@ -73,8 +76,11 @@ if [ -n "$BOARD" ]; then
copy_board_binaries "$BUILD_DIR"
else
echo "==> Copying all built binaries"
+ # Use `%/` parameter expansion to strip the trailing slash from the glob —
+ # rsync needs the bare dir name so the per-board cmake-build-<BOARD>/ subdir
+ # is preserved on the remote (hil_test.py looks up binaries by that path).
for dir in "$ROOT_DIR"/examples/cmake-build-*/; do
- [ -d "$dir" ] && copy_board_binaries "$dir"
+ [ -d "$dir" ] && copy_board_binaries "${dir%/}"
done
fi
@@ -85,5 +91,8 @@ echo "==> Running HIL test on $REMOTE"
ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE'
cd -- "$1"
shift
+# esptool/idf tools live in ~/.local/bin on ci.lan; the non-interactive shell
+# subprocess used for flashing doesn't pick that up otherwise.
+export PATH="$HOME/.local/bin:$PATH"
exec python3 -u test/hil/hil_test.py -B examples "$@"
REMOTE
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index ed9ebbf1a..b0b3fc17e 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -22,6 +22,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+# Host setup:
+# - System packages: sudo apt install mtools libmtp9 alsa-utils iperf
+# mtools - read_disk_file (device/cdc_msc, device/msc_dual_lun)
+# libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64
+# alsa-utils - arecord (device/audio_test_freertos)
+# iperf - throughput tests (device/net_lwip_*)
+# - Python packages: pip install -r requirements.txt
+#
# udev rules :
# ACTION=="add", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", SYMLINK+="ttyUSB_%c.%s{bInterfaceNumber}"
# ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", ENV{ID_FS_USAGE}=="filesystem", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode /media/blkUSB_%c.%s{bInterfaceNumber}"
@@ -34,17 +42,11 @@ import re
import select
import sys
import time
-import warnings
import signal
from contextlib import redirect_stdout
from pathlib import Path
from typing import Any, TypedDict, NotRequired, cast
-# Suppress pkg_resources deprecation warning from fs module
-warnings.filterwarnings("ignore", message="pkg_resources is deprecated")
-# Suppress pyfatfs unclean unmount warning
-warnings.filterwarnings("ignore", message="Filesystem was not cleanly unmounted")
-
import serial
import subprocess
import json
@@ -52,7 +54,6 @@ import glob
import shutil
from multiprocessing import Pool, Lock
from multiprocessing import TimeoutError as MpTimeoutError
-import fs
import hashlib
import ctypes
from pymtp import MTP
@@ -232,23 +233,24 @@ def open_serial_dev(port: str):
def read_disk_file(uid: str, lun: int, fname: str) -> bytes:
- # open_fs("fat://{dev}) require 'pip install pyfatfs'
+ # 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):
- fat = fs.open_fs(f'fat://{dev}?read_only=true')
try:
- with fat.open(fname, 'rb') as f:
- data = f.read()
- finally:
- fat.close()
- assert data, f'Cannot read file {fname} from {dev}'
- return data
+ 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'Storage {dev} not existed')
+ raise AssertionError(f'mtype failed on {dev}: {last_err}' if last_err else f'Storage {dev} not existed')
def open_mtp_dev(uid):
@@ -796,6 +798,10 @@ def test_host_msc_file_explorer(board):
ser.close()
+def test_host_msc_file_explorer_freertos(board):
+ return test_host_msc_file_explorer(board)
+
+
# -------------------------------------------------------------
# Tests: device
# -------------------------------------------------------------
@@ -1402,7 +1408,7 @@ def test_device_audio_test_freertos(board):
def test_device_hid_generic_inout(board):
uid = board['uid']
- import hid
+ import hid # cython-hidapi (pip: hidapi, apt: python3-hid)
# Find HID device by UID (VID=0xCafe)
timeout = ENUM_TIMEOUT
@@ -1418,22 +1424,23 @@ def test_device_hid_generic_inout(board):
timeout -= 1
assert dev is not None, f'HID device not found for {uid}'
- h = hid.Device(vid=dev['vendor_id'], pid=dev['product_id'], serial=uid)
-
- # Echo test: send random data and verify echo
- for size in [8, 32, 63]:
- # Report ID (0) + payload, padded to 64 bytes
- payload = bytes([random.randint(1, 255) for _ in range(size)])
- report = bytes([0]) + payload + bytes(64 - size)
- h.write(report)
- echo = h.read(64, timeout=2000)
- assert echo is not None and len(echo) >= size, (
- f'HID echo timeout or short read ({size} bytes)')
- assert bytes(echo[:size]) == payload, (
- f'HID echo wrong data ({size} bytes):\n'
- f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}')
-
- h.close()
+ h = hid.device()
+ h.open(dev['vendor_id'], dev['product_id'], uid)
+ try:
+ # Echo test: send random data and verify echo
+ for size in [8, 32, 63]:
+ # Report ID (0) + payload, padded to 64 bytes
+ payload = bytes([random.randint(1, 255) for _ in range(size)])
+ report = bytes([0]) + payload + bytes(64 - size)
+ h.write(report)
+ echo = h.read(64, 2000)
+ assert echo and len(echo) >= size, (
+ f'HID echo timeout or short read ({size} bytes)')
+ assert bytes(echo[:size]) == payload, (
+ f'HID echo wrong data ({size} bytes):\n'
+ f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}')
+ finally:
+ h.close()
# -------------------------------------------------------------
@@ -1465,6 +1472,7 @@ dual_tests = [
host_test = [
'host/cdc_msc_hid',
'host/msc_file_explorer',
+ 'host/msc_file_explorer_freertos',
'host/device_info',
]
@@ -1596,7 +1604,18 @@ def test_board(board: Board) -> tuple[str, int, list[str]]:
if name in board_test:
test_list = board_test[name]
elif len(test_only) > 0:
- test_list = test_only
+ # Explicit -t: filter against the board's capabilities so a device-only
+ # board doesn't try to run host/dual tests (the test functions need a
+ # `dev_attached` entry in the board config that won't exist).
+ board_tests = board.get('tests', {})
+ if 'only' in board_tests:
+ allowed = set(board_tests['only'])
+ test_list = [t for t in test_only if t in allowed]
+ else:
+ for t in test_only:
+ category = t.split('/', 1)[0]
+ if board_tests.get(category) is True:
+ test_list.append(t)
else:
if 'tests' in board:
board_tests = board['tests']
diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt
index ef2fecebe..ef1cf575b 100644
--- a/test/hil/requirements.txt
+++ b/test/hil/requirements.txt
@@ -1,4 +1,9 @@
-fs
-hid
-pyfatfs
+# System packages (install separately):
+# sudo apt install mtools libmtp9 alsa-utils iperf
+# mtools - read_disk_file (device/cdc_msc, device/msc_dual_lun)
+# libmtp9 - pymtp ctypes load (device/mtp); Debian 13 uses libmtp9t64
+# alsa-utils - arecord (device/audio_test_freertos)
+# iperf - throughput tests (device/net_lwip_*)
+hidapi
pyserial
+esptool
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index cba7677cf..dc28df7b9 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -3,12 +3,35 @@
{
"name": "espressif_p4_function_ev",
"uid": "6055F9F98715",
- "build" : {
- "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"]
+ "build": {
+ "flags_on": [
+ "",
+ "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"
+ ]
},
"tests": {
- "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"],
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}]
+ "only": [
+ "device/cdc_msc_freertos",
+ "device/hid_composite_freertos",
+ "device/audio_test_freertos",
+ "host/device_info",
+ "host/msc_file_explorer_freertos"
+ ],
+ "dev_attached": [
+ {
+ "vid_pid": "1a86_55d4",
+ "serial": "52D2002427",
+ "is_cdc": true
+ },
+ {
+ "vid_pid": "21c4_0cc7",
+ "serial": "900058944CB80A53",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 60620800,
+ "msc_inquiry": "Lexar USB Flash Drive PMAP"
+ }
+ ]
},
"flasher": {
"name": "esptool",
@@ -21,12 +44,36 @@
{
"name": "espressif_s3_devkitm",
"uid": "84F703C084E4",
- "build" : {
- "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"]
+ "build": {
+ "flags_on": [
+ "",
+ "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"
+ ]
},
"tests": {
- "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"],
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}]
+ "only": [
+ "device/cdc_msc_freertos",
+ "device/hid_composite_freertos",
+ "device/audio_test_freertos",
+ "host/device_info",
+ "host/msc_file_explorer_freertos"
+ ],
+ "dev_attached": [
+ {
+ "vid_pid": "1a86_55d4",
+ "serial": "52D2005402",
+ "is_cdc": true
+ },
+ {
+ "vid_pid": "048d_04d2",
+ "serial": "\u0409",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 30720000,
+ "msc_inquiry": "General UDisk 5.00",
+ "comment": "General UDisk reports iSerialNumber=U+0409"
+ }
+ ]
},
"flasher": {
"name": "esptool",
@@ -39,7 +86,9 @@
"name": "feather_nrf52840_express",
"uid": "1F0479CD0F764471",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",
@@ -51,7 +100,9 @@
"name": "max32666fthr",
"uid": "0C81464124010B20FF0A08CC2C",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "openocd_adi",
@@ -71,7 +122,13 @@
"device": true,
"host": false,
"dual": true,
- "dev_attached": [{"vid_pid": "067b_2303", "serial": "0", "is_cdc": true}],
+ "dev_attached": [
+ {
+ "vid_pid": "067b_2303",
+ "serial": "0",
+ "is_cdc": true
+ }
+ ],
"comment": "pl23x"
},
"flasher": {
@@ -84,7 +141,9 @@
"name": "mimxrt1015_evk",
"uid": "DC28F865D2111D228D00B0543A70463C",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",
@@ -96,9 +155,25 @@
"name": "mimxrt1064_evk",
"uid": "BAE96FB95AFA6DBB8F00005002001200",
"tests": {
- "device": true, "host": true, "dual": true,
- "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001", "is_cdc": true}],
- "comment": "cp2102"
+ "device": true,
+ "host": true,
+ "dual": true,
+ "dev_attached": [
+ {
+ "vid_pid": "10c4_ea60",
+ "serial": "0001",
+ "is_cdc": true,
+ "comment": "cp2102"
+ },
+ {
+ "vid_pid": "21c4_0cc7",
+ "serial": "900058874D871F66",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 60620800,
+ "msc_inquiry": "Lexar USB Flash Drive PMAP"
+ }
+ ]
},
"flasher": {
"name": "jlink",
@@ -110,7 +185,9 @@
"name": "lpcxpresso11u37",
"uid": "17121919",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",
@@ -135,13 +212,32 @@
{
"name": "raspberry_pi_pico",
"uid": "E6614C311B764A37",
- "build" : {
- "flags_on": ["CFG_TUH_RPI_PIO_USB"]
+ "build": {
+ "flags_on": [
+ "CFG_TUH_RPI_PIO_USB"
+ ]
},
"tests": {
- "device": true, "host": true, "dual": true,
- "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0", "is_cdc": true}],
- "comment": "ch34x"
+ "device": true,
+ "host": true,
+ "dual": true,
+ "dev_attached": [
+ {
+ "vid_pid": "1a86_7523",
+ "serial": "0",
+ "is_cdc": true,
+ "comment": "ch34x"
+ },
+ {
+ "vid_pid": "048d_04d2",
+ "serial": "\u0409",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 30720000,
+ "msc_inquiry": "General UDisk 5.00",
+ "comment": "General UDisk reports iSerialNumber=U+0409"
+ }
+ ]
},
"flasher": {
"name": "openocd",
@@ -153,9 +249,16 @@
"name": "raspberry_pi_pico_w",
"uid": "E6614864D35DAE36",
"tests": {
- "device": false, "host": true, "dual": false,
+ "device": false,
+ "host": true,
+ "dual": false,
"dev_attached": [
{
+ "vid_pid": "1a86_55d4",
+ "serial": "52D2002694",
+ "is_cdc": true
+ },
+ {
"vid_pid": "2008_2018",
"serial": "O20070925A002746",
"is_msc": true,
@@ -176,7 +279,9 @@
"name": "raspberry_pi_pico2",
"uid": "560AE75E1C7152C9",
"tests": {
- "device": false, "host": true, "dual": false,
+ "device": false,
+ "host": true,
+ "dual": false,
"dev_attached": [
{
"vid_pid": "0951_1603",
@@ -184,7 +289,7 @@
"is_msc": true,
"block_size": 512,
"block_count": 3987456,
- "msc_inquiry": "Kingston DataTraveler 2.0 1.0"
+ "msc_inquiry": "Kingston DataTraveler 2.0 1.00"
}
]
},
@@ -202,14 +307,24 @@
"host": true,
"dual": true,
"dev_attached": [
- {"vid_pid": "0403_6001", "serial": "0", "is_cdc": true},
- {"vid_pid": "058f_6387", "serial": "A8BEE062633D", "is_msc": true,
- "block_size": 512, "block_count": 7639040, "msc_inquiry": "Generic Flash Disk 8.07"}
+ {
+ "vid_pid": "0403_6001",
+ "serial": "0",
+ "is_cdc": true
+ },
+ {
+ "vid_pid": "058f_6387",
+ "serial": "A8BEE062633D",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 7639040,
+ "msc_inquiry": "Generic Flash Disk 8.07"
+ }
]
},
"flasher": {
"name": "openocd",
- "uid": "E6614103E78E8324",
+ "uid": "E663AC91D3359B38",
"args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\""
}
},
@@ -217,7 +332,9 @@
"name": "stm32f072disco",
"uid": "3A001A001357364230353532",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",
@@ -229,12 +346,31 @@
{
"name": "stm32f723disco",
"uid": "460029001951373031313335",
- "build" : {
- "flags_on": ["", "CFG_TUH_DWC2_DMA_ENABLE"]
+ "build": {
+ "flags_on": [
+ "",
+ "CFG_TUH_DWC2_DMA_ENABLE"
+ ]
},
"tests": {
- "device": true, "host": true, "dual": false,
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414", "is_cdc": true}]
+ "device": true,
+ "host": true,
+ "dual": false,
+ "dev_attached": [
+ {
+ "vid_pid": "1a86_55d4",
+ "serial": "52D2003414",
+ "is_cdc": true
+ },
+ {
+ "vid_pid": "21c4_0cc7",
+ "serial": "90005893730A1A63",
+ "is_msc": true,
+ "block_size": 512,
+ "block_count": 60620800,
+ "msc_inquiry": "Lexar USB Flash Drive PMAP"
+ }
+ ]
},
"flasher": {
"name": "jlink",
@@ -246,11 +382,16 @@
{
"name": "stm32h743nucleo",
"uid": "110018000951383432343236",
- "build" : {
- "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE"]
+ "build": {
+ "flags_on": [
+ "",
+ "CFG_TUD_DWC2_DMA_ENABLE"
+ ]
},
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "openocd",
@@ -262,7 +403,9 @@
"name": "stm32g0b1nucleo",
"uid": "4D0038000450434E37343120",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "openocd",
@@ -276,11 +419,16 @@
{
"name": "stm32f769disco",
"uid": "21002F000F51363531383437",
- "build" : {
- "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE"]
+ "build": {
+ "flags_on": [
+ "",
+ "CFG_TUD_DWC2_DMA_ENABLE"
+ ]
},
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",
@@ -292,7 +440,9 @@
"name": "nanoch32v203",
"uid": "CDAB277B0FBC03E339E339E3",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "openocd_wch",
@@ -304,7 +454,9 @@
"name": "stm32f407disco",
"uid": "30001A000647313332353735",
"tests": {
- "device": true, "host": false, "dual": false
+ "device": true,
+ "host": false,
+ "dual": false
},
"flasher": {
"name": "jlink",