Skip to content

[BUG] rp2040/rp23xx: USB device fails to enumerate on cold plug — usbdev_register() clobbers SIE_CTRL.PULLUP_EN set by CLASS_BIND #19434

Description

@ricardgb

Description

On RP2040 and RP2350, a USB device (tested: composite CDC-ACM + CDC-NCM + USBMSC) frequently fails to enumerate when the board is cold-plugged (physical unplug → replug). The host sees nothing — no enumeration attempt in the host kernel log at all — while NuttX itself boots and runs normally underneath. Warm resets (reflash-reboot, reboot from NSH) enumerate reliably, which makes the failure look like an intermittent early-boot hang and sends debugging in the wrong direction (we chased flash/littlefs, WiFi cold-init, and image size for days).

Root cause is in usbdev_register() in both arch/arm/src/rp2040/rp2040_usbdev.c and arch/arm/src/rp23xx/rp23xx_usbdev.c:

  1. CLASS_BIND(driver, ...) is called first. For composite (and cdcacm), the bind ends with DEV_CONNECT(dev) — i.e. ..._pullup(dev, true), which sets SIE_CTRL.PULLUP_EN.
  2. A few lines later, usbdev_register() does a wholesale write to the same register:
  /* Enable interrupt */

  putreg32(RP2040_USBCTRL_REGS_SIE_CTRL_EP0_INT_1BUF,
           RP2040_USBCTRL_REGS_SIE_CTRL);

(rp2040_usbdev.c:2117, rp23xx_usbdev.c:2108 on current master)

This clobbers PULLUP_EN microseconds after the class asserted it.

Enumeration still usually works, but only by accident: if the host port latched the microsecond pull-up blip, it issues a bus reset; the BUS_RESET handler calls CLASS_DISCONNECT, composite_disconnect() ends with DEV_CONNECT, and that pull-up sticks (nothing clobbers it any more). A warm, already-active host port catches the blip. A cold-plugged port is still in attach/debounce and misses it — no bus reset ever arrives, PULLUP_EN stays 0 forever, and the device is invisible while the OS runs fine.

Evidence (RP2350, Raspberry Pi Pico 2 W, silicon)

Instrumented rp23xx_pullup(), the BUS_RESET handler, and the SIE_CTRL write with console breadcrumbs (console on UART0, captured through a second Pico). A warm boot shows the accidental rescue in action:

[PULLUP=1]        <- composite_bind ends with DEV_CONNECT
[REG:SIE_STRIP]   <- usbdev_register putreg32 clobbers PULLUP_EN
[BUSRESET]        <- host caught the microsecond blip, resets the port
[PULLUP=0]        <- busreset handler
[PULLUP=1]        <- composite_disconnect -> DEV_CONNECT re-arms; sticks
...enumerates normally

A cold-plugged failing boot was inspected live over the serial console: NSH running, all USB class drivers registered (/dev/ttyACM0 present, network app had configured the CDC-NCM interface), and:

  • SIE_CTRL = 0x20000000 (EP0_INT_1BUF only — PULLUP_EN = 0)
  • SIE_STATUS = 0x00000001 (VBUS detected, line state SE0)
  • INTR = 0 (quiescent — no bus reset ever arrived)

i.e. software believes it is connected; the pull-up was never (re-)asserted.

Reproduction statistics on one board, one port, one cable, same day (10 cold-plug cycles each, success = host logs an enumeration):

image cold-plug enumerations
1.5 MB image (unmodified driver) 0 / 10
1.0 MB image (unmodified driver) 10 / 10
the same 1.5 MB image, driver fix below 10 / 10 (8/8 within the logged dmesg window)

The image-size dependence is only boot-timing: it deterministically shifts when the pull-up blip happens relative to the host port's attach-debounce window, which is why this bug can masquerade as almost anything.

Suggested fix

Preserve the pull-up state — set only the intended bit instead of overwriting the register:

  setbits_reg32(RP2040_USBCTRL_REGS_SIE_CTRL_EP0_INT_1BUF,
                RP2040_USBCTRL_REGS_SIE_CTRL);

(and the same in rp23xx_usbdev.c). Validated on silicon (RP2350): the 0/10 image above went to 10/10 cold-plug enumerations with only this change; instrumentation confirms the pull-up now survives registration and enumeration no longer depends on the bus-reset rescue.

A more thorough variant would be to bring the controller fully up (PHY_ISO clear on RP2350, CONTROLLER_EN, SIE_CTRL, INTE) before CLASS_BIND, so the bind-time DEV_CONNECT lands on a live controller — at bind time the controller is currently still disabled (and the PHY still isolated on RP2350). The one-line setbits change is the minimal fix and is what was tested.

Note this may also explain historical intermittent USB flakiness on these ports (e.g. devices that "sometimes don't appear" after plugging, RNDIS/NCM bring-up hangs), since every successful enumeration currently depends on the host catching a microsecond pull-up glitch.

Environment

  • NuttX master (bug present at rp2040_usbdev.c:2117 / rp23xx_usbdev.c:2108 as of 2026-07-14); observed on a recent-master board branch
  • Board: Raspberry Pi Pico 2 W (RP2350), composite USBDEV (CDC-ACM console + CDC-NCM + USBMSC), bus-powered
  • Host: Linux 6.17 xhci

Disclosure

This investigation, root-cause analysis, and the tested fix were performed by an AI agent (Claude Code), operated and directed by the submitter, and the results were reviewed by the submitter before posting. Happy to provide the full breadcrumb traces or test any proposed alternative fix on hardware.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions