Skip to content

arch/rp2040,rp23xx: fix CDC-NCM bulk/notify endpoint handling#19378

Open
ricardgb wants to merge 4 commits into
apache:masterfrom
ricardgb:rp2040-usbdev-ncm-fixes
Open

arch/rp2040,rp23xx: fix CDC-NCM bulk/notify endpoint handling#19378
ricardgb wants to merge 4 commits into
apache:masterfrom
ricardgb:rp2040-usbdev-ncm-fixes

Conversation

@ricardgb

@ricardgb ricardgb commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes three endpoint-handling bugs in the RP2040/RP2350 USB device driver
(rp2040_usbdev.c, and its line-for-line copy rp23xx_usbdev.c). Together they
prevent CDC-NCM (and, for the second one, likely RNDIS) from working on these
controllers; with all three applied, a CONFIG_NET_CDCNCM gadget on a
Raspberry Pi Pico passes traffic in both directions with 0% packet loss.

The bugs were found by comparing the driver against the Pico SDK's TinyUSB
device controller driver (dcd_rp2040.c / rp2040_usb.c).

1. Bulk OUT read length not clamped to the endpoint max packet size.
rp2040_epread() armed the DPSRAM buffer-control register with the full usbdev
request length. The buffer-control LEN field is only 10 bits (max 1023), so a
class driver that posts a larger read request — cdcncm allocates a 16 KiB NTB read
buffer — overflows LEN (16384 & 0x3ff == 0) and corrupts the neighbouring
control bits. The controller then completes the transfer immediately with zero
bytes, forever, and no OUT data is ever received (cdcncm floods
Wrong NTH SIGN, skblen 0). The receive path already accumulates a request across
multiple packets, so the buffer only ever needs to be armed for one max-size
packet. This matches the transmit path, which already chunks to ep.maxpacket.
Bulk classes with small reads (cdcacm, usbmsc) were unaffected, which is why the
defect only surfaced on cdcncm.

2. An endpoint request resubmitted from its own completion callback is armed
twice.
rp2040_txcomplete() / rp2040_rxcomplete() unconditionally started the
next transfer after invoking a request's completion callback. When that callback
resubmits a request on the same, now-idle endpoint — which cdcncm and rndis do from
their notify/interrupt completion handlers — rp2040_epsubmit() already armed the
hardware buffer, and the unconditional re-arm toggles the DATA0/DATA1 PID a second
time. The host discards the packet as a stale retransmission, so e.g. the cdcncm
NETWORK_CONNECTION / SPEED_CHANGE notifications never arrive and the interface
stays NO-CARRIER. A per-request armed flag now guards the redundant re-arm.
This is also the likely cause of the long-standing rndis control-response -110
timeout on this controller.

3. Buffer AVAILABLE bit written together with length/PID. The RP2040 datasheet
§4.1.2.5.1 requires the AVAILABLE bit to be set after the rest of buffer control
and after a short settle, because buffer control crosses into the USB clock domain.
rp2040_update_buffer_control() wrote the whole word at once. Now it writes the
control word with AVAILABLE cleared, waits ~12 CPU cycles, then sets AVAILABLE
— the sequence the datasheet and the Pico SDK use.

Impact

  • CDC-NCM now works on RP2040/RP2350 (previously enumerated but never passed
    data). Likely also fixes RNDIS (bug 2).
  • No functional change for bulk classes that already worked (cdcacm, usbmsc) — their
    read lengths already fit in LEN and they don't resubmit from completion callbacks.
  • Applies to both rp2040_usbdev.c and rp23xx_usbdev.c (RP2350), which is a
    byte-identical copy of the affected functions.

Testing

Built and run on a Raspberry Pi Pico (RP2040), Linux host:

  • CDC-NCM, all three fixes: ping 20/20 host→board and 4/4 board→host, 0% loss,
    ARP resolves, board REACHABLE. Before the fixes the board received nothing
    (Wrong NTH SIGN, skblen 0 flood).
  • Bug 2 in isolation confirmed with usbmon: the cdcncm interrupt-IN
    SPEED_CHANGE notification now completes and reaches the host
    (C Ii:3:NN:1 0:4 16 = a12a...); previously it never did.
  • No regression on cdcacm / usbmsc.

