Skip to content

rp2040/rp23xx USB: missing DMB between BUFF_STATUS clear and AVAILABLE re-arm loses IN completions on RP2350 (Cortex-M33) #19472

Description

@ricardgb

Description

On RP2350 (Cortex-M33), the USB device driver (arch/arm/src/rp23xx/rp23xx_usbdev.c)
re-arms an endpoint by writing the buffer-control AVAILABLE bit without a memory
barrier ordering it after the preceding BUFF_STATUS clear
. These are two writes to two
different peripheral regions:

  • the BUFF_STATUS clear (done in rp23xx_usbintr_buffstat when handling a completion)
    targets the USB controller register block at 0x50110000 (BUFF_STATUS @ +0x58);
  • setting AVAILABLE in the endpoint buffer-control word targets USB DPSRAM at
    0x50100000
    .

On the Cortex-M33 bus fabric, writes to different Device regions may be observed out of
order unless separated by a DMB. In a non-SMP build (CONFIG_SMP unset)
spin_lock_irqsave compiles to a barrier-free up_irq_save, so nothing orders these two
stores.

The race (lost IN completion)

When re-arming the next IN buffer directly from a completion handler under back-to-back TX:

  1. AVAILABLE (DPSRAM) is observed by the controller before the BUFF_STATUS clear
    (register block) lands.
  2. The controller transmits the next IN packet and latches its completion by setting the
    BUFF_STATUS bit
    .
  3. The late-arriving clear then wipes that just-set bit.

The completion edge is lost: the IN endpoint never raises another buffer interrupt, the
class wrcomplete callback never runs, and (for CDC-NCM) wrreq_idle is never reposted, so
transmit wedges permanently. The controller-register note in the header even warns:
"clearing the buffer status bit may instantly re-set it on the next clock cycle."

Steps to reproduce

  • RP2350 (Raspberry Pi Pico 2 W), CDC-NCM composite acting as a USB-NIC.
  • Enable CONFIG_NET_TCP_WRITE_BUFFERS=y so TCP transmit is bursty (a single txavail
    poll drains many segments back-to-back), keeping the IN pipe continuously full.
  • Drive sustained/concurrent HTTP downloads.

Actual: transmit wedges intermittently (worse with denser TX). The core is idle
(up_idle), no fault, wrreq_idle never reposted. Over SWD in the wedged state:
BUFF_STATUS = 0, USB INTS = 0, the bulk-IN buffer-control word shows the buffer
prepared but AVAILABLE clear (e.g. 0x0000_2024 = PID + length, no AVAILABLE), i.e. a
lost completion edge — hardware quiescent while the driver waits forever.

Expected: sustained TX never wedges.

Why RP2040 is unaffected

The identical logic runs on RP2040 (Cortex-M0+), whose far stricter/simpler store ordering
does not exhibit this reorder — so this is RP2350/Cortex-M33-specific.

Proposed fix

Insert a UP_DMB() (and #include <arch/barriers.h>) immediately before setting the
AVAILABLE bit on the re-arm path, so every preceding controller-register write (the
BUFF_STATUS clear) is ordered ahead of the DPSRAM AVAILABLE store.

This was root-caused and fixed on an RP2350 (Pico 2 W) CDC-NCM USB-NIC: with the barrier,
a soak of 144 dense/concurrent HTTP downloads at ~486 KB/s ran with zero wedges (previously
transmit hung within a handful of requests). A patch is available; I can open a PR against
whichever revision of the RP2350 IN-endpoint re-arm path is preferred (the exact call site
depends on recent driver refactors — refs the USB endpoint work in #19378/#19450/#19451 and
the CDC-NCM TX fixes in #19469/#19470).

Environment

  • Chip: RP2350 (Cortex-M33), non-SMP.
  • Board: Raspberry Pi Pico 2 W.
  • NuttX: current master.

Disclosure: this root-cause analysis was performed with the assistance of an AI agent
(Anthropic's Claude, via Claude Code), operated and directed by the submitter, and validated
on-hardware before filing.

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