- Defect
handle_bus_resetarms EP0 IN and never takes it back, so the data stage's first packet leaves as DATA0 carrying 64 bytes where the standard requires DATA1 and the descriptor is 18.- Fix
- One line clearing the endpoint's buffer control at reset — Section 7.
- Status
- not tested, no pull request Read from source, the datasheet and the reporter's capture; there is no RP2040 board here. Nobody should merge it on this page's strength — it needs someone with a Pico, an analyser and a failing host.
1What was measured
On the wire, after a bus reset, the host's first
GET_DESCRIPTOR is answered twice:
- a DATA0 packet of 64 bytes, which the host ACKs and discards;
- then, on the retry, a DATA1 packet of 18 bytes — the correct device descriptor.
USB 2.0 §8.5.3 requires the first data packet of a control transfer's data stage to be DATA1, so a host is right to reject the first one. Some hosts tolerate the retry and enumerate anyway; others fail the address assignment outright, which is the Unknown USB Device (Set Address Failed) in the report. That difference is why this looks machine-dependent: a Windows laptop fails, a mini PC works, a Raspberry Pi 4 does it intermittently.
The two wrong values are worth reading together, because they have one
cause. 64 and DATA0 are not what any code path computed for this descriptor.
They are what handle_bus_reset
put in the register, some microseconds earlier.
2Who owns the buffer
The RP2040's endpoint buffers are in a dual-port RAM that both the CPU and
the USB controller can reach. The AVAILABLE bit is the handover:
the CPU sets it to give the controller the buffer, and the controller clears it
when it has used it. Step through what that bit does here.
EP0 IN's buffer control, from bus reset to the first IN token
1. Bus reset
handle_bus_reset writes
AVAILABLE0=1, TRANSFER_LENGTH0=64,
DATA_PID0=DATA0 into EP0 IN's buffer control.
Tock
The USB controller owns the buffer
- AVAILABLE0
- 1
- TRANSFER_LENGTH0
- 64
- DATA_PID0
- DATA0
If an IN token arrived now: 64 bytes of whatever is in the buffer, as DATA0. Nothing has been queued to send.
The reference driver
The CPU owns the buffer
- AVAILABLE0
- 0
- TRANSFER_LENGTH0
- —
- DATA_PID0
- —
If an IN token arrived now: the endpoint NAKs. Its reset handler does not touch any buffer control register.
2. SETUP arrives
The host sends SETUP GET_DESCRIPTOR. The
hardware clears EP0's stall bits and sets
SIE_STATUS.SETUP_REC. It does not touch
AVAILABLE.
Tock
The USB controller still owns the buffer
- AVAILABLE0
- 1, unchanged
- TRANSFER_LENGTH0
- 64
- DATA_PID0
- DATA0
Still 64 bytes as DATA0. This is the window the capture caught.
The reference driver
The CPU owns the buffer
- AVAILABLE0
- 0
- TRANSFER_LENGTH0
- —
- DATA_PID0
- —
Still NAKs. Nothing can go out until the driver has something to send.
3. Tock writes the descriptor
transmit_in_ep0 copies 18 descriptor bytes
into the buffer and modifies the length, BUFFER0_FULL and the
PID.
Tock
Both sides are touching the buffer
- AVAILABLE0
- 1, never cleared
- TRANSFER_LENGTH0
- 64 → 18
- DATA_PID0
- DATA0 → DATA1
A read-modify-write on a register the controller was handed at step 1. Whichever values the controller samples is a race, and the datasheet's ordering rule cannot help, because it assumes the buffer starts out owned by the CPU.
The reference driver
The CPU owns the buffer
- AVAILABLE0
- 0 → 1, last
- TRANSFER_LENGTH0
- 18
- DATA_PID0
- DATA1
Builds the whole 32-bit value first and stores it once, so there is no interval in which the controller can see a half-updated register.
4. The IN token
The host sends IN. The controller serves
whatever the buffer control said when it sampled it.
Tock
Either answer is possible
- if it sampled early
- 64 bytes, DATA0
- if it sampled late
- 18 bytes, DATA1
- the capture shows
- 64 bytes, DATA0
The host ACKs the wrong-toggle packet and retries; the retry gets the correct 18-byte DATA1. Whether the device then enumerates is up to the host.
The reference driver
One possible answer
- length
- min(18, wLength)
- PID
- DATA1
- retries
- none
The buffer only ever becomes available once it holds the bytes that were meant to go out.
chips/rp2040/src/usb.rs
at 2a7a40450; the reference column from
pico-examples,
the driver the RP2040 datasheet walks through in §4.1.3.2. The hardware
behaviour at step 2 is the datasheet's, §4.1.2.8.1.3What bus reset writes
Three fields, one of which gives the buffer away:
EP_BUFFER_CONTROL, and the three fields handle_bus_reset sets
- AVAILABLE0, bit 10
- 1 — hands the buffer to the controller
- DATA_PID0, bit 13
- 0 — DATA0
- TRANSFER_LENGTH0, bits 9:0
- 64
Setting AVAILABLE with nothing queued to send is the defect. The other two fields are only what the stale packet turns out to say.
register_bitfields!
in the same file. The sixteen bits of the second buffer are drawn collapsed
because nothing on this path uses them.The line has been there since the driver's first commit,
4d903e2c9
of 2022-11-02, and has never been modified. It is also inconsistent with the
rest of the same function. handle_bus_reset writes exactly one
other endpoint buffer — the bulk OUT one — and that write clears it
to zero before it sets AVAILABLE. No other buffer is written at
all: a control endpoint gets a state change and nothing else, and the bulk IN
side is left alone.
chips/rp2040/src/usb.rs:1535 — the bulk OUT path, earlier in the same function self.dpsram.ep_buf_ctrl[ep].ep_out_buf_ctrl.set(0); self.dpsram.ep_buf_ctrl[ep].ep_out_buf_ctrl.modify( EP_BUFFER_CONTROL::AVAILABLE0::SET + EP_BUFFER_CONTROL::TRANSFER_LENGTH0.val(64_u32), ); chips/rp2040/src/usb.rs:1552 — EP0 IN, with no clear self.dpsram.ep_buf_ctrl[0].ep_in_buf_ctrl.modify( EP_BUFFER_CONTROL::AVAILABLE0::SET + EP_BUFFER_CONTROL::TRANSFER_LENGTH0.val(64) + EP_BUFFER_CONTROL::DATA_PID0::CLEAR, );
4The analysis in the issue, checked
The report includes an AI-generated analysis. Two of its three claims hold up and the third does not, which matters because the third is the one that implies a patch.
confirmed The 64-byte DATA0 comes from the reset handler
The packet length of exactly 64 bytes matches a hard-coded value in Tock’s RP2040 bus-reset handler.
It does. usb.rs:1552-1556 writes
TRANSFER_LENGTH0 = 64 and DATA_PID0 = DATA0 into
EP0 IN, and sets AVAILABLE in the same call.
confirmed AVAILABLE was already set before the descriptor was written
But AVAILABLE was already set by handle_bus_reset(). The USB controller already owns that buffer and can read it concurrently while the CPU modifies the length, PID, and contents.
This is the defect. Nothing between the bus reset and
transmit_in_ep0 clears it — and per the datasheet, a
SETUP packet clears EP0's stall bits, not
AVAILABLE.
not as described That Tock skips the datasheet's wait before setting AVAILABLE
It says USB DPRAM is asynchronous and non-atomic, and software must: 1. write the buffer information; 2. wait enough system-clock cycles for a USB-clock cycle; 3. set AVAILABLE separately and last.
The requirement is real and quoted correctly, but Tock
already follows it. transmit_in_ep0 writes the fields, calls
nop_wait() — a hundred nops,
usb.rs:1395 — and only then sets AVAILABLE.
Adding the delay changes nothing, because the bit was already 1 on
entry. The sequence is correct and the precondition it assumes is not
met.
chips/rp2040/src/usb.rs:2096 — transmit_in_ep0, the ordering is already right
if self.next_pid_in[endpoint].get() == 1 {
self.dpsram.ep_buf_ctrl[endpoint].ep_in_buf_ctrl.modify(
EP_BUFFER_CONTROL::TRANSFER_LENGTH0.val(size as u32)
+ EP_BUFFER_CONTROL::BUFFER0_FULL::SET
+ EP_BUFFER_CONTROL::DATA_PID0::SET,
);
self.next_pid_in[endpoint].set(0);
} /* ... */
self.nop_wait();
self.dpsram.ep_buf_ctrl[endpoint]
.ep_in_buf_ctrl
.modify(EP_BUFFER_CONTROL::AVAILABLE0::SET);
The report also asks why the device sends 64 bytes where the descriptor is
18. Same cause: 64 is the stale TRANSFER_LENGTH0 from the reset,
not a length any descriptor path computed. The reference driver sends
MIN(sizeof(descriptor), wLength).
5What the datasheet requires
The AVAILABLE bit in the buffer control register is used to indicate who has ownership of a buffer. This bit should be set to 1 by the processor to give the controller ownership of the buffer. The controller will set the bit back to 0 when it has used the buffer.
RP2040 datasheet, §4.1.2.7.1, Concurrent access
The [USB specification] states that receiving a setup packet also clears any stall bits on EP0. For this reason, the stall bits for EP0 are gated with two bits in the EP_STALL_ARM register. These bits are cleared when a setup packet is received.
RP2040 datasheet, §4.1.2.8.1, SETUP
The second quote is the one that closes the argument. The section describing
what a SETUP packet does to the device controller lists the stall bits, the
setup buffer at DPSRAM offset 0, and SIE_STATUS.SETUP_REC. There
is no mention of buffer-control ownership, so the buffer armed at bus reset is
still armed when the request arrives.
6What the reference driver does instead
Worth stating as a measurement rather than an impression. In the whole of
dev_lowlevel.c, 582 lines:
- there is exactly one write to a buffer control register, at line
259, and it is a single 32-bit store of a value built from scratch:
*ep->buffer_control = val; USB_BUF_CTRL_AVAILappears exactly once, at line 246, where that value is assembled;usb_bus_reset, line 314, sets the device address to zero and clears two flags. It touches no endpoint buffer at all.
So the reference never has a buffer that is available without holding the data it is meant to send, and never read-modify-writes one. Tock does both, on EP0 IN, from reset until the first descriptor goes out.
7The change
The minimal fix is to stop giving the buffer away. There is nothing to send at bus reset, and the endpoint NAKing until there is, is the correct behaviour:
chips/rp2040/src/usb.rs:1552 -self.dpsram.ep_buf_ctrl[0].ep_in_buf_ctrl.modify( - EP_BUFFER_CONTROL::AVAILABLE0::SET - + EP_BUFFER_CONTROL::TRANSFER_LENGTH0.val(64) - + EP_BUFFER_CONTROL::DATA_PID0::CLEAR, -); +// Leave EP0 IN unarmed. There is nothing to send until a SETUP +// arrives, and an available buffer is one the controller owns -- +// transmit_in_ep0() would then be rewriting its length and PID +// underneath it. +self.dpsram.ep_buf_ctrl[0].ep_in_buf_ctrl.set(0);
A second, larger change is worth considering separately: making
transmit_in_ep0 build the buffer control value and
write() it, rather than modify() it. Every
modify() on these registers is a read of a register the controller
also writes status into. Clearing AVAILABLE at reset removes the
window that this bug goes through; it does not make the read-modify-write
pattern correct in general.
8What this does not establish
- The fix is not tested. I do not have an RP2040 board. Everything above is read from the source, the datasheet and the reference driver, plus the capture in the report. Nobody should merge the diff in section 7 on the strength of this page — it needs someone with a Pico, an analyser and the failing host.
- The RP2350 cannot stand in for it.
chips/rp2350has no USB driver at all; the onlyusbmatches on a Pico 2 board arePllUsbclock names. Only the four RP2040 boards have USB at all, and only three of them name it:raspberry_pi_pico,pico_explorer_baseandnano_rp2040_connectinstantiaterp2040::usb::UsbCtrl, whileraspberry_pi_pico_winherits it as a binary overraspberry_pi_pico::Platform. - The race is inferred, not observed. That the controller samples the
buffer control before
transmit_in_ep0rewrites it is what the capture is consistent with and what the ownership rule predicts. I have not instrumented it. - It may not be the only cause. The capture shows one defect clearly. Whether fixing it is sufficient for the Dell and HP hosts in the report is an open question, not something this page answers.
- One loose end, unrelated.
boards/raspberry_pi_pico/src/lib.rspasses a literal64as the CDC control packet size withMAX_CTRL_PACKET_SIZE_RP2040commented out beside it — and that constant does not exist incapsules/extra/src/usb/cdc.rs, which defines only the SAM4L, nRF52840 and EarlGrey ones. The literal is a valid value; the commented name is dead.