From d0c396f040e973d4d1aafe28b26893f936472a05 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:33:16 -0700 Subject: [PATCH 01/12] rp2040 changes for RP6502 --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 277 ++++++++++++++++--- src/portable/raspberrypi/rp2040/rp2040_usb.c | 7 +- 2 files changed, 235 insertions(+), 49 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 02a4e055ee..8d6bda3b39 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -32,6 +32,14 @@ #include "pico.h" + // These two SIE_STATUS bits are present in the RP2350 SDK but absent from the RP2040 SDK. + #ifndef USB_SIE_STATUS_RX_SHORT_PACKET_BITS + #define USB_SIE_STATUS_RX_SHORT_PACKET_BITS 0x00001000u + #endif + #ifndef USB_SIE_STATUS_ENDPOINT_ERROR_BITS + #define USB_SIE_STATUS_ENDPOINT_ERROR_BITS 0x00800000u + #endif + #if defined(PICO_RP2350) && PICO_RP2350 == 1 #define HAS_STOP_EPX_ON_NAK #endif @@ -56,6 +64,11 @@ static hw_endpoint_t ep_pool[USB_MAX_ENDPOINTS]; static hw_endpoint_t *epx = &ep_pool[0]; // current active endpoint +// EP0 max packet size per device address. EPX is shared across all devices so +// its max_packet_size can become stale when switching between devices with +// different EP0 sizes (e.g. low-speed MPS=8 vs full-speed MPS=64). +static uint8_t _ep0_mps[CFG_TUH_DEVICE_MAX + CFG_TUH_HUB + 1]; // +1 for addr0 + #ifndef HAS_STOP_EPX_ON_NAK static volatile bool epx_switch_request = false; #endif @@ -70,6 +83,7 @@ enum { EPX_CTRL_DEFAULT = EP_CTRL_ENABLE_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | offsetof(usb_host_dpram_t, epx_data) }; +//--------------------------------------------------------------------+ //--------------------------------------------------------------------+ // //--------------------------------------------------------------------+ @@ -84,11 +98,25 @@ static hw_endpoint_t *edpt_alloc(void) { return NULL; } +int hcd_free_ep_count(void) { + int count = 0; + for (uint i = 1; i < TU_ARRAY_SIZE(ep_pool); i++) { + if (ep_pool[i].max_packet_size == 0) { + count++; + } + } + return count; +} + static hw_endpoint_t *edpt_find(uint8_t daddr, uint8_t ep_addr) { - for (uint32_t i = 0; i < TU_ARRAY_SIZE(ep_pool); i++) { + // EP0 (control) always uses the shared EPX at ep_pool[0] + if (tu_edpt_number(ep_addr) == 0) { + return &ep_pool[0]; + } + + for (uint32_t i = 1; i < TU_ARRAY_SIZE(ep_pool); i++) { hw_endpoint_t *ep = &ep_pool[i]; - if ((ep->dev_addr == daddr) && (ep->max_packet_size > 0) && - (ep->ep_addr == ep_addr || (tu_edpt_number(ep_addr) == 0 && tu_edpt_number(ep->ep_addr) == 0))) { + if ((ep->dev_addr == daddr) && (ep->max_packet_size > 0) && (ep->ep_addr == ep_addr)) { return ep; } } @@ -128,6 +156,18 @@ TU_ATTR_ALWAYS_INLINE static inline void sie_stop_xfer(void) { } static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bool need_pre) { + // Clear transient handshake/status latches from the prior EPX phase so + // stale bits cannot trigger immediate spurious interrupts on the new transfer. + usb_hw_clear->sie_status = USB_SIE_STATUS_ACK_REC_BITS | + USB_SIE_STATUS_NAK_REC_BITS | + USB_SIE_STATUS_STALL_REC_BITS | + USB_SIE_STATUS_DATA_SEQ_ERROR_BITS | + USB_SIE_STATUS_RX_TIMEOUT_BITS | + USB_SIE_STATUS_RX_SHORT_PACKET_BITS | + USB_SIE_STATUS_TRANS_COMPLETE_BITS | + USB_SIE_STATUS_ENDPOINT_ERROR_BITS; + usb_hw_clear->buf_status = 0x3u; // clear stale EPX buf_status + uint32_t sie_ctrl = usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK; // preserve base bits if (send_setup) { sie_ctrl |= USB_SIE_CTRL_SEND_SETUP_BITS; @@ -139,10 +179,10 @@ static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bo } // START_TRANS bit on SIE_CTRL has the same behavior as the AVAILABLE bit - // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access".! + // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access". // We write everything except the START_TRANS bit first, then wait some cycles. usb_hw->sie_ctrl = sie_ctrl; - busy_wait_at_least_cycles(12); + busy_wait_us(1); usb_hw->sie_ctrl = sie_ctrl | USB_SIE_CTRL_START_TRANS_BITS; } @@ -192,11 +232,11 @@ static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { static void __tusb_irq_path_func(epx_switch_ep)(hw_endpoint_t *ep) { const bool is_setup = (ep->state == EPSTATE_PENDING_SETUP); - epx = ep; // switch pointer + epx = ep; // switch pointer ep->state = EPSTATE_ACTIVE; if (is_setup) { - // panic("new setup \n"); + usbh_dpram->epx_buf_ctrl = 0; // clear stale buf_ctrl from previous endpoint usb_hw->dev_addr_ctrl = ep->dev_addr; sie_start_xfer(true, false, ep->need_pre); } else { @@ -238,8 +278,12 @@ static void __tusb_irq_path_func(xfer_complete_isr)(hw_endpoint_t *ep, xfer_resu rp2usb_reset_transfer(ep); hcd_event_xfer_complete(ep->dev_addr, ep->ep_addr, xferred_len, xfer_result, true); - // Carry more transfer on epx - if (is_more) { + // Carry more transfer on epx. + // Don't dispatch pending after EP0 (control) completions — leave EPX idle + // so the stack's next phase call (hcd_setup_send/hcd_edpt_xfer) starts + // directly. This prevents bulk traffic (e.g. VCP) from monopolising EPX + // between control phases, which starves recovery sequences. + if (is_more && tu_edpt_number(ep->ep_addr) != 0) { hw_endpoint_t *next_ep = epx_next_pending(epx); if (next_ep != NULL) { epx_switch_ep(next_ep); @@ -248,7 +292,6 @@ static void __tusb_irq_path_func(xfer_complete_isr)(hw_endpoint_t *ep, xfer_resu } static void __tusb_irq_path_func(handle_buf_status_isr)(void) { - pico_trace("buf_status 0x%08lx\n", buf_status); enum { BUF_STATUS_EPX = 1u }; @@ -305,36 +348,66 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { const uint32_t status = usb_hw->ints; if (status & USB_INTS_HOST_CONN_DIS_BITS) { - uint8_t speed = dev_speed(); + // Clear speed latch first; after settle the re-read reflects the true current state. + usb_hw_clear->sie_status = USB_SIE_STATUS_SPEED_BITS; + busy_wait_us(1); + const uint8_t speed = dev_speed(); if (speed == SIE_CTRL_SPEED_DISCONNECT) { + // Stop SIE and silence hardware before emitting remove. Concurrent + // BUFF_STATUS / TRANS_COMPLETE bits in this snapshot must not deliver + // completions for the device the stack is about to tear down. + usb_hw->sie_ctrl = SIE_CTRL_BASE; + usb_hw->int_ep_ctrl = 0; + usb_hw_clear->buf_status = 0xffffffffu; + usbh_dpram->epx_buf_ctrl = 0; + #ifndef HAS_STOP_EPX_ON_NAK + epx_switch_request = false; + #endif + for (uint i = 0; i < TU_ARRAY_SIZE(ep_pool); i++) { + if (ep_pool[i].state == EPSTATE_ACTIVE) rp2usb_reset_transfer(&ep_pool[i]); + } hcd_event_device_remove(RHPORT_NATIVE, true); } else { if (speed == SIE_CTRL_SPEED_LOW) { usb_hw->sie_ctrl = SIE_CTRL_BASE | USB_SIE_CTRL_KEEP_ALIVE_EN_BITS; } else { - usb_hw->sie_ctrl = SIE_CTRL_BASE | USB_SIE_CTRL_SOF_EN_BITS; + // KEEP_ALIVE_EN is needed even for full-speed root devices (hubs) when + // low-speed devices are attached downstream. It sends a low-speed EOP + // before each SOF so low-speed devices behind the hub can see SOFs. + usb_hw->sie_ctrl = SIE_CTRL_BASE | USB_SIE_CTRL_SOF_EN_BITS | USB_SIE_CTRL_KEEP_ALIVE_EN_BITS; } hcd_event_device_attach(RHPORT_NATIVE, true); } - usb_hw_clear->sie_status = USB_SIE_STATUS_SPEED_BITS; + } + + if (status & USB_INTS_HOST_RESUME_BITS) { + usb_hw_clear->sie_status = USB_SIE_STATUS_RESUME_BITS; } if (status & USB_INTS_STALL_BITS) { usb_hw_clear->sie_status = USB_SIE_STATUS_STALL_REC_BITS; + usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS + usbh_dpram->epx_buf_ctrl = 0; xfer_complete_isr(epx, XFER_RESULT_STALLED, true); } if (status & USB_INTS_ERROR_RX_TIMEOUT_BITS) { - usb_hw_clear->sie_status = USB_SIE_STATUS_RX_TIMEOUT_BITS; + usb_hw_clear->sie_status = USB_SIE_STATUS_RX_TIMEOUT_BITS | + USB_SIE_STATUS_RX_SHORT_PACKET_BITS | + USB_SIE_STATUS_TRANS_COMPLETE_BITS | + USB_SIE_STATUS_ENDPOINT_ERROR_BITS; const uint32_t sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; usb_hw->sie_ctrl = sie_ctrl; - // while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) {} + while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) tight_loop_contents(); + + usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS + usbh_dpram->epx_buf_ctrl = 0; - // Even if STOP_TRANS bit is clear, controller maybe in middle of retrying and may re-raise timeout once extra time - // Only handle if epx is active, don't carry more epx transfer since STOP_TRANS is raced and not safe. if (epx->state == EPSTATE_ACTIVE) { xfer_complete_isr(epx, XFER_RESULT_FAILED, false); + } else { + // Spurious timeout (EPX not active) — nothing to complete. } } @@ -346,6 +419,34 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { usb_hw->sie_ctrl = sie_ctrl; // clear setup bit epx->xferred_len = 8; xfer_complete_isr(epx, XFER_RESULT_SUCCESS, true); + } else if (epx->state == EPSTATE_ACTIVE && ((usb_hw->buf_status & 0x3u) == 0)) { + // STATUS-phase ZLP (0-byte transfer): TRANS_COMPLETE fires but BUFF_STATUS does not + // because there is no DPRAM movement for zero bytes. Complete EPX now. + xfer_complete_isr(epx, XFER_RESULT_SUCCESS, true); + } + } + + if (status & USB_INTS_ERROR_DATA_SEQ_BITS) { + // DATA_SEQ must be handled before BUFF_STATUS: if both fire simultaneously, + // processing BUFF_STATUS first would sync the wrong-PID data as SUCCESS. + // Clearing EPX buf_status bits here prevents handle_buf_status_isr() from + // delivering a false success completion after we report FAILED. + usb_hw_clear->sie_status = USB_SIE_STATUS_DATA_SEQ_ERROR_BITS | + USB_SIE_STATUS_RX_SHORT_PACKET_BITS | + USB_SIE_STATUS_RX_TIMEOUT_BITS | + USB_SIE_STATUS_TRANS_COMPLETE_BITS | + USB_SIE_STATUS_ENDPOINT_ERROR_BITS; + + const uint32_t ds_sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; + usb_hw->sie_ctrl = ds_sie_ctrl; + while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) tight_loop_contents(); + + usb_hw_clear->buf_status = 0x3u; + TU_LOG(1, " Data Seq Error: [0] = 0x%04x [1] = 0x%04x\r\n", + tu_u32_low16(usbh_dpram->epx_buf_ctrl), tu_u32_high16(usbh_dpram->epx_buf_ctrl)); + usbh_dpram->epx_buf_ctrl = 0; + if (epx->state == EPSTATE_ACTIVE) { + xfer_complete_isr(epx, XFER_RESULT_FAILED, false); } } @@ -380,6 +481,11 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; usb_hw->nak_poll = USB_NAK_POLL_RESET; epx_switch_request = false; + } else if (epx->state != EPSTATE_ACTIVE) { + // EPX is idle with pending transfers (e.g. after RX_TIMEOUT). + // Start the next pending transfer directly. + epx_switch_request = false; + epx_switch_ep(next_ep); } else if (epx->state == EPSTATE_ACTIVE) { if (epx_switch_request) { // Second SOF with no transfer completion: endpoint is NAK-retrying, safe to switch. @@ -393,11 +499,6 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { } } #endif - - if (status & USB_INTS_ERROR_DATA_SEQ_BITS) { - usb_hw_clear->sie_status = USB_SIE_STATUS_DATA_SEQ_ERROR_BITS; - panic("Data Seq Error \n"); - } } void __tusb_irq_path_func(hcd_int_handler)(uint8_t rhport, bool in_isr) { @@ -484,6 +585,10 @@ void hcd_device_close(uint8_t rhport, uint8_t dev_addr) { return; // address 0 is for device enumeration } + if (dev_addr < TU_ARRAY_SIZE(_ep0_mps)) { + _ep0_mps[dev_addr] = 0; + } + rp2usb_critical_enter(); for (size_t i = 0; i < TU_ARRAY_SIZE(ep_pool); i++) { @@ -532,8 +637,8 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t (void)rhport; pico_trace("hcd_edpt_open dev_addr %d, ep_addr %d\n", dev_addr, ep_desc->bEndpointAddress); hw_endpoint_t *ep; - if (dev_addr == 0) { - ep = &ep_pool[0]; + if (dev_addr == 0 || ep_desc->bmAttributes.xfer == TUSB_XFER_CONTROL) { + ep = &ep_pool[0]; // EP0 shares EPX, no pool slot needed } else { ep = edpt_alloc(); } @@ -549,14 +654,30 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t ep->need_pre = need_pre(dev_addr); ep->next_pid = 0u; + // Remember EP0 max packet size so hcd_setup_send/hcd_edpt_xfer can restore it + if (tu_edpt_number(ep_addr) == 0 && dev_addr < TU_ARRAY_SIZE(_ep0_mps)) { + _ep0_mps[dev_addr] = (uint8_t)max_packet_size; + } + if (ep->transfer_type != TUSB_XFER_INTERRUPT) { ep->dpram_buf = usbh_dpram->epx_data; } else { - // from 15 interrupt endpoints pool + // Scan ep_pool (not usb_hw->int_ep_ctrl) to find a free interrupt slot. + // int_ep_ctrl is zeroed during EPX suppression and by abort, so using it + // as a free-slot bitmap causes interrupt_num collisions between devices. uint8_t int_idx; for (int_idx = 0; int_idx < USB_HOST_INTERRUPT_ENDPOINTS; int_idx++) { - if (!tu_bit_test(usb_hw->int_ep_ctrl, 1 + int_idx)) { - ep->interrupt_num = int_idx + 1; + const uint8_t candidate = int_idx + 1; + bool in_use = false; + for (uint j = 0; j < TU_ARRAY_SIZE(ep_pool); j++) { + if (&ep_pool[j] != ep && ep_pool[j].interrupt_num == candidate && + ep_pool[j].max_packet_size > 0) { + in_use = true; + break; + } + } + if (!in_use) { + ep->interrupt_num = candidate; break; } } @@ -591,17 +712,62 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t 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 + if (tu_edpt_number(ep_addr) == 0) return true; // EP0 (EPX) is shared + hw_endpoint_t *ep = edpt_find(daddr, ep_addr); + if (!ep) return true; + + rp2usb_critical_enter(); + if (ep->interrupt_num) { + usb_hw_clear->int_ep_ctrl = TU_BIT(ep->interrupt_num); + usb_hw->int_ep_addr_ctrl[ep->interrupt_num - 1] = 0; + *dpram_int_ep_ctrl(ep->interrupt_num) = 0; + *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; + } else { + // Non-interrupt (bulk/iso) endpoint using shared EPX + if (epx == ep) { + usbh_dpram->epx_buf_ctrl = 0; + } + } + rp2usb_reset_transfer(ep); + ep->max_packet_size = 0; + rp2usb_critical_exit(); + return true; } 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); + if (!ep) return true; + + rp2usb_critical_enter(); + + if (ep->interrupt_num) { + *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; + rp2usb_reset_transfer(ep); + } else { + if (ep->state == EPSTATE_ACTIVE && ep == epx) { + sie_stop_xfer(); + usbh_dpram->epx_buf_ctrl = 0; + rp2usb_reset_transfer(ep); + + // Clear any pending NAK/SOF round-robin state so a stale interrupt + // doesn't immediately dispatch another endpoint when IRQs re-enable. + #ifdef HAS_STOP_EPX_ON_NAK + usb_hw_clear->nak_poll = USB_NAK_POLL_EPX_STOPPED_ON_NAK_BITS | + USB_NAK_POLL_STOP_EPX_ON_NAK_BITS; + #else + epx_switch_request = false; + #endif + } else { + ep->state = EPSTATE_IDLE; + ep->remaining_len = 0; + ep->user_buf = 0; + } + } + ep->next_pid = 0; + + 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) { @@ -616,11 +782,18 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *b 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); rp2usb_xfer_start(ep, ep_reg, buf_reg, buffer, NULL, buflen); + // Re-enable polling in case it was cleared by hcd_edpt_abort_xfer. + // Always set unconditionally; suppress only applies to EPX transactions. + usb_hw_set->int_ep_ctrl = TU_BIT(ep->interrupt_num); } else { - // Control endpoint can change direction 0x00 <-> 0x80 when changing stages - if (ep_addr != ep->ep_addr) { - ep->ep_addr = ep_addr; - ep->next_pid = 1; // data and status stage start with DATA1 + // Control transfers: always reset next_pid to DATA1 for data and status stages. + ep->ep_addr = ep_addr; + if (tu_edpt_number(ep_addr) == 0) { + ep->dev_addr = dev_addr; + ep->need_pre = need_pre(dev_addr); + ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(_ep0_mps) && _ep0_mps[dev_addr]) + ? _ep0_mps[dev_addr] : 8; + ep->next_pid = 1; } // If EPX is busy with another transfer, mark as pending @@ -667,9 +840,13 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet usbh_dpram->setup_packet[i] = setup_packet[i]; } - ep->ep_addr = 0; // setup is OUT - ep->remaining_len = 8; - ep->xferred_len = 0; + ep->dev_addr = dev_addr; + ep->need_pre = need_pre(dev_addr); + ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(_ep0_mps) && _ep0_mps[dev_addr]) + ? _ep0_mps[dev_addr] : 8; + ep->ep_addr = 0; // setup is OUT + ep->remaining_len = 8; + ep->xferred_len = 0; // If EPX is busy, mark as pending setup (DPRAM already has the packet) if (epx->state == EPSTATE_ACTIVE) { @@ -684,6 +861,7 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet epx = ep; ep->state = EPSTATE_ACTIVE; + usbh_dpram->epx_buf_ctrl = 0; // clear stale buf_ctrl from previous phase usb_hw->dev_addr_ctrl = ep->dev_addr; sie_start_xfer(true, tu_edpt_dir(ep->ep_addr) == TUSB_DIR_IN, ep->need_pre); } @@ -694,11 +872,18 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet bool hcd_edpt_clear_stall(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { (void)rhport; - (void)dev_addr; - (void)ep_addr; - - panic("hcd_clear_stall"); - // return true; + hw_endpoint_t *ep = edpt_find(dev_addr, ep_addr); + if (ep) { + rp2usb_critical_enter(); + ep->next_pid = 0; + if (ep->interrupt_num) { + *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; + } else { + usbh_dpram->epx_buf_ctrl = 0; + } + rp2usb_critical_exit(); + } + return true; } #endif diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index 1b13934d31..f85b0ea742 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -123,7 +123,7 @@ void __tusb_irq_path_func(bufctrl_write32)(io_rw_32 *buf_reg, uint32_t value) { // Don't need delay in host mode as host is in charge of when to start the transaction. if (value & (USB_BUF_CTRL_AVAIL | (USB_BUF_CTRL_AVAIL << 16))) { if (!rp2usb_is_host_mode()) { - busy_wait_at_least_cycles(12); + busy_wait_us(1); } *buf_reg = value; // then set AVAILABLE bit last } @@ -139,7 +139,7 @@ void __tusb_irq_path_func(bufctrl_write16)(io_rw_16 *buf_reg16, uint16_t value) // Section 4.1.2.7.1 (rp2040) / 12.7.3.7.1 (rp2350) Concurrent access if (value & USB_BUF_CTRL_AVAIL) { if (!rp2usb_is_host_mode()) { - busy_wait_at_least_cycles(12); + busy_wait_us(1); } *buf_reg16 = value; // then set AVAILABLE bit last } @@ -257,6 +257,7 @@ void rp2usb_xfer_start(hw_endpoint_t *ep, io_rw_32 *ep_reg, io_rw_32 *buf_reg, u if (ep->remaining_len == 0) { const uint16_t xferred_len = ep->xferred_len; rp2usb_reset_transfer(ep); + dcd_event_xfer_complete(0, ep->ep_addr, xferred_len, XFER_RESULT_SUCCESS, false); hw_endpoint_lock_update(ep, -1); return; @@ -275,7 +276,7 @@ void rp2usb_xfer_start(hw_endpoint_t *ep, io_rw_32 *ep_reg, io_rw_32 *buf_reg, u } } #endif // CFG_TUSB_RP2_ERRATA_E15 - #endif // CFG_TUD_ENABLED + #endif // CFG_TUD_ENABLED rp2usb_buffer_start(ep, ep_reg, buf_reg, is_rx); hw_endpoint_lock_update(ep, -1); From aceaebb8d514fe2797b502fe39d701ab93569aa8 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:11:35 -0700 Subject: [PATCH 02/12] fix unaligned_memcpy --- src/portable/raspberrypi/rp2040/rp2040_usb.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index f85b0ea742..c340ef7a39 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -51,10 +51,14 @@ critical_section_t rp2usb_lock; //--------------------------------------------------------------------+ // Implementation //--------------------------------------------------------------------+ -// Provide own byte by byte memcpy as not all copies are aligned +// Provide own byte by byte memcpy as not all copies are aligned. +// Use volatile to prevent compiler from widening to 16/32-bit accesses +// which cause hard fault on RP2350 when dst/src point to USB DPRAM. static void unaligned_memcpy(uint8_t *dst, const uint8_t *src, size_t n) { + volatile uint8_t *vdst = dst; + const volatile uint8_t *vsrc = src; while (n--) { - *dst++ = *src++; + *vdst++ = *vsrc++; } } From 7ef411901f854d461a596d9efcf7ce02a7f76fbe Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:22:26 -0700 Subject: [PATCH 03/12] apply AI suggestions --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 5 ++++- src/portable/raspberrypi/rp2040/rp2040_usb.c | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 8d6bda3b39..4b4cea921d 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -607,6 +607,7 @@ void hcd_device_close(uint8_t rhport, uint8_t dev_addr) { *ep_reg = 0; } + ep->interrupt_num = 0; ep->max_packet_size = 0; // mark as unused } } @@ -660,6 +661,7 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t } if (ep->transfer_type != TUSB_XFER_INTERRUPT) { + ep->interrupt_num = 0; ep->dpram_buf = usbh_dpram->epx_data; } else { // Scan ep_pool (not usb_hw->int_ep_ctrl) to find a free interrupt slot. @@ -729,6 +731,7 @@ bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) { } } rp2usb_reset_transfer(ep); + ep->interrupt_num = 0; ep->max_packet_size = 0; rp2usb_critical_exit(); return true; @@ -737,7 +740,7 @@ bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) { bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { (void)rhport; hw_endpoint_t *ep = edpt_find(dev_addr, ep_addr); - if (!ep) return true; + if (!ep) return false; rp2usb_critical_enter(); diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index c340ef7a39..270d934d96 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -30,7 +30,7 @@ #if CFG_TUSB_MCU == OPT_MCU_RP2040 && (CFG_TUD_ENABLED || CFG_TUH_ENABLED) #include - #include "rp2040_usb.h" + #include "portable/raspberrypi/rp2040/rp2040_usb.h" #include "device/dcd.h" #include "host/hcd.h" @@ -261,7 +261,6 @@ void rp2usb_xfer_start(hw_endpoint_t *ep, io_rw_32 *ep_reg, io_rw_32 *buf_reg, u if (ep->remaining_len == 0) { const uint16_t xferred_len = ep->xferred_len; rp2usb_reset_transfer(ep); - dcd_event_xfer_complete(0, ep->ep_addr, xferred_len, XFER_RESULT_SUCCESS, false); hw_endpoint_lock_update(ep, -1); return; @@ -280,7 +279,7 @@ void rp2usb_xfer_start(hw_endpoint_t *ep, io_rw_32 *ep_reg, io_rw_32 *buf_reg, u } } #endif // CFG_TUSB_RP2_ERRATA_E15 - #endif // CFG_TUD_ENABLED + #endif // CFG_TUD_ENABLED rp2usb_buffer_start(ep, ep_reg, buf_reg, is_rx); hw_endpoint_lock_update(ep, -1); From 0459bb3caccf4bf750a1bdcf493235515bce8dde Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:50:38 -0700 Subject: [PATCH 04/12] busy_wait_at_least_cycles(32); --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 9 ++++----- src/portable/raspberrypi/rp2040/rp2040_usb.c | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 4b4cea921d..8c2a1ce19d 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -83,7 +83,6 @@ enum { EPX_CTRL_DEFAULT = EP_CTRL_ENABLE_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | offsetof(usb_host_dpram_t, epx_data) }; -//--------------------------------------------------------------------+ //--------------------------------------------------------------------+ // //--------------------------------------------------------------------+ @@ -179,10 +178,10 @@ static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bo } // START_TRANS bit on SIE_CTRL has the same behavior as the AVAILABLE bit - // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access". + // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access".! // We write everything except the START_TRANS bit first, then wait some cycles. usb_hw->sie_ctrl = sie_ctrl; - busy_wait_us(1); + busy_wait_at_least_cycles(32); usb_hw->sie_ctrl = sie_ctrl | USB_SIE_CTRL_START_TRANS_BITS; } @@ -232,7 +231,7 @@ static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { static void __tusb_irq_path_func(epx_switch_ep)(hw_endpoint_t *ep) { const bool is_setup = (ep->state == EPSTATE_PENDING_SETUP); - epx = ep; // switch pointer + epx = ep; // switch pointer ep->state = EPSTATE_ACTIVE; if (is_setup) { @@ -350,7 +349,7 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (status & USB_INTS_HOST_CONN_DIS_BITS) { // Clear speed latch first; after settle the re-read reflects the true current state. usb_hw_clear->sie_status = USB_SIE_STATUS_SPEED_BITS; - busy_wait_us(1); + busy_wait_at_least_cycles(32); const uint8_t speed = dev_speed(); if (speed == SIE_CTRL_SPEED_DISCONNECT) { // Stop SIE and silence hardware before emitting remove. Concurrent diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index 270d934d96..d1d4d255a8 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -127,7 +127,7 @@ void __tusb_irq_path_func(bufctrl_write32)(io_rw_32 *buf_reg, uint32_t value) { // Don't need delay in host mode as host is in charge of when to start the transaction. if (value & (USB_BUF_CTRL_AVAIL | (USB_BUF_CTRL_AVAIL << 16))) { if (!rp2usb_is_host_mode()) { - busy_wait_us(1); + busy_wait_at_least_cycles(32); } *buf_reg = value; // then set AVAILABLE bit last } @@ -143,7 +143,7 @@ void __tusb_irq_path_func(bufctrl_write16)(io_rw_16 *buf_reg16, uint16_t value) // Section 4.1.2.7.1 (rp2040) / 12.7.3.7.1 (rp2350) Concurrent access if (value & USB_BUF_CTRL_AVAIL) { if (!rp2usb_is_host_mode()) { - busy_wait_us(1); + busy_wait_at_least_cycles(32); } *buf_reg16 = value; // then set AVAILABLE bit last } From 42092085bebfe27f7e4e4770ad479820131e200c Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:58:57 -0700 Subject: [PATCH 05/12] fix CDC umount failure --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 127 ++++++++++++------- 1 file changed, 80 insertions(+), 47 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 8c2a1ce19d..a156fd22b0 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -69,6 +69,8 @@ static hw_endpoint_t *epx = &ep_pool[0]; // current active endpoint // different EP0 sizes (e.g. low-speed MPS=8 vs full-speed MPS=64). static uint8_t _ep0_mps[CFG_TUH_DEVICE_MAX + CFG_TUH_HUB + 1]; // +1 for addr0 +static bool epx_post_error = false; + #ifndef HAS_STOP_EPX_ON_NAK static volatile bool epx_switch_request = false; #endif @@ -197,33 +199,26 @@ TU_ATTR_ALWAYS_INLINE static inline void epx_ctrl_prepare(uint8_t transfer_type) // IN completed: FULL set to 1 in STATUS phase (was 0 when armed) // So undo when: AVAIL=1 (never started), or (OUT: FULL=1) or (IN: FULL=0) static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { - uint32_t buf_ctrl = usbh_dpram->epx_buf_ctrl; - const bool is_out = (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_OUT); - - do { - const uint16_t bc16 = (uint16_t)buf_ctrl; - if (bc16) { - const bool avail = (bc16 & USB_BUF_CTRL_AVAIL); - const bool full = (bc16 & USB_BUF_CTRL_FULL); - if (avail || (is_out ? full : !full)) { - const uint16_t buf_len = bc16 & USB_BUF_CTRL_LEN_MASK; - ep->remaining_len += buf_len; - ep->next_pid ^= 1u; - if (is_out) { - ep->user_buf -= buf_len; - } + const uint32_t buf_ctrl = usbh_dpram->epx_buf_ctrl; + const bool is_out = (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_OUT); + const bool is_double = (usbh_dpram->epx_ctrl & EP_CTRL_DOUBLE_BUFFERED_BITS); + + for (uint b = 0; b <= (is_double ? 1u : 0u); b++) { + const uint16_t bc16 = (uint16_t)(buf_ctrl >> (b * 16u)); + if (!bc16) continue; + const bool avail = (bc16 & USB_BUF_CTRL_AVAIL); + const bool full = (bc16 & USB_BUF_CTRL_FULL); + if (avail || (is_out ? full : !full)) { + const uint16_t buf_len = bc16 & USB_BUF_CTRL_LEN_MASK; + ep->remaining_len += buf_len; + ep->next_pid ^= 1u; + if (is_out) { + ep->user_buf -= buf_len; } } - - if (usbh_dpram->epx_ctrl & EP_CTRL_DOUBLE_BUFFERED_BITS) { - buf_ctrl >>= 16; - } else { - buf_ctrl = 0; - } - } while (buf_ctrl > 0); + } usbh_dpram->epx_buf_ctrl = 0; - ep->state = EPSTATE_PENDING; } @@ -359,11 +354,12 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { usb_hw->int_ep_ctrl = 0; usb_hw_clear->buf_status = 0xffffffffu; usbh_dpram->epx_buf_ctrl = 0; + epx_post_error = false; #ifndef HAS_STOP_EPX_ON_NAK epx_switch_request = false; #endif for (uint i = 0; i < TU_ARRAY_SIZE(ep_pool); i++) { - if (ep_pool[i].state == EPSTATE_ACTIVE) rp2usb_reset_transfer(&ep_pool[i]); + if (ep_pool[i].state != EPSTATE_IDLE) rp2usb_reset_transfer(&ep_pool[i]); } hcd_event_device_remove(RHPORT_NATIVE, true); } else { @@ -405,8 +401,14 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (epx->state == EPSTATE_ACTIVE) { xfer_complete_isr(epx, XFER_RESULT_FAILED, false); - } else { - // Spurious timeout (EPX not active) — nothing to complete. + } + + // After STOP_TRANS following an error the SIE needs time to settle before + // the next START_TRANS. Defer dispatch to the next SOF (~1ms) instead of + // dispatching immediately, which can silently lose the new transfer. + if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { + epx_post_error = true; + usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; } } @@ -447,14 +449,17 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (epx->state == EPSTATE_ACTIVE) { xfer_complete_isr(epx, XFER_RESULT_FAILED, false); } + // Defer to next SOF — same SIE settling issue as RX_TIMEOUT. + if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { + epx_post_error = true; + usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; + } } if (status & USB_INTS_BUFF_STATUS_BITS) { handle_buf_status_isr(); } - // SOF-based round-robin MUST run BEFORE BUFF_STATUS to avoid processing - // buf_status on the wrong EPX after a completion+switch in handle_buf_status_isr. #ifdef HAS_STOP_EPX_ON_NAK if (status & USB_INTS_EPX_STOPPED_ON_NAK_BITS) { usb_hw_clear->nak_poll = USB_NAK_POLL_EPX_STOPPED_ON_NAK_BITS; @@ -467,6 +472,25 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { sie_start_xfer(false, TUSB_DIR_IN == tu_edpt_dir(epx->ep_addr), epx->need_pre); } } + + // Post-error SOF deferral: after STOP_TRANS in error handlers, wait one SOF + // for the SIE to settle before dispatching the next pending transfer. + if (status & USB_INTS_HOST_SOF_BITS) { + (void)usb_hw->sof_rd; + if (epx_post_error) { + // This SOF may be in the same IRQ snapshot as the error. Skip it; + // the next SOF (~1ms) will dispatch. + epx_post_error = false; + } else if (epx->state != EPSTATE_ACTIVE) { + hw_endpoint_t *next_ep = epx_next_pending(epx); + if (next_ep != NULL) { + epx_switch_ep(next_ep); + } + usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; + } else { + usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; + } + } #else // RP2040: on SOF, switch EPX if another endpoint is pending. // First SOF sets epx_switch_request. If a transfer completes before next SOF, the flag is @@ -475,25 +499,31 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { // This avoids stopping mid-data-transfer which corrupts double-buffered PID tracking. if (status & USB_INTS_HOST_SOF_BITS) { (void)usb_hw->sof_rd; // clear SOF by reading SOF_RD - hw_endpoint_t *next_ep = epx_next_pending(epx); - if (next_ep == NULL) { - usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; - usb_hw->nak_poll = USB_NAK_POLL_RESET; - epx_switch_request = false; - } else if (epx->state != EPSTATE_ACTIVE) { - // EPX is idle with pending transfers (e.g. after RX_TIMEOUT). - // Start the next pending transfer directly. - epx_switch_request = false; - epx_switch_ep(next_ep); - } else if (epx->state == EPSTATE_ACTIVE) { - if (epx_switch_request) { - // Second SOF with no transfer completion: endpoint is NAK-retrying, safe to switch. + if (epx_post_error) { + // Skip this SOF — may be in the same IRQ snapshot as the error. + // Next SOF (~1ms) will dispatch via the idle-EPX path below. + epx_post_error = false; + } else { + hw_endpoint_t *next_ep = epx_next_pending(epx); + if (next_ep == NULL) { + usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; + usb_hw->nak_poll = USB_NAK_POLL_RESET; + epx_switch_request = false; + } else if (epx->state != EPSTATE_ACTIVE) { + // EPX is idle with pending transfers (e.g. after RX_TIMEOUT). + // Start the next pending transfer directly. epx_switch_request = false; - sie_stop_xfer(); - epx_save_context(epx); epx_switch_ep(next_ep); - } else { - epx_switch_request = true; + } else if (epx->state == EPSTATE_ACTIVE) { + if (epx_switch_request) { + // Second SOF with no transfer completion: endpoint is NAK-retrying, safe to switch. + epx_switch_request = false; + sie_stop_xfer(); + epx_save_context(epx); + epx_switch_ep(next_ep); + } else { + epx_switch_request = true; + } } } } @@ -527,6 +557,7 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { // clear epx and interrupt eps memset(&ep_pool, 0, sizeof(ep_pool)); + epx_post_error = false; // Enable in host mode with SOF / Keep alive on usb_hw->main_ctrl = USB_MAIN_CTRL_CONTROLLER_EN_BITS | USB_MAIN_CTRL_HOST_NDEVICE_BITS; @@ -865,7 +896,7 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet usbh_dpram->epx_buf_ctrl = 0; // clear stale buf_ctrl from previous phase usb_hw->dev_addr_ctrl = ep->dev_addr; - sie_start_xfer(true, tu_edpt_dir(ep->ep_addr) == TUSB_DIR_IN, ep->need_pre); + sie_start_xfer(true, false, ep->need_pre); } rp2usb_critical_exit(); @@ -881,7 +912,9 @@ bool hcd_edpt_clear_stall(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { if (ep->interrupt_num) { *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; } else { - usbh_dpram->epx_buf_ctrl = 0; + if (epx == ep) { + usbh_dpram->epx_buf_ctrl = 0; + } } rp2usb_critical_exit(); } From 7786fa7f5a8e8806d1ebff588323e962477f4bf8 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:26:48 -0700 Subject: [PATCH 06/12] another fix --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index a156fd22b0..1bbf420b56 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -283,6 +283,12 @@ static void __tusb_irq_path_func(xfer_complete_isr)(hw_endpoint_t *ep, xfer_resu epx_switch_ep(next_ep); } } +#ifdef HAS_STOP_EPX_ON_NAK + // RP2350: after EP0 completes EPX is idle and STOP_EPX_ON_NAK cannot fire. + else if (is_more && epx_next_pending(epx) != NULL) { + usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; + } +#endif } static void __tusb_irq_path_func(handle_buf_status_isr)(void) { From 0786cbdc4c564e92382adf81daae041be7005b9d Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:48:38 -0700 Subject: [PATCH 07/12] fix crash when unplugging device --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 185 +++++++++++-------- 1 file changed, 107 insertions(+), 78 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 1bbf420b56..db08e5e7db 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -60,14 +60,14 @@ // //--------------------------------------------------------------------+ -// Host mode uses one shared endpoint register for non-interrupt endpoint +// Host mode uses one shared endpoint register (EPX) for all non-interrupt endpoints static hw_endpoint_t ep_pool[USB_MAX_ENDPOINTS]; static hw_endpoint_t *epx = &ep_pool[0]; // current active endpoint // EP0 max packet size per device address. EPX is shared across all devices so // its max_packet_size can become stale when switching between devices with // different EP0 sizes (e.g. low-speed MPS=8 vs full-speed MPS=64). -static uint8_t _ep0_mps[CFG_TUH_DEVICE_MAX + CFG_TUH_HUB + 1]; // +1 for addr0 +static uint8_t ep0_mps[CFG_TUH_DEVICE_MAX + CFG_TUH_HUB + 1]; // +1 for addr0 static bool epx_post_error = false; @@ -151,12 +151,11 @@ TU_ATTR_ALWAYS_INLINE static inline bool need_pre(uint8_t dev_addr) { // EPX //--------------------------------------------------------------------+ TU_ATTR_ALWAYS_INLINE static inline void sie_stop_xfer(void) { - uint32_t sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; - usb_hw->sie_ctrl = sie_ctrl; - while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) {} + usb_hw->sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; + while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) tight_loop_contents(); } -static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bool need_pre) { +static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bool use_preamble) { // Clear transient handshake/status latches from the prior EPX phase so // stale bits cannot trigger immediate spurious interrupts on the new transfer. usb_hw_clear->sie_status = USB_SIE_STATUS_ACK_REC_BITS | @@ -175,13 +174,14 @@ static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bo } else { sie_ctrl |= (is_rx ? USB_SIE_CTRL_RECEIVE_DATA_BITS : USB_SIE_CTRL_SEND_DATA_BITS); } - if (need_pre) { + if (use_preamble) { sie_ctrl |= USB_SIE_CTRL_PREAMBLE_EN_BITS; } // START_TRANS bit on SIE_CTRL has the same behavior as the AVAILABLE bit - // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access".! - // We write everything except the START_TRANS bit first, then wait some cycles. + // described in RP2040 Datasheet, release 2.1, section "4.1.2.5.1. Concurrent access". + // Write everything except START_TRANS first, wait for the controller to see it, + // then set START_TRANS. usb_hw->sie_ctrl = sie_ctrl; busy_wait_at_least_cycles(32); usb_hw->sie_ctrl = sie_ctrl | USB_SIE_CTRL_START_TRANS_BITS; @@ -192,16 +192,15 @@ TU_ATTR_ALWAYS_INLINE static inline void epx_ctrl_prepare(uint8_t transfer_type) usbh_dpram->epx_ctrl = EPX_CTRL_DEFAULT | ((uint32_t)transfer_type << EP_CTRL_BUFFER_TYPE_LSB); } -// Save buffer context for EPX preemption (called after STOP_TRANS). // Undo PID toggle and buffer accounting for buffers NOT completed on the wire. // A buffer completed on wire means: controller reached STATUS phase (ACK received). // OUT completed: FULL cleared to 0 in STATUS phase (was 1 when armed) // IN completed: FULL set to 1 in STATUS phase (was 0 when armed) -// So undo when: AVAIL=1 (never started), or (OUT: FULL=1) or (IN: FULL=0) -static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { - const uint32_t buf_ctrl = usbh_dpram->epx_buf_ctrl; - const bool is_out = (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_OUT); - const bool is_double = (usbh_dpram->epx_ctrl & EP_CTRL_DOUBLE_BUFFERED_BITS); +// So undo when: AVAIL=1 (never started), or (OUT: FULL=1) or (IN: FULL=0). +// Caller passes the captured buf_ctrl (read BEFORE clearing epx_buf_ctrl). +static void __tusb_irq_path_func(epx_rollback_pid)(hw_endpoint_t *ep, uint32_t buf_ctrl) { + const bool is_out = (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_OUT); + const bool is_double = (usbh_dpram->epx_ctrl & EP_CTRL_DOUBLE_BUFFERED_BITS); for (uint b = 0; b <= (is_double ? 1u : 0u); b++) { const uint16_t bc16 = (uint16_t)(buf_ctrl >> (b * 16u)); @@ -217,7 +216,11 @@ static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { } } } +} +// Save buffer context for EPX preemption (called after STOP_TRANS). +static void __tusb_irq_path_func(epx_save_context)(hw_endpoint_t *ep) { + epx_rollback_pid(ep, usbh_dpram->epx_buf_ctrl); usbh_dpram->epx_buf_ctrl = 0; ep->state = EPSTATE_PENDING; } @@ -262,6 +265,27 @@ static hw_endpoint_t *__tusb_irq_path_func(epx_next_pending)(hw_endpoint_t *cur_ return NULL; } +// After STOP_TRANS in an error handler, the SIE needs time to settle before the +// next START_TRANS. Arm the SOF interrupt so dispatch happens on the next frame +// (~1ms) instead of immediately, which can silently lose the new transfer. +TU_ATTR_ALWAYS_INLINE static inline void arm_post_error_sof(void) { + if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { + epx_post_error = true; + usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; + } +} + +// Queue a pending EPX transfer for round-robin dispatch. +// RP2350: STOP_EPX_ON_NAK switches when the active EPX NAKs. +// RP2040: 2-SOF fallback driven by the SOF interrupt handler. +TU_ATTR_ALWAYS_INLINE static inline void enable_pending_dispatch(void) { + #ifdef HAS_STOP_EPX_ON_NAK + usb_hw_set->nak_poll = USB_NAK_POLL_STOP_EPX_ON_NAK_BITS; + #else + usb_hw->nak_poll = (300 << USB_NAK_POLL_DELAY_FS_LSB) | (300 << USB_NAK_POLL_DELAY_LS_LSB); + usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; + #endif +} //--------------------------------------------------------------------+ // Interrupt handlers @@ -388,8 +412,14 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (status & USB_INTS_STALL_BITS) { usb_hw_clear->sie_status = USB_SIE_STATUS_STALL_REC_BITS; usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS + const uint32_t stall_bc = usbh_dpram->epx_buf_ctrl; usbh_dpram->epx_buf_ctrl = 0; - xfer_complete_isr(epx, XFER_RESULT_STALLED, true); + if (epx->state == EPSTATE_ACTIVE) { + // Device sent STALL: no data ACKed, PID did not toggle on the device. + // Roll back so the post-CLEAR_FEATURE retry can't drift. + epx_rollback_pid(epx, stall_bc); + xfer_complete_isr(epx, XFER_RESULT_STALLED, true); + } } if (status & USB_INTS_ERROR_RX_TIMEOUT_BITS) { @@ -398,28 +428,24 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { USB_SIE_STATUS_TRANS_COMPLETE_BITS | USB_SIE_STATUS_ENDPOINT_ERROR_BITS; - const uint32_t sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; - usb_hw->sie_ctrl = sie_ctrl; - while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) tight_loop_contents(); + sie_stop_xfer(); usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS + const uint32_t rx_bc = usbh_dpram->epx_buf_ctrl; usbh_dpram->epx_buf_ctrl = 0; if (epx->state == EPSTATE_ACTIVE) { + // Device didn't respond: no ACK on the wire, so device's PID did not toggle. + // Undo the pre-arm toggle from bufctrl_prepare16 so the retry matches. + epx_rollback_pid(epx, rx_bc); xfer_complete_isr(epx, XFER_RESULT_FAILED, false); } - // After STOP_TRANS following an error the SIE needs time to settle before - // the next START_TRANS. Defer dispatch to the next SOF (~1ms) instead of - // dispatching immediately, which can silently lose the new transfer. - if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { - epx_post_error = true; - usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; - } + arm_post_error_sof(); } if (status & USB_INTS_TRANS_COMPLETE_BITS) { - // only applies for epx, interrupt endpoint does not seem to raise this + // Only raised by EPX. Interrupt endpoints signal completion via BUFF_STATUS only. usb_hw_clear->sie_status = USB_SIE_STATUS_TRANS_COMPLETE_BITS; if (usb_hw->sie_ctrl & USB_SIE_CTRL_SEND_SETUP_BITS) { uint32_t sie_ctrl = usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK; @@ -444,22 +470,20 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { USB_SIE_STATUS_TRANS_COMPLETE_BITS | USB_SIE_STATUS_ENDPOINT_ERROR_BITS; - const uint32_t ds_sie_ctrl = (usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK) | USB_SIE_CTRL_STOP_TRANS_BITS; - usb_hw->sie_ctrl = ds_sie_ctrl; - while (usb_hw->sie_ctrl & USB_SIE_CTRL_STOP_TRANS_BITS) tight_loop_contents(); + sie_stop_xfer(); usb_hw_clear->buf_status = 0x3u; + const uint32_t ds_bc = usbh_dpram->epx_buf_ctrl; TU_LOG(1, " Data Seq Error: [0] = 0x%04x [1] = 0x%04x\r\n", - tu_u32_low16(usbh_dpram->epx_buf_ctrl), tu_u32_high16(usbh_dpram->epx_buf_ctrl)); + tu_u32_low16(ds_bc), tu_u32_high16(ds_bc)); usbh_dpram->epx_buf_ctrl = 0; if (epx->state == EPSTATE_ACTIVE) { + // PID mismatch was not ACKed on the wire, so device's PID did not toggle. + // Undo the pre-arm toggle from bufctrl_prepare16 so the retry matches. + epx_rollback_pid(epx, ds_bc); xfer_complete_isr(epx, XFER_RESULT_FAILED, false); } - // Defer to next SOF — same SIE settling issue as RX_TIMEOUT. - if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { - epx_post_error = true; - usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; - } + arm_post_error_sof(); } if (status & USB_INTS_BUFF_STATUS_BITS) { @@ -481,19 +505,21 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { // Post-error SOF deferral: after STOP_TRANS in error handlers, wait one SOF // for the SIE to settle before dispatching the next pending transfer. + // STOP_EPX_ON_NAK handles the active-EPX case; here we only drive idle-EPX + // dispatch after errors. if (status & USB_INTS_HOST_SOF_BITS) { (void)usb_hw->sof_rd; if (epx_post_error) { // This SOF may be in the same IRQ snapshot as the error. Skip it; // the next SOF (~1ms) will dispatch. epx_post_error = false; - } else if (epx->state != EPSTATE_ACTIVE) { - hw_endpoint_t *next_ep = epx_next_pending(epx); - if (next_ep != NULL) { - epx_switch_ep(next_ep); - } - usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; } else { + if (epx->state != EPSTATE_ACTIVE) { + hw_endpoint_t *next_ep = epx_next_pending(epx); + if (next_ep != NULL) { + epx_switch_ep(next_ep); + } + } usb_hw_clear->inte = USB_INTE_HOST_SOF_BITS; } } @@ -589,7 +615,9 @@ bool hcd_deinit(uint8_t rhport) { void hcd_port_reset(uint8_t rhport) { (void)rhport; - // TODO: Nothing to do here yet. Perhaps need to reset some state? + // No software action: the SIE drives the electrical reset timing for the + // root port automatically during connect/enumerate. Downstream port resets + // are issued by the hub class driver via SET_FEATURE(PORT_RESET). } void hcd_port_reset_end(uint8_t rhport) { @@ -621,12 +649,21 @@ void hcd_device_close(uint8_t rhport, uint8_t dev_addr) { return; // address 0 is for device enumeration } - if (dev_addr < TU_ARRAY_SIZE(_ep0_mps)) { - _ep0_mps[dev_addr] = 0; + if (dev_addr < TU_ARRAY_SIZE(ep0_mps)) { + ep0_mps[dev_addr] = 0; } rp2usb_critical_enter(); + // If the current EPX endpoint belongs to the closing device, the SIE may + // still be polling a device that is about to vanish. Stop it and clear + // buf_ctrl so the next EPX user starts from a clean slate. + // (epx never points at an interrupt endpoint — those use int_ep slots.) + if (epx->dev_addr == dev_addr) { + sie_stop_xfer(); + usbh_dpram->epx_buf_ctrl = 0; + } + for (size_t i = 0; i < TU_ARRAY_SIZE(ep_pool); i++) { hw_endpoint_t *ep = &ep_pool[i]; if (ep->dev_addr == dev_addr && ep->max_packet_size > 0) { @@ -658,12 +695,14 @@ uint32_t hcd_frame_number(uint8_t rhport) { void hcd_int_enable(uint8_t rhport) { (void)rhport; + // Per-core; see hcd_int_disable for the calling-core contract. irq_set_enabled(USBCTRL_IRQ, true); } void hcd_int_disable(uint8_t rhport) { (void)rhport; - // todo we should check this is disabling from the correct core; note currently this is never called + // irq_set_enabled is per-core: this only masks the USB IRQ on the caller's + // core. Contract: must be called from the same core that called hcd_init. irq_set_enabled(USBCTRL_IRQ, false); } @@ -692,8 +731,8 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t ep->next_pid = 0u; // Remember EP0 max packet size so hcd_setup_send/hcd_edpt_xfer can restore it - if (tu_edpt_number(ep_addr) == 0 && dev_addr < TU_ARRAY_SIZE(_ep0_mps)) { - _ep0_mps[dev_addr] = (uint8_t)max_packet_size; + if (tu_edpt_number(ep_addr) == 0 && dev_addr < TU_ARRAY_SIZE(ep0_mps)) { + ep0_mps[dev_addr] = (uint8_t)max_packet_size; } if (ep->transfer_type != TUSB_XFER_INTERRUPT) { @@ -763,6 +802,9 @@ bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) { } else { // Non-interrupt (bulk/iso) endpoint using shared EPX if (epx == ep) { + if (ep->state == EPSTATE_ACTIVE) { + sie_stop_xfer(); + } usbh_dpram->epx_buf_ctrl = 0; } } @@ -825,13 +867,13 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *b // Always set unconditionally; suppress only applies to EPX transactions. usb_hw_set->int_ep_ctrl = TU_BIT(ep->interrupt_num); } else { - // Control transfers: always reset next_pid to DATA1 for data and status stages. ep->ep_addr = ep_addr; if (tu_edpt_number(ep_addr) == 0) { + // Control data and status stages always start with DATA1. ep->dev_addr = dev_addr; ep->need_pre = need_pre(dev_addr); - ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(_ep0_mps) && _ep0_mps[dev_addr]) - ? _ep0_mps[dev_addr] : 8; + ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(ep0_mps) && ep0_mps[dev_addr]) + ? ep0_mps[dev_addr] : 8; ep->next_pid = 1; } @@ -841,14 +883,7 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *b ep->user_buf = buffer; ep->remaining_len = buflen; ep->state = EPSTATE_PENDING; - - #ifdef HAS_STOP_EPX_ON_NAK - usb_hw_set->nak_poll = USB_NAK_POLL_STOP_EPX_ON_NAK_BITS; - #else - // Only enable SOF round-robin for non-control endpoints - usb_hw->nak_poll = (300 << USB_NAK_POLL_DELAY_FS_LSB) | (300 << USB_NAK_POLL_DELAY_LS_LSB); - usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; - #endif + enable_pending_dispatch(); } else { io_rw_32 *ep_reg = &usbh_dpram->epx_ctrl; io_rw_32 *buf_reg = &usbh_dpram->epx_buf_ctrl; @@ -881,8 +916,8 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet ep->dev_addr = dev_addr; ep->need_pre = need_pre(dev_addr); - ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(_ep0_mps) && _ep0_mps[dev_addr]) - ? _ep0_mps[dev_addr] : 8; + ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(ep0_mps) && ep0_mps[dev_addr]) + ? ep0_mps[dev_addr] : 8; ep->ep_addr = 0; // setup is OUT ep->remaining_len = 8; ep->xferred_len = 0; @@ -890,12 +925,7 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet // If EPX is busy, mark as pending setup (DPRAM already has the packet) if (epx->state == EPSTATE_ACTIVE) { ep->state = EPSTATE_PENDING_SETUP; - #ifdef HAS_STOP_EPX_ON_NAK - usb_hw_set->nak_poll = USB_NAK_POLL_STOP_EPX_ON_NAK_BITS; - #else - usb_hw->nak_poll = (300 << USB_NAK_POLL_DELAY_FS_LSB) | (300 << USB_NAK_POLL_DELAY_LS_LSB); - usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; - #endif + enable_pending_dispatch(); } else { epx = ep; ep->state = EPSTATE_ACTIVE; @@ -911,19 +941,18 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet bool hcd_edpt_clear_stall(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { (void)rhport; + if (tu_edpt_number(ep_addr) == 0) return true; // CLEAR_FEATURE(HALT) is not valid on EP0 hw_endpoint_t *ep = edpt_find(dev_addr, ep_addr); - if (ep) { - rp2usb_critical_enter(); - ep->next_pid = 0; - if (ep->interrupt_num) { - *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; - } else { - if (epx == ep) { - usbh_dpram->epx_buf_ctrl = 0; - } - } - rp2usb_critical_exit(); + if (!ep) return true; + + rp2usb_critical_enter(); + ep->next_pid = 0; + if (ep->interrupt_num) { + *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; + } else if (epx == ep) { + usbh_dpram->epx_buf_ctrl = 0; } + rp2usb_critical_exit(); return true; } From 08e00e4fdb3820b45b2d3ade0a6ffc97b89db6a6 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Wed, 3 Jun 2026 00:19:28 -0700 Subject: [PATCH 08/12] missed a epx_buf_ctrl=0 --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index db08e5e7db..e2f3aae45f 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -455,6 +455,7 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { } else if (epx->state == EPSTATE_ACTIVE && ((usb_hw->buf_status & 0x3u) == 0)) { // STATUS-phase ZLP (0-byte transfer): TRANS_COMPLETE fires but BUFF_STATUS does not // because there is no DPRAM movement for zero bytes. Complete EPX now. + usbh_dpram->epx_buf_ctrl = 0; xfer_complete_isr(epx, XFER_RESULT_SUCCESS, true); } } From f2d402d3ebfc6e288a33c907d0f623cfb0092f9c Mon Sep 17 00:00:00 2001 From: hathach Date: Sat, 20 Jun 2026 23:48:09 +0700 Subject: [PATCH 09/12] rp2040: remove unused hcd_free_ep_count Non-static function with no prototype or caller triggers -Werror=missing-prototypes, breaking the RP2040/RP2350 host build. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index e2f3aae45f..9a9ca2b752 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -99,16 +99,6 @@ static hw_endpoint_t *edpt_alloc(void) { return NULL; } -int hcd_free_ep_count(void) { - int count = 0; - for (uint i = 1; i < TU_ARRAY_SIZE(ep_pool); i++) { - if (ep_pool[i].max_packet_size == 0) { - count++; - } - } - return count; -} - static hw_endpoint_t *edpt_find(uint8_t daddr, uint8_t ep_addr) { // EP0 (control) always uses the shared EPX at ep_pool[0] if (tu_edpt_number(ep_addr) == 0) { From d8fefa3cd6a6a54841854b65106e0733a7d61bad Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:05:38 -0700 Subject: [PATCH 10/12] more cleanup and a remove an extra ep->next_pid = 0; --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 147 +++++++++---------- 1 file changed, 70 insertions(+), 77 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 9a9ca2b752..01dee421fc 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -85,6 +85,15 @@ enum { EPX_CTRL_DEFAULT = EP_CTRL_ENABLE_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | offsetof(usb_host_dpram_t, epx_data) }; +// EPX occupies buf_status bits 0 (buf0) and 1 (buf1). +#define EPX_BUF_STATUS_MASK 0x3u + +// Stale SIE transfer-status latches cleared before (re)arming and on EPX error. +#define SIE_STATUS_ERROR_CLEAR (USB_SIE_STATUS_RX_TIMEOUT_BITS | \ + USB_SIE_STATUS_RX_SHORT_PACKET_BITS | \ + USB_SIE_STATUS_TRANS_COMPLETE_BITS | \ + USB_SIE_STATUS_ENDPOINT_ERROR_BITS) + //--------------------------------------------------------------------+ // //--------------------------------------------------------------------+ @@ -137,6 +146,12 @@ TU_ATTR_ALWAYS_INLINE static inline bool need_pre(uint8_t dev_addr) { return hcd_port_speed_get(0) != tuh_speed_get(dev_addr); } +// EP0 max packet size for dev_addr, falling back to the 8-byte enumeration +// default until hcd_edpt_open records the real value. +TU_ATTR_ALWAYS_INLINE static inline uint8_t ep0_mps_get(uint8_t dev_addr) { + return (dev_addr < TU_ARRAY_SIZE(ep0_mps) && ep0_mps[dev_addr]) ? ep0_mps[dev_addr] : 8; +} + //--------------------------------------------------------------------+ // EPX //--------------------------------------------------------------------+ @@ -148,15 +163,12 @@ TU_ATTR_ALWAYS_INLINE static inline void sie_stop_xfer(void) { static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bool use_preamble) { // Clear transient handshake/status latches from the prior EPX phase so // stale bits cannot trigger immediate spurious interrupts on the new transfer. - usb_hw_clear->sie_status = USB_SIE_STATUS_ACK_REC_BITS | + usb_hw_clear->sie_status = SIE_STATUS_ERROR_CLEAR | + USB_SIE_STATUS_ACK_REC_BITS | USB_SIE_STATUS_NAK_REC_BITS | USB_SIE_STATUS_STALL_REC_BITS | - USB_SIE_STATUS_DATA_SEQ_ERROR_BITS | - USB_SIE_STATUS_RX_TIMEOUT_BITS | - USB_SIE_STATUS_RX_SHORT_PACKET_BITS | - USB_SIE_STATUS_TRANS_COMPLETE_BITS | - USB_SIE_STATUS_ENDPOINT_ERROR_BITS; - usb_hw_clear->buf_status = 0x3u; // clear stale EPX buf_status + USB_SIE_STATUS_DATA_SEQ_ERROR_BITS; + usb_hw_clear->buf_status = EPX_BUF_STATUS_MASK; // clear stale EPX buf_status uint32_t sie_ctrl = usb_hw->sie_ctrl & SIE_CTRL_BASE_MASK; // preserve base bits if (send_setup) { @@ -173,7 +185,7 @@ static void __tusb_irq_path_func(sie_start_xfer)(bool send_setup, bool is_rx, bo // Write everything except START_TRANS first, wait for the controller to see it, // then set START_TRANS. usb_hw->sie_ctrl = sie_ctrl; - busy_wait_at_least_cycles(32); + busy_wait_at_least_cycles(24); usb_hw->sie_ctrl = sie_ctrl | USB_SIE_CTRL_START_TRANS_BITS; } @@ -255,10 +267,11 @@ static hw_endpoint_t *__tusb_irq_path_func(epx_next_pending)(hw_endpoint_t *cur_ return NULL; } -// After STOP_TRANS in an error handler, the SIE needs time to settle before the -// next START_TRANS. Arm the SOF interrupt so dispatch happens on the next frame -// (~1ms) instead of immediately, which can silently lose the new transfer. -TU_ATTR_ALWAYS_INLINE static inline void arm_post_error_sof(void) { +// After STOP_TRANS (error handler or endpoint close) the SIE needs time to settle +// before the next START_TRANS. If EPX is idle with a transfer still pending, arm +// the SOF interrupt so dispatch happens on the next frame (~1ms) instead of +// immediately, which can silently lose the new transfer. +TU_ATTR_ALWAYS_INLINE static inline void arm_deferred_dispatch(void) { if (epx->state != EPSTATE_ACTIVE && epx_next_pending(epx) != NULL) { epx_post_error = true; usb_hw_set->inte = USB_INTE_HOST_SOF_BITS; @@ -305,6 +318,19 @@ static void __tusb_irq_path_func(xfer_complete_isr)(hw_endpoint_t *ep, xfer_resu #endif } +// Capture the EPX buffer control, clear it and the stale EPX buf_status, then — if +// EPX is still active — undo the pre-armed PID toggle (the transaction was not ACKed +// on the wire, so the device's toggle did not advance) and report to the stack. +static void __tusb_irq_path_func(epx_abort_active)(xfer_result_t result, bool is_more) { + usb_hw_clear->buf_status = EPX_BUF_STATUS_MASK; // prevent spurious EPX BUFF_STATUS + const uint32_t bc = usbh_dpram->epx_buf_ctrl; + usbh_dpram->epx_buf_ctrl = 0; + if (epx->state == EPSTATE_ACTIVE) { + epx_rollback_pid(epx, bc); + xfer_complete_isr(epx, result, is_more); + } +} + static void __tusb_irq_path_func(handle_buf_status_isr)(void) { enum { BUF_STATUS_EPX = 1u @@ -364,7 +390,7 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (status & USB_INTS_HOST_CONN_DIS_BITS) { // Clear speed latch first; after settle the re-read reflects the true current state. usb_hw_clear->sie_status = USB_SIE_STATUS_SPEED_BITS; - busy_wait_at_least_cycles(32); + busy_wait_at_least_cycles(24); const uint8_t speed = dev_speed(); if (speed == SIE_CTRL_SPEED_DISCONNECT) { // Stop SIE and silence hardware before emitting remove. Concurrent @@ -400,38 +426,16 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { } if (status & USB_INTS_STALL_BITS) { + // Device STALLed: no data ACKed, so roll back so a post-CLEAR_FEATURE retry can't drift. usb_hw_clear->sie_status = USB_SIE_STATUS_STALL_REC_BITS; - usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS - const uint32_t stall_bc = usbh_dpram->epx_buf_ctrl; - usbh_dpram->epx_buf_ctrl = 0; - if (epx->state == EPSTATE_ACTIVE) { - // Device sent STALL: no data ACKed, PID did not toggle on the device. - // Roll back so the post-CLEAR_FEATURE retry can't drift. - epx_rollback_pid(epx, stall_bc); - xfer_complete_isr(epx, XFER_RESULT_STALLED, true); - } + epx_abort_active(XFER_RESULT_STALLED, true); } if (status & USB_INTS_ERROR_RX_TIMEOUT_BITS) { - usb_hw_clear->sie_status = USB_SIE_STATUS_RX_TIMEOUT_BITS | - USB_SIE_STATUS_RX_SHORT_PACKET_BITS | - USB_SIE_STATUS_TRANS_COMPLETE_BITS | - USB_SIE_STATUS_ENDPOINT_ERROR_BITS; - + usb_hw_clear->sie_status = SIE_STATUS_ERROR_CLEAR; sie_stop_xfer(); - - usb_hw_clear->buf_status = 0x3u; // prevent spurious EPX BUFF_STATUS - const uint32_t rx_bc = usbh_dpram->epx_buf_ctrl; - usbh_dpram->epx_buf_ctrl = 0; - - if (epx->state == EPSTATE_ACTIVE) { - // Device didn't respond: no ACK on the wire, so device's PID did not toggle. - // Undo the pre-arm toggle from bufctrl_prepare16 so the retry matches. - epx_rollback_pid(epx, rx_bc); - xfer_complete_isr(epx, XFER_RESULT_FAILED, false); - } - - arm_post_error_sof(); + epx_abort_active(XFER_RESULT_FAILED, false); + arm_deferred_dispatch(); } if (status & USB_INTS_TRANS_COMPLETE_BITS) { @@ -442,7 +446,7 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { usb_hw->sie_ctrl = sie_ctrl; // clear setup bit epx->xferred_len = 8; xfer_complete_isr(epx, XFER_RESULT_SUCCESS, true); - } else if (epx->state == EPSTATE_ACTIVE && ((usb_hw->buf_status & 0x3u) == 0)) { + } else if (epx->state == EPSTATE_ACTIVE && ((usb_hw->buf_status & EPX_BUF_STATUS_MASK) == 0)) { // STATUS-phase ZLP (0-byte transfer): TRANS_COMPLETE fires but BUFF_STATUS does not // because there is no DPRAM movement for zero bytes. Complete EPX now. usbh_dpram->epx_buf_ctrl = 0; @@ -453,28 +457,14 @@ static void __tusb_irq_path_func(hcd_rp2040_irq)(void) { if (status & USB_INTS_ERROR_DATA_SEQ_BITS) { // DATA_SEQ must be handled before BUFF_STATUS: if both fire simultaneously, // processing BUFF_STATUS first would sync the wrong-PID data as SUCCESS. - // Clearing EPX buf_status bits here prevents handle_buf_status_isr() from - // delivering a false success completion after we report FAILED. - usb_hw_clear->sie_status = USB_SIE_STATUS_DATA_SEQ_ERROR_BITS | - USB_SIE_STATUS_RX_SHORT_PACKET_BITS | - USB_SIE_STATUS_RX_TIMEOUT_BITS | - USB_SIE_STATUS_TRANS_COMPLETE_BITS | - USB_SIE_STATUS_ENDPOINT_ERROR_BITS; - + // epx_abort_active() clears the EPX buf_status bits so handle_buf_status_isr() + // can't deliver a false success completion after we report FAILED. + usb_hw_clear->sie_status = SIE_STATUS_ERROR_CLEAR | USB_SIE_STATUS_DATA_SEQ_ERROR_BITS; sie_stop_xfer(); - - usb_hw_clear->buf_status = 0x3u; - const uint32_t ds_bc = usbh_dpram->epx_buf_ctrl; TU_LOG(1, " Data Seq Error: [0] = 0x%04x [1] = 0x%04x\r\n", - tu_u32_low16(ds_bc), tu_u32_high16(ds_bc)); - usbh_dpram->epx_buf_ctrl = 0; - if (epx->state == EPSTATE_ACTIVE) { - // PID mismatch was not ACKed on the wire, so device's PID did not toggle. - // Undo the pre-arm toggle from bufctrl_prepare16 so the retry matches. - epx_rollback_pid(epx, ds_bc); - xfer_complete_isr(epx, XFER_RESULT_FAILED, false); - } - arm_post_error_sof(); + tu_u32_low16(usbh_dpram->epx_buf_ctrl), tu_u32_high16(usbh_dpram->epx_buf_ctrl)); + epx_abort_active(XFER_RESULT_FAILED, false); + arm_deferred_dispatch(); } if (status & USB_INTS_BUFF_STATUS_BITS) { @@ -676,6 +666,10 @@ void hcd_device_close(uint8_t rhport, uint8_t dev_addr) { } } + // Stopping the EPX above may have left a surviving device's transfer pending; + // kick it (no-op if EPX is still busy with another device). + arm_deferred_dispatch(); + rp2usb_critical_exit(); } @@ -733,19 +727,16 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_endpoint_t // Scan ep_pool (not usb_hw->int_ep_ctrl) to find a free interrupt slot. // int_ep_ctrl is zeroed during EPX suppression and by abort, so using it // as a free-slot bitmap causes interrupt_num collisions between devices. + uint32_t used = 0; + for (uint j = 0; j < TU_ARRAY_SIZE(ep_pool); j++) { + if (&ep_pool[j] != ep && ep_pool[j].max_packet_size > 0 && ep_pool[j].interrupt_num) { + used |= TU_BIT(ep_pool[j].interrupt_num); + } + } uint8_t int_idx; for (int_idx = 0; int_idx < USB_HOST_INTERRUPT_ENDPOINTS; int_idx++) { - const uint8_t candidate = int_idx + 1; - bool in_use = false; - for (uint j = 0; j < TU_ARRAY_SIZE(ep_pool); j++) { - if (&ep_pool[j] != ep && ep_pool[j].interrupt_num == candidate && - ep_pool[j].max_packet_size > 0) { - in_use = true; - break; - } - } - if (!in_use) { - ep->interrupt_num = candidate; + if (!(used & TU_BIT(int_idx + 1))) { + ep->interrupt_num = int_idx + 1; break; } } @@ -802,6 +793,7 @@ bool hcd_edpt_close(uint8_t rhport, uint8_t daddr, uint8_t ep_addr) { rp2usb_reset_transfer(ep); ep->interrupt_num = 0; ep->max_packet_size = 0; + arm_deferred_dispatch(); // dispatch any endpoint left pending behind the closed EPX rp2usb_critical_exit(); return true; } @@ -819,7 +811,11 @@ bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { } else { if (ep->state == EPSTATE_ACTIVE && ep == epx) { sie_stop_xfer(); + const uint32_t bc = usbh_dpram->epx_buf_ctrl; usbh_dpram->epx_buf_ctrl = 0; + // Preserve the data toggle across the abort (USB 2.0 §5.8.5): undo only the + // pre-armed toggle for buffers not completed on the wire, never force DATA0. + epx_rollback_pid(ep, bc); rp2usb_reset_transfer(ep); // Clear any pending NAK/SOF round-robin state so a stale interrupt @@ -836,7 +832,6 @@ bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { ep->user_buf = 0; } } - ep->next_pid = 0; rp2usb_critical_exit(); return true; @@ -863,8 +858,7 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *b // Control data and status stages always start with DATA1. ep->dev_addr = dev_addr; ep->need_pre = need_pre(dev_addr); - ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(ep0_mps) && ep0_mps[dev_addr]) - ? ep0_mps[dev_addr] : 8; + ep->max_packet_size = ep0_mps_get(dev_addr); ep->next_pid = 1; } @@ -907,8 +901,7 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, const uint8_t setup_packet ep->dev_addr = dev_addr; ep->need_pre = need_pre(dev_addr); - ep->max_packet_size = (dev_addr < TU_ARRAY_SIZE(ep0_mps) && ep0_mps[dev_addr]) - ? ep0_mps[dev_addr] : 8; + ep->max_packet_size = ep0_mps_get(dev_addr); ep->ep_addr = 0; // setup is OUT ep->remaining_len = 8; ep->xferred_len = 0; From e858ed1f027af48f4946f71859ce7352cb6a7d13 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:16:33 -0700 Subject: [PATCH 11/12] re-read datasheet and talked this over with piers, 24 is enough --- src/portable/raspberrypi/rp2040/rp2040_usb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index d1d4d255a8..3e1a90bb53 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -127,7 +127,7 @@ void __tusb_irq_path_func(bufctrl_write32)(io_rw_32 *buf_reg, uint32_t value) { // Don't need delay in host mode as host is in charge of when to start the transaction. if (value & (USB_BUF_CTRL_AVAIL | (USB_BUF_CTRL_AVAIL << 16))) { if (!rp2usb_is_host_mode()) { - busy_wait_at_least_cycles(32); + busy_wait_at_least_cycles(24); } *buf_reg = value; // then set AVAILABLE bit last } @@ -143,7 +143,7 @@ void __tusb_irq_path_func(bufctrl_write16)(io_rw_16 *buf_reg16, uint16_t value) // Section 4.1.2.7.1 (rp2040) / 12.7.3.7.1 (rp2350) Concurrent access if (value & USB_BUF_CTRL_AVAIL) { if (!rp2usb_is_host_mode()) { - busy_wait_at_least_cycles(32); + busy_wait_at_least_cycles(24); } *buf_reg16 = value; // then set AVAILABLE bit last } From d85ef7d7316b246eb4cefb817f375cd724468a1a Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:37:13 -0700 Subject: [PATCH 12/12] improvements from hathach/tinyusb/pull/3702 --- src/portable/raspberrypi/rp2040/hcd_rp2040.c | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 01dee421fc..20a8431d43 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -805,8 +805,16 @@ bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { rp2usb_critical_enter(); + // Contract (hcd.h): true only if a transfer was actually aborted. + const bool had_xfer = (ep->state != EPSTATE_IDLE); + if (ep->interrupt_num) { *dpram_int_ep_buffer_ctrl(ep->interrupt_num) = 0; + // Drain a completion latched during teardown (IN at even bit, OUT at odd) + // so it can't fire into the reset endpoint when IRQs re-enable. + 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 (ep->state == EPSTATE_ACTIVE && ep == epx) { @@ -818,14 +826,23 @@ bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { epx_rollback_pid(ep, bc); rp2usb_reset_transfer(ep); - // Clear any pending NAK/SOF round-robin state so a stale interrupt - // doesn't immediately dispatch another endpoint when IRQs re-enable. + // Drain latched EPX completion/error state so a stale bit can't fire a + // spurious completion into the now-idle EPX when IRQs re-enable. + usb_hw_clear->buf_status = EPX_BUF_STATUS_MASK; + usb_hw_clear->sie_status = SIE_STATUS_ERROR_CLEAR | USB_SIE_STATUS_STALL_REC_BITS; + + // Drop any latched NAK round-robin state; the deferred dispatch below, not + // a stale NAK-stop, is what re-homes the scheduler. #ifdef HAS_STOP_EPX_ON_NAK usb_hw_clear->nak_poll = USB_NAK_POLL_EPX_STOPPED_ON_NAK_BITS | USB_NAK_POLL_STOP_EPX_ON_NAK_BITS; #else epx_switch_request = false; #endif + + // EPX is idle now: dispatch any endpoint queued behind the aborted one + // (deferred to the next SOF so the SIE settles after STOP_TRANS). + arm_deferred_dispatch(); } else { ep->state = EPSTATE_IDLE; ep->remaining_len = 0; @@ -834,7 +851,7 @@ bool hcd_edpt_abort_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr) { } rp2usb_critical_exit(); - return true; + return had_xfer; } bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *buffer, uint16_t buflen) {