RP2350: the affected functions in rp23xx_usbdev.c are byte-identical to their
RP2040 counterparts and the change compiles cleanly for raspberrypi-pico-2, but it
was not separately re-tested on RP2350 silicon.

Host-side note for reviewers reproducing this: NuttX gadgets ship with the
Linux-gadget VID/PID 0525:a4a2, which is also claimed by the host cdc_subset
driver; if cdc_subset binds the data interface first you'll see
cdc_ncm: failed to claim data intf. Force NCM with
echo 3-1:1.1 > /sys/bus/usb/drivers/cdc_subset/unbind; echo 3-1:1.0 > /sys/bus/usb/drivers/cdc_ncm/bind.


Disclosure: this change — the code, its validation, and this description — was
produced by an AI agent (Claude Code), operated and directed by me, and reviewed by
me before submission.

@linguini1 linguini1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide test logs to verify your patch.

@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium labels Jul 9, 2026
ricardgb and others added 4 commits July 9, 2026 15:36
rp2040_epread() armed the DPSRAM buffer-control register with the full
usbdev request length.  That length is only correct for requests no
larger than the buffer-control LEN field, which is 10 bits wide (max
1023 bytes).  Class drivers that post larger read requests -- e.g.
cdcncm allocates a 16 KiB NTB read buffer -- overflow LEN: 16384 & 0x3ff
is 0, and the high bits corrupt the neighbouring control flags.  The
controller then sees a zero-length available buffer and completes the
transfer immediately with zero bytes, over and over, so no OUT data is
ever received (cdcncm floods "Wrong NTH SIGN, skblen 0").

The receive path already accumulates a request across multiple packets:
rp2040_rxcomplete() copies each packet, advances xfrd and re-arms via
rp2040_rdrequest() until the request is satisfied or a short packet
arrives.  So the buffer only ever needs to be armed for a single
maximum-size packet.  Clamp nbytes accordingly.  This matches the
transmit path, which already chunks to ep.maxpacket in rp2040_wrrequest.

Bulk classes with small reads (cdcacm, usbmsc) were unaffected because
their request lengths already fit in LEN, which is why the defect only
showed up on cdcncm.

Validated on raspberrypi-pico (RP2040): a CONFIG_NET_CDCNCM device that
previously received nothing now passes traffic in both directions with
0% packet loss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHJRvWeBMTHwzpwjaUg4HW
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
…llback

rp2040_txcomplete() and rp2040_rxcomplete() unconditionally called
rp2040_wrrequest()/rp2040_rdrequest() to start the next transfer after
invoking a request's completion callback.  When that callback resubmits
a request on the same, now-idle endpoint -- which cdcncm and rndis do
from their interrupt/notify completion handlers -- rp2040_epsubmit()
already arms the hardware buffer for it.  The unconditional re-arm in the
completion path then arms the same buffer a second time, toggling the
DATA0/DATA1 PID twice.  The host sees a stale PID and silently discards
the packet as a retransmission, so e.g. the cdcncm NETWORK_CONNECTION /
SPEED_CHANGE notifications never reach the host and the interface stays
NO-CARRIER.

Track whether a request's hardware buffer has already been armed with a
per-request flag (set in rp2040_wrrequest/rp2040_rdrequest, cleared in
rp2040_epsubmit) and skip the redundant re-arm when the completion
callback has already resubmitted.  The in-progress multi-packet case
(transfer not yet complete) still continues normally.

