diff options
| author | hathach <[email protected]> | 2026-08-28 00:36:38 +0700 |
|---|---|---|
| committer | hathach <[email protected]> | 2026-08-28 00:36:38 +0700 |
| commit | f748c01c4fa1104168a0f3317c37e6586d0027f7 (patch) | |
| tree | 90ddab4dcdc8917c90664ba06e55d79e61b0c71a /src | |
| parent | 42db982e7c6e8ee7d2f80d15a93a7092d3b708c9 (diff) | |
usbh: add enumeration control transfer timeoutclaude/usbh-enum-timeout
A device that ACKs SETUP but never completes the following stage (e.g. NAKs
the EP0 IN data stage forever) froze the control state machine: enumeration
never finishes and hub interrupt polling never resumes, so one dead device
blocks all further enumeration on the bus. Observed with a degraded USB disk
behind a hub on a rp2040 PIO-USB host: wire capture shows SETUP ACKed then
an endless IN/NAK storm with no completion event.
Arm a watchdog (CFG_TUH_ENUM_TIMEOUT_MS, default 5s to match Linux's
USB_CTRL_GET_TIMEOUT, 0 disables) on enumeration-owned control transfers
using the existing call_after slot, which already wakes a blocked host task.
On expiry abort the transfer at hcd level and complete it as FAILED so the
normal enumeration failure path frees dev0 and resumes hub polling.
Non-enumeration control transfers keep their current behavior.
Diffstat (limited to 'src')
| -rw-r--r-- | src/host/usbh.c | 52 | ||||
| -rw-r--r-- | src/tusb_option.h | 7 |
2 files changed, 58 insertions, 1 deletions
diff --git a/src/host/usbh.c b/src/host/usbh.c index 44819b016..dd85b1759 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -336,6 +336,10 @@ TU_ATTR_ALWAYS_INLINE static inline usbh_class_driver_t const *get_driver(uint8_ //--------------------------------------------------------------------+ static void enum_new_device(hcd_event_t* event); static void enum_delay_async(uintptr_t state); +static void process_enumeration(tuh_xfer_t *xfer); +#if CFG_TUH_ENUM_TIMEOUT_MS +static void enum_xfer_timeout_async(uintptr_t user_data); +#endif 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); @@ -870,6 +874,20 @@ static void control_xfer_sync_complete(tuh_xfer_t* xfer) { s->result = xfer->result; } +// Arm a watchdog on the in-flight control transfer if it belongs to enumeration: submitted by the +// enum state machine (incl. those to the parent hub) or addressed to the device being enumerated +// (class driver config). Without it a broken device that never completes a stage wedges enumeration +// and hub polling forever. Non-enumeration transfers are not armed: application owns their pacing. +TU_ATTR_ALWAYS_INLINE static inline void enum_xfer_timeout_arm(void) { +#if CFG_TUH_ENUM_TIMEOUT_MS + const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + if ((ctrl_info->complete_cb == process_enumeration || ctrl_info->daddr == _usbh_data.enumerating_daddr) && + _usbh_data.call_after.func == NULL) { + usbh_defer_func_ms_async(CFG_TUH_ENUM_TIMEOUT_MS, enum_xfer_timeout_async, ctrl_info->daddr); + } +#endif +} + // TODO timeout_ms is not supported yet bool tuh_control_xfer (tuh_xfer_t* xfer) { const uint8_t daddr = xfer->daddr; @@ -960,6 +978,7 @@ bool tuh_control_xfer (tuh_xfer_t* xfer) { control_xfer_set_stage(CONTROL_STAGE_IDLE); return false; } + enum_xfer_timeout_arm(); if (!is_nonblocking) { // No tuh_connected() escape needed: usbh_device_close() routes through @@ -1019,6 +1038,7 @@ static void control_xfer_dispatch_pending(void) { 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)) { + enum_xfer_timeout_arm(); return; // transfer kicked-off, we are done } } @@ -1032,6 +1052,13 @@ 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; +#if CFG_TUH_ENUM_TIMEOUT_MS + // disarm enumeration watchdog before callback, which may schedule a delay function + if (_usbh_data.call_after.func == enum_xfer_timeout_async) { + _usbh_data.call_after.func = NULL; + } +#endif + // duplicate xfer since user can execute control transfer within callback tusb_control_request_t const request = _usbh_epbuf.request; tuh_xfer_t xfer_temp = { @@ -1696,7 +1723,6 @@ enum { static uint8_t enum_get_new_address(bool is_hub); static bool enum_parse_configuration_desc(uint8_t dev_addr, const tusb_desc_configuration_t *desc_cfg); static void enum_full_complete(bool success); -static void process_enumeration(tuh_xfer_t *xfer); enum { ENUM_AFTER_DEBOUNCING_DELAY, @@ -2198,6 +2224,30 @@ void usbh_driver_set_config_complete(uint8_t dev_addr, uint8_t itf_num) { } } +#if CFG_TUH_ENUM_TIMEOUT_MS +// Enumeration watchdog expired: the device ACKed SETUP but never completed a later stage +// (e.g NAK forever). Abort at hcd level and complete as FAILED (not ABORTED, which +// process_enumeration does not treat as error) to fail this enumeration and resume hub polling. +static void enum_xfer_timeout_async(uintptr_t user_data) { + const uint8_t daddr = (uint8_t) user_data; + const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info; + TU_VERIFY(ctrl_info->stage != CONTROL_STAGE_IDLE && ctrl_info->daddr == daddr, ); + + const tusb_control_request_t* request = &_usbh_epbuf.request; + uint8_t ep_addr = 0; // SETUP stage is OUT + if (ctrl_info->stage == CONTROL_STAGE_DATA) { + ep_addr = tu_edpt_addr(0, request->bmRequestType_bit.direction); + } else if (ctrl_info->stage == CONTROL_STAGE_ACK) { + ep_addr = tu_edpt_addr(0, 1 - request->bmRequestType_bit.direction); + } + + const uint8_t rhport = usbh_get_rhport(daddr); + TU_LOG_USBH("[%u:%u] Control transfer timed out after %u ms\r\n", rhport, daddr, (unsigned) CFG_TUH_ENUM_TIMEOUT_MS); + hcd_edpt_abort_xfer(rhport, daddr, ep_addr); + control_xfer_complete(daddr, XFER_RESULT_FAILED); +} +#endif + static void enum_full_complete(bool success) { (void)success; TU_LOG_USBH("Enumeration complete: success = %u\r\n", success); diff --git a/src/tusb_option.h b/src/tusb_option.h index 1eb23fb00..8b3a41a75 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -753,6 +753,13 @@ #define CFG_TUH_ENUMERATION_BUFSIZE 256 #endif + // Timeout in ms for each enumeration control transfer, 0 to wait forever. + // A broken device that ACKs SETUP but never completes the following stage would otherwise + // stall enumeration and hub polling forever. 5s matches Linux's USB_CTRL_GET_TIMEOUT. + #ifndef CFG_TUH_ENUM_TIMEOUT_MS + #define CFG_TUH_ENUM_TIMEOUT_MS 5000 + #endif + #endif // CFG_TUH_ENABLED // Attribute to place data in accessible RAM for host controller (default: CFG_TUSB_MEM_SECTION) |
