Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 136 additions & 9 deletions src/portable/raspberrypi/rp2040/hcd_rp2040.c
Original file line number Diff line number Diff line change
Expand Up @@ -589,19 +589,146 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t
return true;
}

bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) {
(void)rhport;
(void)daddr;
(void)ep_addr;
return false; // TODO not implemented yet
// Disarm the EPX round-robin scheduler. Call when the active EPX endpoint is torn
// down and no other endpoint is pending, so a stale SOF / NAK-stop wakeup cannot
// resurrect the aborted transfer. Mirrors the "no more pending" cleanup in the ISR.
TU_ATTR_ALWAYS_INLINE static inline void epx_scheduler_disarm(void) {
#ifdef HAS_STOP_EPX_ON_NAK
usb_hw_clear->nak_poll = USB_NAK_POLL_STOP_EPX_ON_NAK_BITS;
usb_hw_clear->nak_poll = USB_NAK_POLL_EPX_STOPPED_ON_NAK_BITS; // clear any latched NAK-stop
#else
usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS;
usb_hw->nak_poll = USB_NAK_POLL_RESET;
epx_switch_request = false;
#endif
}

// Abort an in-flight (or queued) transfer and leave the endpoint configured and
// re-armable. The caller (tuh_edpt_abort_xfer) has already cleared the usbh-layer
// BUSY/claim, so this only quiesces hardware + the EPSTATE machine. The endpoint is
// NOT deconfigured here (that is hcd_edpt_close's job).
bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) {
(void)rhport;
(void)dev_addr;
(void)ep_addr;
// TODO not implemented yet
return false;

hw_endpoint_t *ep = edpt_find(dev_addr, ep_addr);
TU_ASSERT(ep);

// hw_endpoint_lock_update is a no-op on this port; the critical section is the
// only serialisation against hcd_rp2040_irq, which mutates epx/state/buf_status.
rp2usb_critical_enter();

if (ep->interrupt_num > 0) {
// Interrupt endpoint: dedicated hardware that polls autonomously, not on the
// shared EPX path. There is no STOP_TRANS for it. Drop state to IDLE, de-arm the
// buffer (clears AVAIL), and clear its latched completion bit. The critical
// section already blocks the ISR; the cleared bit plus the IDLE state also cover
// the post-exit window. Leave int_ep_ctrl polling enabled (abort != close): the
// next hcd_edpt_xfer re-arms via the buffer control.
ep->state = EPSTATE_IDLE;
*dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0;
// buf_status bit layout: IN at (interrupt_num << 1), OUT at ((interrupt_num << 1) | 1)
const uint8_t bit_idx = (uint8_t)((ep->interrupt_num << 1u) | (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_IN ? 0u : 1u));
usb_hw_clear->buf_status = TU_BIT(bit_idx);
rp2usb_reset_transfer(ep);
} else if (epx == ep && ep->state == EPSTATE_ACTIVE) {
// Active control/bulk endpoint owning the shared EPX engine. Stop the in-flight
// transaction on the wire BEFORE touching state (host stop primitive is SIE
// STOP_TRANS via sie_stop_xfer; the device usb_hw->abort handshake is device-only).
sie_stop_xfer();

// Retract any half still marked AVAIL so the controller cannot poll a stale
// buffer after we reset state. Unlike epx_save_context (the preemption path),
// we do NOT roll back PID/len and re-queue: abort means do-not-resume. The data
// toggle (next_pid) is therefore left as-armed, the same stance as the
// device-side hw_endpoint_abort_xfer; a bulk EP re-armed after abort (without a
// close or a CLEAR_FEATURE(ENDPOINT_HALT) toggle reset) may resend with an
// unexpected toggle.
usbh_dpram->epx_buf_ctrl = 0;

// Drop the endpoint to IDLE, then clear latched completions so the ISR cannot
// fire xfer_complete_isr into the now-idle EP.
rp2usb_reset_transfer(ep);
usb_hw_clear->buf_status = 1u; // BUF_STATUS_EPX (bit 0)
usb_hw_clear->sie_status =
USB_SIE_STATUS_TRANS_COMPLETE_BITS | USB_SIE_STATUS_RX_TIMEOUT_BITS | USB_SIE_STATUS_STALL_REC_BITS;

// epx now points at an idle slot. Promote a queued endpoint if any (the engine
// is free); otherwise leave epx on the idle slot and disarm the scheduler.
hw_endpoint_t *next_ep = epx_next_pending(epx);
if (next_ep != NULL) {
#ifdef HAS_STOP_EPX_ON_NAK
// Drop any latched NAK-stop so it cannot fire a spurious preemption round-trip
// against the transfer we are about to start on the promoted endpoint.
usb_hw_clear->nak_poll = USB_NAK_POLL_EPX_STOPPED_ON_NAK_BITS;
#endif
epx_switch_ep(next_ep);
} else {
epx_scheduler_disarm();
}
} else if (ep->state >= EPSTATE_PENDING) {
// Queued control/bulk endpoint (PENDING or PENDING_SETUP) that does NOT own the
// engine: another endpoint is on the wire. No SIE stop / buf_ctrl touch. Setting
// state to IDLE removes it from the implicit pending set (epx_next_pending only
// matches state >= EPSTATE_PENDING); the staged setup_packet bytes are harmless.
rp2usb_reset_transfer(ep);

// If that emptied the pending set and EPX itself is idle, disarm the scheduler.
if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) == NULL) {
epx_scheduler_disarm();
}
}
// else: already IDLE, nothing to stop (idempotent). Invariant: a control/bulk EP
// is ACTIVE only while it is epx, so the branches above catch every active case.