Validated on raspberrypi-pico (RP2040): the cdcncm interrupt-IN
notification is now delivered (confirmed with usbmon) and the host
brings the link up; previously it never was.  This is also the likely
cause of the long-standing rndis control-response timeout on this
controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHJRvWeBMTHwzpwjaUg4HW
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
Per the RP2040 datasheet section 4.1.2.5.1, when handing a buffer to the
USB controller the AVAILABLE bit must be written after the rest of the
buffer-control register (length and data PID) and after a short delay,
because buffer control crosses from the system clock domain into the USB
clock domain.  Writing everything in a single store risks the controller
acting on a stale length or PID.  rp2040_update_buffer_control() wrote
the whole word, AVAILABLE included, in one access.

Follow the sequence the datasheet (and the Pico SDK) use: write the
control word with AVAILABLE cleared, wait ~12 CPU cycles, then set
AVAILABLE.  The delay covers system clocks up to 12x the 48 MHz USB
clock.

Validated on raspberrypi-pico (RP2040) as part of bringing up cdcncm;
no regression on cdcacm/usbmsc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHJRvWeBMTHwzpwjaUg4HW
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
rp23xx_usbdev.c is a line-for-line copy of the rp2040 USB device driver
and shares all three endpoint-handling defects fixed in the preceding
commits:

  - bulk OUT reads armed with the full request length, overflowing the
    10-bit buffer-control LEN field for large reads (e.g. cdcncm);
  - endpoint requests resubmitted from their own completion callback
    being armed twice, corrupting the data PID;
  - the buffer AVAILABLE bit written together with length/PID instead of
    afterwards.

Port the identical fixes to the RP2350 driver.  The affected functions
are byte-identical to their rp2040 counterparts, so the changes match
verbatim.  These were validated on RP2040 hardware; RP2350 shares the
same USB controller IP and driver, but was not re-tested on silicon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHJRvWeBMTHwzpwjaUg4HW
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@ricardgb ricardgb force-pushed the rp2040-usbdev-ncm-fixes branch from 18b2a57 to f982711 Compare July 9, 2026 19:47
@ricardgb

ricardgb commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for reviewing. Here are before/after logs.

Setup: raspberrypi-pico (RP2040), Linux host, standalone CONFIG_NET_CDCNCM
gadget. NuttX gadgets use the Linux-gadget VID/PID 0525:a4a2, which the host
cdc_subset driver also claims, so to exercise NCM I unbind it and bind cdc_ncm:
echo 3-1:1.1 > /sys/bus/usb/drivers/cdc_subset/unbind; echo 3-1:1.0 > /sys/bus/usb/drivers/cdc_ncm/bind.
Board is 192.168.7.1, host 192.168.7.2 on usb0, ifup eth0 on the board.

Before the patches

The host binds cdc_ncm and brings the link up, but no traffic passes. Every bulk
OUT read completes with zero bytes (the read buffer is armed with the full 16 KiB
request length, overflowing the 10-bit LEN field), so the board floods:

cdcncm_receive: Wrong NTH SIGN, skblen 0
cdcncm_receive: Wrong NTH SIGN, skblen 0
cdcncm_receive: Wrong NTH SIGN, skblen 0
...
$ ping -c 10 192.168.7.1
10 packets transmitted, 0 received, +10 errors, 100% packet loss

Separately, the interrupt-IN NETWORK_CONNECTION/SPEED_CHANGE notification is
armed twice (once by epsubmit from the notify callback, once by the unconditional
re-arm in txcomplete), toggling the data PID and getting dropped by the host, so
the interface never sees carrier from the notification path.

After the patches

The Wrong NTH SIGN, skblen 0 flood is gone, OUT data is received, and ping is
0% loss in both directions:

# host -> board
$ ping -c 20 -i 0.2 192.168.7.1
20 packets transmitted, 20 received, 0% packet loss
rtt min/avg/max/mdev = 12.852/18.594/37.781/5.248 ms
# board -> host (nsh)
nsh> ping -c 4 192.168.7.2
4 packets transmitted, 4 received, 0% packet loss

The interrupt-IN notification now completes and reaches the host — usbmon on the
bulk-notify endpoint (EP 0x81), 16-byte a1 (NETWORK_CONNECTION/SPEED_CHANGE)
completing with status 0:

C Ii:3:015:1 0:4 16 = a12a0000 00000800 00709400 00709400

