#373·deskhop

Keyboard cannot re-enumerate on the same port after a failed enumeration (stale TinyUSB retry counter)

Author: ErikPhilipsCreated Sep 20, 2026Updated Sep 20, 2026

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:

c
// 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:

diff
     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.