hcd/rp2040: implement host endpoint abort and close#3702
Conversation
There was a problem hiding this comment.
Pull request overview
Implements the missing rp2040 host-controller endpoint lifecycle APIs so host-mode endpoint cancellation and teardown behave correctly and endpoint pool slots can be reused.
Changes:
- Implement
hcd_edpt_abort_xfer()for rp2040/rp2350 host mode, including EP_ABORT/EP_ABORT_DONE handling for “interrupt” endpoints. - Implement
hcd_edpt_close()to deconfigure an endpoint and free itsep_poolslot for reuse.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ( ep == &epx ) | ||
| { | ||
| // Control endpoint is driven directly by the SIE, not the EP_ABORT register. | ||
| // Drop any armed buffer and reset our view; leave the SIE_CTRL SOF / | ||
| // keep-alive base untouched. | ||
| *ep->buffer_control = 0; | ||
| hw_endpoint_reset_transfer(ep); | ||
| } |
| uint32_t const bit = 1ul << ((ep->interrupt_num + 1) * 2 + (ep->rx ? 0 : 1)); | ||
| usb_hw->abort_done = bit; // EP_ABORT_DONE is write-clear: drop any stale done so we wait on a fresh abort | ||
| usb_hw_set->abort = bit; | ||
| while ( !(usb_hw->abort_done & bit) ) {} | ||
|
|
||
| // Controller is idle on this endpoint: clear the armed buffer, drop any | ||
| // completion latched for it, clear done for the next abort, release abort. | ||
| *ep->buffer_control = 0; | ||
| usb_hw_clear->buf_status = bit; | ||
| usb_hw->abort_done = bit; | ||
| usb_hw_clear->abort = bit; | ||
| hw_endpoint_reset_transfer(ep); |
|
@claude review |
|
Claude finished @hathach's task in 3m 5s —— View job Code Review — hcd/rp2040: implement host endpoint abort and close
Overall: The design direction is correct — the EP_ABORT/ABORT_DONE handshake for interrupt endpoints and the pool-slot teardown in Critical — EPX abort does not clear latched completion bitsFile: *ep->buffer_control = 0;
hw_endpoint_reset_transfer(ep); // sets ep->active = false
// ... then hcd_int_enable()If
Fix: before *ep->buffer_control = 0;
usb_hw_clear->buf_status = 1u; // bit 0 = EPX
usb_hw_clear->sie_status = USB_SIE_STATUS_TRANS_COMPLETE_BITS
| USB_SIE_STATUS_STALL_REC_BITS;
hw_endpoint_reset_transfer(ep);Moderate —
|
| Severity | Issue |
|---|---|
| Critical | EPX abort: latched buf_status/sie_status bits must be cleared before re-enabling IRQ |
| Moderate | abort_done should use usb_hw_clear->abort_done alias, not base register |
| Moderate | Unbounded spin with IRQs masked; no RP2040-E2 chip-rev guard |
| Minor | hcd_edpt_close should assert !ep->active |
| Nit | hcd_device_close teardown inconsistently unmasked vs hcd_edpt_close |
The EP_ABORT handshake is the right primitive here and the bit-index calculation for interrupt endpoints is correct. Fixing the EPX latched-status bug and the two moderate issues would make this solid.
Fill in the hcd_edpt_abort_xfer and hcd_edpt_close TODO stubs for the rp2040/rp2350 host controller, against the EPSTATE / round-robin EPX driver. hcd_edpt_abort_xfer stops any in-flight transfer and leaves the endpoint re-armable (no double-arm or panic on the next hcd_edpt_xfer): - active EPX control/bulk: sie_stop_xfer (host SIE STOP_TRANS; the usb_hw->abort handshake is device-only), retract the AVAIL buffer, reset to IDLE, clear the latched EPX buf_status/sie_status so the ISR cannot fire into the idle slot, then re-home the round-robin scheduler (promote next pending EP or disarm). - queued control/bulk: drop to IDLE, removing it from the pending set. - interrupt EP: drop to IDLE first so a just-latched buf_status hits the IDLE guard, de-arm the buffer, clear its buf_status bit; polling stays enabled (abort is not close). All under the existing critical section (hw_endpoint_lock_update is a no-op here). hcd_edpt_close deconfigures one endpoint and frees its ep_pool slot, mirroring the per-slot teardown in hcd_device_close, clearing max_packet_size last. Compiles warning-free and links for the rp2040 host example; on-hardware validation of the abort race timing and round-robin interleaving is pending.
- abort (RP2350): clear a latched EPX_STOPPED_ON_NAK before promoting the next queued endpoint, so a stale NAK-stop cannot fire a spurious preemption against the transfer just started on the promoted endpoint. - close: defensively disarm the scheduler when the slot being freed is still the active epx. tuh_edpt_close aborts first so this is a no-op on the normal path, but the HCD API does not mandate abort-first. - comments: note the data toggle is intentionally not preserved across abort (same stance as the device-side hw_endpoint_abort_xfer); de-overclaim the interrupt-EP IDLE-guard note (the critical section already blocks the ISR); record the ACTIVE-control/bulk-is-epx invariant. Behaviour on the validated paths (active-EPX abort+resubmit, close+reopen) is unchanged; the new NAK-latch clear and close self-disarm cover branches the single-endpoint hardware test did not exercise.
|
Rebased onto current master and force-pushed. The branch was sitting a long way behind master so the original abort/close didn't line up with the current rp2040 host driver (epx is a round-robin pointer, an EPSTATE state machine, sie_stop_xfer to stop a transfer). I've reimplemented abort stops the active EPX transfer with Tested on an RP2350 (Pico 2 W) hosting a CDC device: aborting a pending bulk-IN read and resubmitting on the same endpoint runs clean now, no |
48654ce to
b414cc7
Compare
|
| target | .text | .rodata | .data | .bss | total | % diff |
|---|---|---|---|---|---|---|
| raspberry_pi_pico/hid_controller | 24,032 → 24,296 (+264) | — | — | — | 25,232 → 25,496 (+264) | +1.0% |
| raspberry_pi_pico/device_info | 24,752 → 25,016 (+264) | — | — | — | 26,048 → 26,312 (+264) | +1.0% |
| raspberry_pi_pico/bare_api | 25,408 → 25,672 (+264) | — | — | — | 26,792 → 27,056 (+264) | +1.0% |
| raspberry_pi_pico/midi_rx | 26,024 → 26,288 (+264) | — | — | — | 26,980 → 27,244 (+264) | +1.0% |
| raspberry_pi_pico/midi2_host | 26,416 → 26,680 (+264) | — | — | — | 27,652 → 27,916 (+264) | +1.0% |
| raspberry_pi_pico/cdc_msc_hid | 36,640 → 36,904 (+264) | — | — | — | 38,964 → 39,228 (+264) | +0.7% |
| raspberry_pi_pico/msc_file_explorer | 44,296 → 44,560 (+264) | — | — | — | 48,200 → 48,464 (+264) | +0.5% |
| raspberry_pi_pico/msc_file_explorer_freertos | 51,356 → 51,620 (+264) | — | — | — | 55,484 → 55,748 (+264) | +0.5% |
…). Points the submodule at the andrewleech/tinyusb rp2-host-edpt-abort branch (b414cc7d8): the reimplemented hcd_edpt_abort_xfer and hcd_edpt_close for the rp2040/rp2350 host against the refactored sie_stop_xfer / EPSTATE driver on TinyUSB 0.20 (hathach/tinyusb#3702). Signed-off-by: Andrew Leech <andrew@alelec.net>
lib/tinyusb: bump to rp2 host endpoint abort/close fix (hathach/tinyusb#3702) MBM-URL: https://github.com/andrewleech/micropython/tree/tinyusb-rp2-host-abort Signed-off-by: Andrew Leech <andrew@alelec.net>
lib/tinyusb: bump to rp2 host endpoint abort/close fix (hathach/tinyusb#3702) MBM-URL: https://github.com/andrewleech/micropython/tree/tinyusb-rp2-host-abort Signed-off-by: Andrew Leech <andrew@alelec.net>
Summary
This extends the rp2040/rp2350 host driver to implement two HCD endpoint methods that were left as TODOs when host support landed,
hcd_edpt_abort_xferandhcd_edpt_close. I came at them building raw bulk endpoint forwarding over the rp2 host, but they're on the normal host paths too,tuh_edpt_closeaborts then closes an endpoint and usbh aborts every endpoint on a SET_CONFIGURATION, so filling them in rounds out the existing host support.The abort runs the EP_ABORT / EP_ABORT_DONE handshake so the controller stops accessing an interrupt endpoint before we clear its buffer, and leaves the endpoint configured so the next submit re-arms cleanly. Without that the buffer stays armed while the stack thinks the endpoint is idle, so the next transfer arms an already-available buffer and trips
panic("ep %02X was already available"). Control (EPX) just clears its buffer and resets. The USB interrupt is masked across the teardown sincehw_endpoint_lock_updateis a no-op on this port.The close deconfigures the endpoint the same way
hcd_device_closealready does per endpoint, which frees theep_poolslot for reuse. Without ittuh_edpt_closeaborts the transfer but leaves the endpoint allocated, so a latertuh_edpt_openof the same endpoint takes a fresh slot and works through the 15 interrupt slots.Both land on rp2350 as well since it shares the driver.
Testing
Tested on RP2350 (Pico 2 W) acting as a USB host against a full-speed CDC device, driving raw bulk endpoint transfers with
CFG_TUH_API_EDPT_XFER=1. Before this, aborting a pending bulk-IN read and resubmitting on the same endpoint panicked, confirmed over SWD with the endpoint stillactiveand its buffer armed. After, repeated cancel/resubmit cycles run clean with no panic and no duplicate or stuck endpoint-pool entries.EP_ABORT and EP_ABORT_DONE are tagged "Device only" in the datasheet, so I instrumented the spin to check the handshake actually works host-side: across 48 aborts from a clean boot, EP_ABORT_DONE asserted within 0 spin iterations every time, so it does work in host mode for these interrupt endpoints. Haven't tested on an actual rp2040 yet though, the abort register path is identical on both.
Trade-offs and Alternatives
The abort waits on
abort_donebefore clearing the buffer, the same handshake the device side uses inrp2040_usb.c. It runs at task level, not in the ISR, and done asserts within a few cycles for these endpoints (measured, see Testing), so the spin's fine. The alternative that avoids the "Device only" register entirely is to disable the endpoint'sint_ep_ctrlpolling and then clear the buffer (whathcd_device_closedoes), but given the handshake measurably works and gives a clean "controller is idle" signal, I went with EP_ABORT.Generative AI
I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the code and the description above.