EP0 IN is armed at bus reset and never taken back

tock/tock#5153 open when this was written reported 2026-09-06 by potto216

The report comes with a USB analyser capture, which is the hard part already done: the first packet of the data stage goes out as DATA0 carrying 64 bytes, where the standard requires DATA1 and the descriptor is 18 bytes long. Both wrong values are sitting in the endpoint's buffer control register before the request even arrives — written there by handle_bus_reset, which hands the buffer to the USB controller and never asks for it back.

Defect
handle_bus_reset arms 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:

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.

Figure 1. Tock's column is read from 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

bits 31:16the second buffer — unused, this endpoint is single buffered
15FULL
14LAST
13DATA_PID0
12RESET
11STALL
10AVAIL
9:0TRANSFER_LENGTH0
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.

Figure 2. Field positions from 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:

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