cdc_ncm bound, carrier up after the fix:

usb0   UP   02:00:00:11:22:33 <BROADCAST,MULTICAST,UP,LOWER_UP>   driver: cdc_ncm

No regression on cdcacm / usbmsc (their reads already fit in LEN and they don't
resubmit from completion callbacks). The RP2350 (rp23xx_usbdev.c) changes are the
identical fix to byte-identical functions; they compile for raspberrypi-pico-2 but
were not separately re-tested on RP2350 silicon.


Disclosure: these logs, the patch, and its validation were produced by an AI agent
(Claude Code), operated and directed by me, and reviewed by me before posting.

ricardgb added a commit to ricardgb/nuttx that referenced this pull request Jul 11, 2026
Port the three RP2040 USB device fixes (PR apache#19378) to the RP2350
controller, without which CDC-NCM RX returns zero-length reads and the
data-PID toggle desynchronises under load:

  - Clamp the bulk-OUT read length to the endpoint maxpacket size before
    arming the hardware buffer.  The buffer-control LEN field is 10 bits;
    a larger request overflows it and the host sees zero-byte reads.
  - Track whether a request already armed the hardware buffer (new
    rp23xx_req_s.armed) so a request resubmitted from its own completion
    callback is not armed a second time.  A double arm toggles the data
    PID and the host drops the packet.
  - Set the AVAILABLE bit in a separate write after the LEN/PID word, per
    RP2040 datasheet section 4.1.2.5.1, instead of in the same store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ricardgb

Copy link
Copy Markdown
Contributor Author

The failing analyze (arduino-mega2560, ...) job hit "no space left on device" on the runner while setting up its container — an infra flake unrelated to this PR (which only touches rp2040_usbdev.c/rp23xx_usbdev.c). I don't have permission to re-run it; could a committer re-trigger the failed job when convenient? Thanks!

@xiaoxiang781216

Copy link
Copy Markdown
Contributor

The failing analyze (arduino-mega2560, ...) job hit "no space left on device" on the runner while setting up its container — an infra flake unrelated to this PR (which only touches rp2040_usbdev.c/rp23xx_usbdev.c). I don't have permission to re-run it; could a committer re-trigger the failed job when convenient? Thanks!

please rebase your patch to the latest master and push again.

mek-x pushed a commit to mekops-labs/nuttx that referenced this pull request Jul 12, 2026
Port the three RP2040 USB device fixes (PR apache#19378) to the RP2350
controller, without which CDC-NCM RX returns zero-length reads and the
data-PID toggle desynchronises under load:

  - Clamp the bulk-OUT read length to the endpoint maxpacket size before
    arming the hardware buffer.  The buffer-control LEN field is 10 bits;
    a larger request overflows it and the host sees zero-byte reads.
  - Track whether a request already armed the hardware buffer (new
    rp23xx_req_s.armed) so a request resubmitted from its own completion
    callback is not armed a second time.  A double arm toggles the data
    PID and the host drops the packet.
  - Set the AVAILABLE bit in a separate write after the LEN/PID word, per
    RP2040 datasheet section 4.1.2.5.1, instead of in the same store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 0ef4f53)
ricardgb added a commit to ricardgb/nuttx that referenced this pull request Jul 13, 2026
Port the three RP2040 USB device fixes (PR apache#19378) to the RP2350
controller, without which CDC-NCM RX returns zero-length reads and the
data-PID toggle desynchronises under load:

  - Clamp the bulk-OUT read length to the endpoint maxpacket size before
    arming the hardware buffer.  The buffer-control LEN field is 10 bits;
    a larger request overflows it and the host sees zero-byte reads.
  - Track whether a request already armed the hardware buffer (new
    rp23xx_req_s.armed) so a request resubmitted from its own completion
    callback is not armed a second time.  A double arm toggles the data
    PID and the host drops the packet.
  - Set the AVAILABLE bit in a separate write after the LEN/PID word, per
    RP2040 datasheet section 4.1.2.5.1, instead of in the same store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants