Skip to content

hcd/rp2040: implement host endpoint abort and close#3702

Open
andrewleech wants to merge 2 commits into
hathach:masterfrom
andrewleech:rp2-host-edpt-abort
Open

hcd/rp2040: implement host endpoint abort and close#3702
andrewleech wants to merge 2 commits into
hathach:masterfrom
andrewleech:rp2-host-edpt-abort

Conversation

@andrewleech

Copy link
Copy Markdown
Contributor

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_xfer and hcd_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_close aborts 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 since hw_endpoint_lock_update is a no-op on this port.

The close deconfigures the endpoint the same way hcd_device_close already does per endpoint, which frees the ep_pool slot for reuse. Without it tuh_edpt_close aborts the transfer but leaves the endpoint allocated, so a later tuh_edpt_open of 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 still active and 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_done before clearing the buffer, the same handshake the device side uses in rp2040_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's int_ep_ctrl polling and then clear the buffer (what hcd_device_close does), 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.

Copilot AI 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.

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 its ep_pool slot for reuse.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +637 to +644
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);
}
Comment on lines +655 to +666
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);
@hathach

hathach commented Jun 14, 2026

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hathach's task in 3m 5s —— View job


Code Review — hcd/rp2040: implement host endpoint abort and close

  • Gather context
  • Read changed file and diff
  • Review hcd_edpt_abort_xfer implementation
  • Review hcd_edpt_close implementation
  • Check against existing copilot review comments
  • Post findings

Overall: The design direction is correct — the EP_ABORT/ABORT_DONE handshake for interrupt endpoints and the pool-slot teardown in hcd_edpt_close are both the right approach. There are a few concrete bugs and robustness gaps worth addressing before merge.


Critical — EPX abort does not clear latched completion bits

File: src/portable/raspberrypi/rp2040/hcd_rp2040.c, EPX branch of hcd_edpt_abort_xfer (~line 637)

*ep->buffer_control = 0;
hw_endpoint_reset_transfer(ep);   // sets ep->active = false
// ... then hcd_int_enable()

If buf_status bit 0 (EPX) or sie_status STALL_REC / TRANS_COMPLETE are already latched when the IRQ is re-enabled, the ISR fires immediately and hits:

  • _handle_buff_status_bit()assert(ep->active)panic (ep->active is now false)
  • hw_trans_complete() for an in-flight setup → assert(ep->active)panic

Fix: before hcd_int_enable(), drain the pending status:

*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);

Fix this →


Moderate — abort_done cleared via wrong register alias

File: hcd_edpt_abort_xfer, lines 656 and 664

usb_hw->abort_done = bit;   // line 656 — stale-done clear
// ...
usb_hw->abort_done = bit;   // line 664 — post-abort clear

dcd_rp2040.c uses the CLEAR alias consistently:

usb_hw_clear->abort_done = abort_mask;

abort_done is write-1-to-clear (W1C), so writing bit to the base address is functionally equivalent (only the written bits are affected). But writing to the base address reads as "write the value to this register" whereas the CLEAR alias has a well-defined "write 1 to clear" semantic that matches the intent and is the established pattern everywhere else in this driver. Prefer usb_hw_clear->abort_done = bit for both occurrences for consistency and clarity.

Fix this →


Moderate — unbounded spin with IRQs masked

File: hcd_edpt_abort_xfer, line 658

while ( !(usb_hw->abort_done & bit) ) {}

With the USB IRQ masked and no timeout, a pathological endpoint state (very long or stalled packet in-flight, or unexpected silicon behavior on B0/B1) turns this into a permanent hang with no recovery path. The device-side code in dcd_rp2040.c already guards the entire abort on rp2040_chip_version() >= 2 to avoid the RP2040-E2 errata; the host driver makes no such check and doesn't document why it's safe to skip it.

Recommended: add a bounded iteration counter (e.g., 1000 iterations) and, on timeout, fall back to disabling the endpoint's int_ep_ctrl bit before clearing the buffer (the approach hcd_device_close already uses).


Minor — hcd_edpt_close lacks defensive active-check

File: hcd_edpt_close, line 537

The comment correctly notes that tuh_edpt_close aborts before calling this, but the function silently succeeds even if ep->active is still true (a race if the abort was skipped). Adding TU_ASSERT(!ep->active) before the teardown would make contract violations visible during development:

TU_ASSERT(!ep->active);   // caller must have aborted first
hcd_int_disable(rhport);
...

Nit — hcd_edpt_close IRQ masking inconsistent with hcd_device_close

hcd_device_close performs the same per-slot teardown without hcd_int_disable/enable. hcd_edpt_close adds the masking, which is correct and defensive — but it means the same sequence is unprotected in hcd_device_close. Consider aligning them (or at minimum add a comment explaining why hcd_device_close is safe without masking — e.g., called only after disconnect when no transfers are in flight).


Summary

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.
@andrewleech

Copy link
Copy Markdown
Contributor Author

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 hcd_edpt_abort_xfer and hcd_edpt_close against the current model, with the review points folded in.

abort stops the active EPX transfer with sie_stop_xfer (the host STOP_TRANS, not the device-only abort handshake), drops the ep to IDLE and clears the latched buf_status / sie_status before re-homing the round-robin scheduler, so the ep stays re-armable with no double-arm. close tears the ep down and frees its ep_pool slot the same way hcd_device_close does.

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 ep .. was already available panic, and close/reopen works over a couple of cycles. Haven't exercised the multi-endpoint promote path or an actual rp2040 on hardware yet though.

@andrewleech andrewleech force-pushed the rp2-host-edpt-abort branch from 48654ce to b414cc7 Compare June 16, 2026 01:35
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

Top 8 targets by memory change (%) (out of 2384 targets) View Project Dashboard →

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%

rumbledethumps added a commit to rumbledethumps/tinyusb that referenced this pull request Jun 22, 2026
andrewleech added a commit to andrewleech/micropython that referenced this pull request Jun 22, 2026
…).

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>
andrewleech added a commit to andrewleech/micropython that referenced this pull request Jun 22, 2026
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>
andrewleech added a commit to andrewleech/micropython that referenced this pull request Jul 1, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants