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:
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.
- 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.
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,
rebootfrom 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 botharch/arm/src/rp2040/rp2040_usbdev.candarch/arm/src/rp23xx/rp23xx_usbdev.c:CLASS_BIND(driver, ...)is called first. For composite (and cdcacm), the bind ends withDEV_CONNECT(dev)— i.e...._pullup(dev, true), which setsSIE_CTRL.PULLUP_EN.usbdev_register()does a wholesale write to the same register:(rp2040_usbdev.c:2117, rp23xx_usbdev.c:2108 on current master)
This clobbers
PULLUP_ENmicroseconds 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_RESEThandler callsCLASS_DISCONNECT,composite_disconnect()ends withDEV_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_ENstays 0 forever, and the device is invisible while the OS runs fine.Evidence (RP2350, Raspberry Pi Pico 2 W, silicon)
Instrumented
rp23xx_pullup(), theBUS_RESEThandler, and theSIE_CTRLwrite with console breadcrumbs (console on UART0, captured through a second Pico). A warm boot shows the accidental rescue in action:A cold-plugged failing boot was inspected live over the serial console: NSH running, all USB class drivers registered (
/dev/ttyACM0present, network app had configured the CDC-NCM interface), and:SIE_CTRL = 0x20000000(EP0_INT_1BUFonly —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):
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:
(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_ISOclear on RP2350,CONTROLLER_EN,SIE_CTRL,INTE) beforeCLASS_BIND, so the bind-timeDEV_CONNECTlands on a live controller — at bind time the controller is currently still disabled (and the PHY still isolated on RP2350). The one-linesetbitschange 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
rp2040_usbdev.c:2117/rp23xx_usbdev.c:2108as of 2026-07-14); observed on a recent-master board branchDisclosure
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.