rp2usb_critical_exit();
return true;
}

// Deconfigure a single endpoint and free its ep_pool slot. The caller
// (tuh_edpt_close) always aborts first and rejects EP0, so by here the transfer is
// already stopped; this only deconfigures hardware and releases the slot. Mirrors
// the per-slot body of hcd_device_close.
bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) {
(void)rhport;

// Reject EP0 / the shared EPX control slot: it is never closed and ep_pool[0]
// must not be freed.
TU_VERIFY(tu_edpt_number(ep_addr) != 0);

hw_endpoint_t *ep = edpt_find(daddr, ep_addr);
TU_VERIFY(ep);

rp2usb_critical_enter();

ep->state = EPSTATE_IDLE; // defensive: drop from any scheduling (abort already did)

// Defensive: tuh_edpt_close aborts first, but the HCD API does not require it. If
// this slot is somehow still the active EPX owner, disarm the scheduler so a stale
// SOF/NAK wakeup cannot start a transfer on the slot we are about to free. After
// the normal abort-first path this is a no-op.
if (epx == ep) {
epx_scheduler_disarm();
}

if (ep->interrupt_num > 0) {
// Interrupt endpoint: tear down the dedicated hardware so the int_idx is returned
// to the free pool that hcd_edpt_open scans. Disable SIE polling, clear the
// address control, then zero the DPRAM buffer + control words.
usb_hw_clear->int_ep_ctrl = TU_BIT(ep->interrupt_num);
usb_hw->int_ep_addr_ctrl[ep->interrupt_num - 1] = 0;

io_rw_32 *ep_reg = dpram_int_ep_ctrl(ep->interrupt_num);
io_rw_32 *buf_reg = dpram_int_ep_buffer_ctrl(ep->interrupt_num);
*buf_reg = 0;
*ep_reg = 0;
}
// Control/bulk endpoints share the EPX registers (re-prepared on every xfer), so
// there is no per-endpoint hardware to deconfigure here.

ep->max_packet_size = 0; // mark slot unused (must be last: the allocation sentinel)

rp2usb_critical_exit();
return true;
}

bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *buffer, uint16_t buflen) {
Expand Down
Loading