summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-06-13 00:22:03 +0700
committerhathach <[email protected]>2026-06-13 23:34:57 +0700
commit3b73ee7e926d228bfa6ea4961d15a11a0310ed44 (patch)
tree645c27eeb69b2ada40dd1fa5214cebc1422fef3f /src
parent91608e3c4f8c944674547e5c254759c1dda08379 (diff)
dcd/musb: gate stale EP0 RXRDY interrupts with rxrdy_consumed
The deferral path drains the SETUP but leaves RxPktRdy set, and the SETUP's IRQ latches after the ISR's clear-on-read intr_tx read - so a second process_ep0 pass (same ISR, via the intr_tx re-read merge) is guaranteed and misreads the leftovers: count0==0 fires a spurious DATA OUT completion, the replay's RXRDYC write turns the second pass into a phantom csrl==0 DATA IN completion, and a zero-length replay re-enters the deferral case on a drained FIFO (count0 assert or garbage saved as a SETUP). The registers cannot expose the staleness: RxPktRdy and count0 read unchanged until ServicedRxPktRdy is written. Track it in software: rxrdy_consumed means "RxPktRdy is set in hw but its packet was already consumed". Set wherever a drained packet's RXRDY is intentionally left set (OUT/zero-length flow-control parks, every DATA OUT drain awaiting the next arm, the deferral path); cleared at every RXRDYC write site (edpt0_xfer arms, dcd_set_address, STALLED/SETEND recovery, bus reset). The RXRDY block returns early while parked. Replayed IN requests skip the RXRDYC in pipe0_start_setup and keep the packet parked until the edpt0_xfer(DATA IN) arm acks it (before loading the shared FIFO), so the stale pass sees RXRDY+parked instead of csrl==0. The normal IDLE path is unchanged - master never re-entered these windows because the single SETUP edge was always consumed by the pass that parked it; the deferral is what introduced a pending second pass. Review follow-up for #3643 (dcd_musb.c l.516 finding). Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/portable/mentor/musb/dcd_musb.c36
1 files changed, 32 insertions, 4 deletions
diff --git a/src/portable/mentor/musb/dcd_musb.c b/src/portable/mentor/musb/dcd_musb.c
index 2c31e3b2f..0485c4374 100644
--- a/src/portable/mentor/musb/dcd_musb.c
+++ b/src/portable/mentor/musb/dcd_musb.c
@@ -99,6 +99,8 @@ typedef struct {
uint8_t pending_addr; // new USB address latched by dcd_set_address; applied when STATUS IN completes
tusb_control_request_t deferred_setup;
bool deferred_setup_valid;
+ bool rxrdy_consumed; // RxPktRdy left set in hw for an already-consumed packet (NAK flow control);
+ // RXRDY events are stale while set. Cleared when RXRDYC is written.
} pipe0;
pipe_state_t pipe[MUSB_PIPE_COUNT];
} dcd_data_t;
@@ -123,16 +125,23 @@ static void pipe0_start_setup(uint8_t rhport, musb_ep_csr_t* ep_csr,
_dcd.pipe0.remain_wlength = req->wLength;
if (req->wLength == 0) {
+ // Leave RXRDY set; edpt0_xfer(STATUS IN) acks it together with DATAEND.
_dcd.pipe0.state = PIPE0_STATE_STATUS_IN;
+ _dcd.pipe0.rxrdy_consumed = true;
} else {
if (req->bmRequestType & TUSB_DIR_IN_MASK) {
_dcd.pipe0.state = PIPE0_STATE_DATA_IN;
- ep_csr->csr0l = MUSB_CSRL0_RXRDYC;
+ // On a deferred replay the packet's RXRDY stays parked until the edpt0_xfer(DATA IN) arm
+ // acks it — a stale latched EP0 IRQ in between is gated by rxrdy_consumed.
+ if (!_dcd.pipe0.rxrdy_consumed) {
+ ep_csr->csr0l = MUSB_CSRL0_RXRDYC;
+ }
} else {
// If OUT (rx) direction, let edpt0_xfer() clear RXRDY when it's ready to receive data.
// Deliberate deviation from the databook's canonical flow (ack right after unload),
// used as NAK flow control until usbd arms the drain buffer.
_dcd.pipe0.state = PIPE0_STATE_DATA_OUT;
+ _dcd.pipe0.rxrdy_consumed = true;
}
}
@@ -412,6 +421,11 @@ static bool edpt0_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_
case PIPE0_STATE_DATA_OUT: {
_dcd.pipe0.xact_len = total_bytes;
if (dir_in) {
+ // Replayed SETUP keeps its RXRDY parked until here; ack it before loading the shared FIFO.
+ if (_dcd.pipe0.rxrdy_consumed) {
+ ep_csr->csr0l = MUSB_CSRL0_RXRDYC;
+ _dcd.pipe0.rxrdy_consumed = false;
+ }
// DATA IN: load FIFO, set TXRDY. Add DATAEND on the last chunk
// (ep0_remain_datalen == 0 after this load) to end the data stage.
tu_hwfifo_write(&musb_regs->fifo[0], buffer, total_bytes, NULL);
@@ -425,6 +439,7 @@ static bool edpt0_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_
// DATA OUT: arm drain target, ack RXRDY so host can send DATA OUT.
_dcd.pipe0.buf = buffer;
ep_csr->csr0l = MUSB_CSRL0_RXRDYC;
+ _dcd.pipe0.rxrdy_consumed = false;
}
break;
}
@@ -432,6 +447,7 @@ static bool edpt0_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_
case PIPE0_STATE_STATUS_IN:
TU_ASSERT(dir_in && total_bytes == 0); // only STATUS IN allowed
ep_csr->csr0l = MUSB_CSRL0_RXRDYC | MUSB_CSRL0_DATAEND;
+ _dcd.pipe0.rxrdy_consumed = false;
break;
case PIPE0_STATE_STATUS_OUT:
@@ -465,6 +481,7 @@ static void process_ep0(uint8_t rhport) {
ep_csr->csr0l = 0;
_dcd.pipe0.state = PIPE0_STATE_IDLE;
_dcd.pipe0.deferred_setup_valid = false;
+ _dcd.pipe0.rxrdy_consumed = false;
return;
}
@@ -474,6 +491,7 @@ static void process_ep0(uint8_t rhport) {
ep_csr->csr0l = MUSB_CSRL0_SETENDC;
_dcd.pipe0.state = PIPE0_STATE_IDLE;
_dcd.pipe0.deferred_setup_valid = false;
+ _dcd.pipe0.rxrdy_consumed = false;
if (!(csrl & MUSB_CSRL0_RXRDY)) {
return; /* no SETUP waiting behind it */
}
@@ -481,6 +499,9 @@ static void process_ep0(uint8_t rhport) {
// Receive Data (Setup or OUT)
if (csrl & MUSB_CSRL0_RXRDY) {
+ if (_dcd.pipe0.rxrdy_consumed) {
+ return; // stale latched IRQ: this RXRDY's packet was already drained
+ }
switch (_dcd.pipe0.state) {
case PIPE0_STATE_IDLE: {
tusb_control_request_t req;
@@ -498,8 +519,10 @@ static void process_ep0(uint8_t rhport) {
tu_hwfifo_read(&musb_regs->fifo[0], _dcd.pipe0.buf, count0, NULL);
_dcd.pipe0.remain_wlength -= count0;
}
+ // RXRDY stays set until the next edpt0_xfer arm acks it (NAK flow control):
+ // edpt0_xfer(DATA OUT) for a mid-stream packet, edpt0_xfer(STATUS IN) for the last.
+ _dcd.pipe0.rxrdy_consumed = true;
if (_dcd.pipe0.remain_wlength == 0) {
- // last packet: change state and leave RXRDY for edpt0_xfer(STATUS IN) to ack
_dcd.pipe0.state = PIPE0_STATE_STATUS_IN;
}
dcd_event_xfer_complete(rhport, TU_EP0_OUT, count0, XFER_RESULT_SUCCESS, true);
@@ -511,7 +534,8 @@ static void process_ep0(uint8_t rhport) {
// - Status IN/OUT finished, its IRQ and the new SETUP IRQ arrive at the same time.
// - Data IN finished and status OUT is received, both IRQs and the new SETUP IRQ arrive at the same time.
// Save the SETUP; it is replayed only once the old transfer is fully retired — i.e. when usbd has
- // made (or already made) its final edpt0_xfer() call for it.
+ // made (or already made) its final edpt0_xfer() call for it. Until the replayed
+ // packet is acked, its RXRDY stays parked so a stale latched EP0 IRQ cannot re-process it.
case PIPE0_STATE_DATA_IN:
case PIPE0_STATE_STATUS_OUT:
case PIPE0_STATE_STATUS_OUT_PENDING_XFER:
@@ -519,6 +543,7 @@ static void process_ep0(uint8_t rhport) {
case PIPE0_STATE_STATUS_IN: {
TU_VERIFY(pipe0_read_setup(musb_regs, ep_csr, &_dcd.pipe0.deferred_setup), );
_dcd.pipe0.deferred_setup_valid = true;
+ _dcd.pipe0.rxrdy_consumed = true;
switch (_dcd.pipe0.state) {
case PIPE0_STATE_DATA_IN:
@@ -549,7 +574,8 @@ static void process_ep0(uint8_t rhport) {
break;
default:
- // PIPE0_STATE_STATUS_IN: ZLP-sent IRQ coalesced with the SETUP.
+ // PIPE0_STATE_STATUS_IN: rxrdy_consumed gate + SetupEnd guarantee DATAEND was armed, i.e.
+ // usbd already made its status call; the ZLP-sent IRQ coalesced with the SETUP.
if (_dcd.pipe0.pending_addr) {
musb_regs->faddr = _dcd.pipe0.pending_addr;
_dcd.pipe0.pending_addr = 0;
@@ -633,6 +659,7 @@ static void process_bus_reset(uint8_t rhport) {
_dcd.pipe0.xact_len = 0;
_dcd.pipe0.remain_wlength = 0;
_dcd.pipe0.deferred_setup_valid = false;
+ _dcd.pipe0.rxrdy_consumed = false;
musb->intr_txen = 1; /* Enable only EP0 */
musb->intr_rxen = 0;
@@ -708,6 +735,7 @@ void dcd_set_address(uint8_t rhport, uint8_t dev_addr)
_dcd.pipe0.state = PIPE0_STATE_STATUS_IN;
/* Send STATUS IN ZLP with DATAEND; host ACK fires the confirmation IRQ. */
ep_csr->csr0l = MUSB_CSRL0_RXRDYC | MUSB_CSRL0_DATAEND;
+ _dcd.pipe0.rxrdy_consumed = false;
}
// Wake up host