Keyboard cannot re-enumerate on the same port after a failed enumeration (stale TinyUSB retry counter)
Keyboard cannot re-enumerate on the same port after a failed enumeration (stale TinyUSB retry counter)
Symptom
After sitting idle, my keyboard (Perixx PERIBOARD-835, tri-mode board used over USB-C, auto-sleeps on its own timer) stopped working on board A. The mouse on board B kept working.
- Unplugging and replugging the keyboard into board A: still dead, every time.
- The same keyboard plugged directly into the PC: works.
- Plugging the mouse into board A: works immediately.
- Plugging the keyboard into board B: works immediately.
So the port isn't dead — it just refuses to bring up that device again, while a "cleaner" device enumerates fine.
Cause
The vendored TinyUSB (0.16.0, pico-sdk/lib/tinyusb/src/host/usbh.c) keeps the enumeration retry counter in a function-static that is only reset on a successful transfer:
// process device enumeration
static void process_enumeration(tuh_xfer_t* xfer) {
enum {
ATTEMPT_COUNT_MAX = 3,
ATTEMPT_DELAY_MS = 100
};
static uint8_t failed_count = 0; // <-- persists across enumerations
if (XFER_RESULT_SUCCESS != xfer->result) {
bool retry = _dev0.enumerating && (failed_count < ATTEMPT_COUNT_MAX);
if ( retry ) {
failed_count++;
osal_task_delay(ATTEMPT_DELAY_MS);
TU_LOG1("Enumeration attempt %u\r\n", failed_count);
retry = tuh_control_xfer(xfer);
}
if (!retry) {
enum_full_complete(); // <-- gives up, but failed_count stays at 3
}
return;
}
failed_count = 0; // <-- only reset here, on success
...Once a device fails three transfers in a row (e.g. a keyboard that is slow to answer the first GET_DESCRIPTOR after waking from its own sleep), enum_full_complete() abandons enumeration with failed_count == 3. From then on, every new enumeration on that host has zero retries: the first failed transfer aborts it immediately. A device whose first transfer always succeeds (a simple mouse) enumerates fine and incidentally resets the counter; a device that needs even one retry never comes back until the board is rebooted.
Upstream TinyUSB no longer has this bug — the counter was moved into the per-transfer usbh_ctrl_xfer_info_t and is reset to 0 in tuh_control_xfer() every time a new control transfer starts.
Fix
Reset the counter whenever an enumeration ends, not just on success. Minimal patch to pico-sdk/lib/tinyusb/src/host/usbh.c:
if (!retry) {
+ failed_count = 0; // next enumeration gets a fresh retry budget
enum_full_complete();
}
return;
}
failed_count = 0;Equivalent alternative: hoist failed_count to file scope and zero it at the top of enum_new_device().
Notes
- This is separate from (and complementary to) the PIO-USB transaction retry added in 0.79; that retries at the packet level inside one transfer, this is the transfer-level retry budget across enumerations.
- Any HID device that is flaky right after attach (wireless/BT boards waking from sleep, keyboards with slow controllers, anything that briefly browns out on VBUS) can trigger the initial 3-strike failure; after that, the port looks "dead" for that device until power cycle.
- Firmware: fork of current
main(e5f8ae8, 0.79-dev) — the vendored TinyUSB is unchanged from upstream deskhop.
Source: hrvach/deskhop