Skip to content

usbh: 10 ms reset recovery + hcd fail-fast + no enumeration retry leaves devices behind a hub permanently absent #3876

Open

Description

@UKTailwind

Environment

Symptom

With three devices attached at power-up, only the first enumerates; the others are absent until physically replugged. No panic, no assert, host stack idle and healthy afterwards.

A timing-neutral in-RAM event log of the hcd (transfer starts, buffer completions, interrupts — added because any CFG_TUSB_DEBUG logging masks the fault completely by changing timing) shows the mechanism:

  1. the hub finishes the second device's PORT_RESET; ENUM_RESET_RECOVERY_DELAY_MS = 10 later, the first SETUP goes to address 0;
  2. the device is not answering yet (10 ms is the USB 2.0 minimum reset recovery; these devices want more);
  3. the controller's automatic retries raise ERROR_RX_TIMEOUT every ~17 µs;
  4. the rp2 hcd stops and fails the transfer on the first timeout;
  5. usbh abandons the enumeration — and there is no retry path for a failed enumeration, so the device stays absent;
  6. the next device's enumeration then met it half-configured and drew a STALL on the 8-byte descriptor read — also abandoned.

Under 0.20 the same devices enumerate: its RX-timeout handler did nothing and the controller retried until the device answered. That also explains why every debug build "fixes" this — the logging supplies the recovery time by accident, which is worth knowing when triaging similar reports.

Suggestions

Any one of these would have avoided the failure:

  • make ENUM_RESET_RECOVERY_DELAY_MS configurable. We run 100 ms (build-time patch: https://github.com/UKTailwind/micropython/blob/pico-computer-3/ports/rp2/usbh.patch) and all three devices then enumerate on 8/8 boots including cold starts, at +90 ms per device;
  • and/or retry a failed address-0 GET_DESCRIPTOR with backoff (Linux does);
  • and/or tolerate RX timeouts in the rp2 hcd for a bounded window before failing. Caution from our attempts: naively not stopping a timed-out transfer on RP2350 and letting timeouts accumulate hung the controller — the stop-and-fail path needs to remain as the backstop.

Hardware is available for testing candidate fixes; the full investigation and measurements are in ports/rp2/boards/PICO_COMPUTER_3/DEVELOPMENT_NOTES.md (§73) on the branch above.

Activity

  1. hathach commented on Oct 5, 2026

    @hathach
    Owner

    #3862 (430fcd2) adds the missing enumeration retry: a device that fails an enumeration control transfer is torn down and re-enumerated from address 0 after a fresh port reset (CFG_TUH_ENUM_ATTEMPT_MAX, default 2), and a control transfer that never completes now times out after CFG_TUH_CONTROL_TIMEOUT_MS (5 s). The 10 ms reset recovery delay is unchanged.

    Could you test current master on your RP2350 + hub setup and report whether all devices mount?

  2. UKTailwind commented on Oct 7, 2026

    @UKTailwind
    Author

    Thanks. Tested master at e42fa93 (which includes #3862) on the same board: a Pico Computer 3 (RP2350B, native controller in host mode) with a keyboard, mouse, gamepad and USB flash drive behind its on-board hub, built into our PicoMite (MMBasic) firmware rather than MicroPython.

    The retry replaces all of our usbh.c workarounds, but on its own it is not enough: we still need a bounded RX-timeout hold-off in hcd_rp2040.c.

    Build Result
    master + our other RP2 driver fixes (including #3874 and #3875), stock usbh.c, no hold-off the gamepad and flash drive mounted; the keyboard and mouse did not, although the hub had powered all four ports
    the same + the hold-off below all four mounted every time: 5 software restarts, 4 reset-button resets, 4 power-ons and hot-plugging on one firmware variant, and 4 of each reset type on three more variants

    With the hold-off and master's retry, the three usbh.c changes we carried on 0.21 are no longer needed, and we have dropped them: the 100 ms reset recovery, disabling the hub port of an abandoned enumeration, and holding application control transfers back while a device enumerates.

    The hold-off restores 0.20's behaviour for endpoint 0 only, for a bounded time. For 1 s from the first EP0 RX_TIMEOUT, the timeout is cleared and the transfer is left armed, so the SIE keeps retrying. After that, the existing STOP_TRANS + XFER_RESULT_FAILED path runs as before; it has to stay as the backstop (see the caution in the original report). Only an endpoint-0 completion closes the window: closing it on any completion let hub status and HID traffic re-arm it for ever. Bulk and interrupt endpoints keep failing fast, since a slow device NAKs there rather than timing out.

    #define CTRL_RX_TIMEOUT_GRACE_US (1000u * 1000u)
    static bool     ctrl_rx_timeout_timing = false;
    static uint32_t ctrl_rx_timeout_start_us;
    
    // xfer_complete_isr(): an endpoint-0 completion closes the window
    if (tu_edpt_number(ep->ep_addr) == 0) ctrl_rx_timeout_timing = false;
    
    // hcd_rp2040_irq()
    if (status & USB_INTS_ERROR_RX_TIMEOUT_BITS) {
      usb_hw_clear->sie_status = USB_SIE_STATUS_RX_TIMEOUT_BITS;
    
      // within the window, leave the transfer armed and let the SIE retry (0.20 behaviour);
      // no STOP_TRANS, which would cancel the retry
      bool in_grace = false;
      if (epx->state == EPSTATE_ACTIVE && tu_edpt_number(epx->ep_addr) == 0) {
        const uint32_t now = time_us_32();
        if (!ctrl_rx_timeout_timing) {
          ctrl_rx_timeout_timing   = true;
          ctrl_rx_timeout_start_us = now;
        }
        in_grace = (uint32_t)(now - ctrl_rx_timeout_start_us) < CTRL_RX_TIMEOUT_GRACE_US;
      }
    
      if (!in_grace) {
        // existing STOP_TRANS + XFER_RESULT_FAILED path, unchanged
        // (with usbh_dpram->epx_buf_ctrl = 0 before failing, #3874)
      }
    }

    We haven't instrumented master to see why the retry alone misses the keyboard and mouse. A likely reason is that the retry's fresh port reset is followed by the same 10 ms recovery, so a device that is slow after a reset is just as mute on the second attempt. A longer recovery delay on the retry might do the same job as the hold-off; we haven't tried that.

    #3874 and #3875 are still needed on master; we carry fixes for both.

  3. UKTailwind commented on Oct 8, 2026

    @UKTailwind
    Author

    A follow-up to the hold-off above, now that #4166 (e204823) implements hcd_edpt_abort_xfer() and retires EPX in hcd_device_close(). Those end a control transfer without going through xfer_complete_isr(), so the window has to close there too. Otherwise a window opened by the aborted transfer stays open with its old start time, and the next endpoint-0 transfer, usually the enumeration retry after the teardown, finds it already expired and fails on its first timeout. In epx_retire(), inside the ep == epx && ep->state == EPSTATE_ACTIVE branch:

    if (tu_edpt_number(ep->ep_addr) == 0) ctrl_rx_timeout_timing = false; // an aborted/closed control transfer ends the window

    We have this on e204823 with the rest of our RP2 fixes rebased onto it. On the same Pico Computer 3 (four devices behind the hub) all four enumerate on every one of 5 CPU restarts, 4 reset-button resets and 4 power-ons, and flash-drive hot-plug is clean 5/5. We have not seen the abort path fire in those tests; the change follows from the code.

  4. HiFiPhile commented on Nov 12, 2026

    @HiFiPhile
    Collaborator

    @UKTailwind could you try #4484 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions