summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-07-17 16:40:34 +0700
committerGitHub <[email protected]>2026-07-17 16:40:34 +0700
commitaa410008e8e74b0727f8c30a1ec109ff2c37efc6 (patch)
tree613a6d86ae8afbec9d97f8ca3feac50c596b7782 /src
parentac595bc5cf64949332347a2b9d901de507a744a6 (diff)
parent8a42508300e03e3ed3bf7dc3e31821adf079189e (diff)
Merge pull request #3758 from hathach/usbtest
usbtest: device-side peer for the Linux kernel usbtest battery + DCD fixes across 13 ports
Diffstat (limited to 'src')
-rw-r--r--src/class/audio/audio_device.c3
-rw-r--r--src/class/vendor/vendor_device.c620
-rw-r--r--src/class/vendor/vendor_device.h161
-rw-r--r--src/common/tusb_mcu.h9
-rw-r--r--src/device/usbd.c84
-rw-r--r--src/portable/chipidea/ci_hs/dcd_ci_hs.c4
-rw-r--r--src/portable/mentor/musb/dcd_musb.c10
-rw-r--r--src/portable/microchip/samd/dcd_samd.c44
-rw-r--r--src/portable/nordic/nrf5x/dcd_nrf5x.c98
-rw-r--r--src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c83
-rw-r--r--src/portable/raspberrypi/rp2040/dcd_rp2040.c43
-rw-r--r--src/portable/raspberrypi/rp2040/rp2040_usb.c9
-rw-r--r--src/portable/renesas/rusb2/dcd_rusb2.c253
-rw-r--r--src/portable/renesas/rusb2/hcd_rusb2.c8
-rw-r--r--src/portable/renesas/rusb2/rusb2_ra.h42
-rw-r--r--src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c12
-rw-r--r--src/portable/synopsys/dwc2/dcd_dwc2.c34
-rw-r--r--src/portable/wch/ch32_usbfs_reg.h8
-rw-r--r--src/portable/wch/dcd_ch32_usbfs.c109
-rw-r--r--src/portable/wch/dcd_ch32_usbhs.c12
20 files changed, 1403 insertions, 243 deletions
diff --git a/src/class/audio/audio_device.c b/src/class/audio/audio_device.c
index 12e82190e..94881521a 100644
--- a/src/class/audio/audio_device.c
+++ b/src/class/audio/audio_device.c
@@ -1161,9 +1161,6 @@ static bool audiod_set_interface(uint8_t rhport, tusb_control_request_t const *p
is_feedback_ep = (desc_ep->bmAttributes.usage == 1);
}
- //TODO: We need to set EP non busy since this is not taken care of right now in ep_close() - THIS IS A WORKAROUND!
- usbd_edpt_clear_stall(rhport, ep_addr);
-
#if CFG_TUD_AUDIO_ENABLE_EP_IN
// For data or data with implicit feedback IN EP
if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN && is_data_ep)
diff --git a/src/class/vendor/vendor_device.c b/src/class/vendor/vendor_device.c
index b3537b665..c4a550ef0 100644
--- a/src/class/vendor/vendor_device.c
+++ b/src/class/vendor/vendor_device.c
@@ -21,6 +21,28 @@ typedef struct {
uint8_t rhport;
uint8_t itf_num;
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ uint8_t ep_int_out;
+ uint16_t int_rx_xfer_len;
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ uint8_t ep_int_in;
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ uint8_t ep_iso_out;
+ uint16_t iso_rx_xfer_len;
+ const tusb_desc_endpoint_t* iso_out_desc; // for deactivation on altsetting de-selection
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ uint8_t ep_iso_in;
+ const tusb_desc_endpoint_t* iso_in_desc; // for deactivation on altsetting de-selection
+ #endif
+ #if CFG_TUD_VENDOR_ALT_SETTINGS // implies non-buffered: fields cleared by bus reset
+ uint8_t cur_alt;
+ const uint8_t* p_itf_desc; // whole interface block incl. all altsettings (static app descriptor)
+ uint16_t itf_desc_len;
+ #endif
+
#if CFG_TUD_VENDOR_TXRX_BUFFERED
/*------------- From this point, data is not cleared by bus reset -------------*/
tu_edpt_stream_t tx_stream;
@@ -35,7 +57,10 @@ typedef struct {
} vendord_interface_t;
#if CFG_TUD_VENDOR_TXRX_BUFFERED
- #define ITF_MEM_RESET_SIZE (offsetof(vendord_interface_t, itf_num) + TU_FIELD_SIZE(vendord_interface_t, itf_num))
+ // The reset region is everything before the streams; tx_stream is the first preserved field
+ // (see the struct comment), so its offset is exactly that boundary regardless of which endpoint
+ // gates are enabled.
+ #define ITF_MEM_RESET_SIZE offsetof(vendord_interface_t, tx_stream)
#else
#define ITF_MEM_RESET_SIZE sizeof(vendord_interface_t)
#endif
@@ -52,6 +77,32 @@ typedef struct {
CFG_TUD_MEM_SECTION static vendord_epbuf_t _vendord_epbuf[CFG_TUD_VENDOR];
#endif
+#if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN
+typedef struct {
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ TUD_EPBUF_DEF(int_out, CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE);
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ TUD_EPBUF_DEF(int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE);
+ #endif
+} vendord_int_epbuf_t;
+
+CFG_TUD_MEM_SECTION static vendord_int_epbuf_t _vendord_int_epbuf[CFG_TUD_VENDOR];
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_OUT || CFG_TUD_VENDOR_EP_ISO_IN
+typedef struct {
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ TUD_EPBUF_DEF(iso_out, CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE);
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ TUD_EPBUF_DEF(iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE);
+ #endif
+} vendord_iso_epbuf_t;
+
+CFG_TUD_MEM_SECTION static vendord_iso_epbuf_t _vendord_iso_epbuf[CFG_TUD_VENDOR];
+#endif
+
//--------------------------------------------------------------------+
// Weak stubs: invoked if no strong implementation is available
//--------------------------------------------------------------------+
@@ -66,15 +117,61 @@ TU_ATTR_WEAK void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes) {
(void) sent_bytes;
}
+#if CFG_TUD_VENDOR_EP_INT_OUT
+TU_ATTR_WEAK void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) {
+ (void)idx;
+ (void)buffer;
+ (void)bufsize;
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_INT_IN
+TU_ATTR_WEAK void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes) {
+ (void)idx;
+ (void)sent_bytes;
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+TU_ATTR_WEAK void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize) {
+ (void)idx;
+ (void)buffer;
+ (void)bufsize;
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_IN
+TU_ATTR_WEAK void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes) {
+ (void)idx;
+ (void)sent_bytes;
+}
+#endif
+
bool tud_vendor_n_mounted(uint8_t idx) {
TU_VERIFY(idx < CFG_TUD_VENDOR);
vendord_interface_t *p_itf = &_vendord_itf[idx];
+ // bulk may be absent (interrupt-only vendor interface): count the interrupt endpoints too
#if CFG_TUD_VENDOR_TXRX_BUFFERED
- return (p_itf->rx_stream.ep_addr != 0) || (p_itf->tx_stream.ep_addr != 0);
+ bool mounted = (p_itf->rx_stream.ep_addr != 0) || (p_itf->tx_stream.ep_addr != 0);
#else
- return (p_itf->ep_out != 0) || (p_itf->ep_in != 0);
+ bool mounted = (p_itf->ep_out != 0) || (p_itf->ep_in != 0);
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ mounted = mounted || (p_itf->ep_int_out != 0);
#endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ mounted = mounted || (p_itf->ep_int_in != 0);
+ #endif
+ // an altsetting may expose only isochronous endpoints; count them so apps that gate an iso
+ // pump on tud_vendor_mounted() still arm it
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ mounted = mounted || (p_itf->ep_iso_out != 0);
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ mounted = mounted || (p_itf->ep_iso_in != 0);
+ #endif
+ return mounted;
}
//--------------------------------------------------------------------+
@@ -107,6 +204,30 @@ void tud_vendor_n_read_flush(uint8_t idx) {
}
#endif
+// Shared non-buffered transfer helpers for the bulk / interrupt / isochronous endpoints, which are
+// identical apart from the endpoint, its epbuf and its buffer size. TU_ATTR_UNUSED: in buffered
+// mode with the int/iso gates off none is referenced, and clang/IAR error on an unused static.
+TU_ATTR_UNUSED static inline uint32_t vendord_ep_write(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf,
+ uint32_t bufsize, const void *buffer, uint32_t len) {
+ TU_VERIFY(ep > 0, 0); // must be opened
+ TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep), 0);
+ const uint32_t xact_len = tu_min32(len, bufsize);
+ memcpy(epbuf, buffer, xact_len);
+ TU_ASSERT(usbd_edpt_xfer(p_itf->rhport, ep, epbuf, (uint16_t) xact_len, false), 0);
+ return xact_len;
+}
+
+TU_ATTR_UNUSED static inline uint32_t vendord_ep_write_available(vendord_interface_t *p_itf, uint8_t ep, uint32_t bufsize) {
+ TU_VERIFY(ep > 0, 0); // must be opened
+ return usbd_edpt_busy(p_itf->rhport, ep) ? 0 : bufsize;
+}
+
+TU_ATTR_UNUSED static inline bool vendord_ep_read_xfer(vendord_interface_t *p_itf, uint8_t ep, uint8_t *epbuf, uint16_t xfer_len) {
+ TU_VERIFY(ep > 0); // must be opened
+ TU_VERIFY(usbd_edpt_claim(p_itf->rhport, ep));
+ return usbd_edpt_xfer(p_itf->rhport, ep, epbuf, xfer_len, false);
+}
+
#if CFG_TUD_VENDOR_RX_MANUAL_XFER
bool tud_vendor_n_read_xfer(uint8_t idx) {
TU_VERIFY(idx < CFG_TUD_VENDOR);
@@ -116,9 +237,8 @@ bool tud_vendor_n_read_xfer(uint8_t idx) {
return tu_edpt_stream_read_xfer(&p_itf->rx_stream);
#else
- // Non-FIFO mode
- TU_VERIFY(usbd_edpt_claim(p_itf->rhport, p_itf->ep_out));
- return usbd_edpt_xfer(p_itf->rhport, p_itf->ep_out, _vendord_epbuf[idx].epout, p_itf->rx_xfer_len, false);
+ // Non-FIFO mode (0 while an altsetting without a bulk OUT ep is active)
+ return vendord_ep_read_xfer(p_itf, p_itf->ep_out, _vendord_epbuf[idx].epout, p_itf->rx_xfer_len);
#endif
}
#endif
@@ -135,12 +255,8 @@ uint32_t tud_vendor_n_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
return tu_edpt_stream_write(&p_itf->tx_stream, buffer, (uint16_t)bufsize);
#else
- // non-fifo mode: direct transfer
- TU_VERIFY(usbd_edpt_claim(p_itf->rhport, p_itf->ep_in), 0);
- const uint32_t xact_len = tu_min32(bufsize, CFG_TUD_VENDOR_TX_EPSIZE);
- memcpy(_vendord_epbuf[idx].epin, buffer, xact_len);
- TU_ASSERT(usbd_edpt_xfer(p_itf->rhport, p_itf->ep_in, _vendord_epbuf[idx].epin, (uint16_t)xact_len, false), 0);
- return xact_len;
+ // non-fifo mode: direct transfer (ep_in is 0 while an altsetting without a bulk IN ep is active)
+ return vendord_ep_write(p_itf, p_itf->ep_in, _vendord_epbuf[idx].epin, CFG_TUD_VENDOR_TX_EPSIZE, buffer, bufsize);
#endif
}
@@ -152,9 +268,7 @@ uint32_t tud_vendor_n_write_available(uint8_t idx) {
return tu_edpt_stream_write_available(&p_itf->tx_stream);
#else
- // Non-FIFO mode
- TU_VERIFY(p_itf->ep_in > 0, 0); // must be opened
- return usbd_edpt_busy(p_itf->rhport, p_itf->ep_in) ? 0 : CFG_TUD_VENDOR_TX_EPSIZE;
+ return vendord_ep_write_available(p_itf, p_itf->ep_in, CFG_TUD_VENDOR_TX_EPSIZE);
#endif
}
@@ -174,6 +288,63 @@ bool tud_vendor_n_write_clear(uint8_t idx) {
#endif
//--------------------------------------------------------------------+
+// Interrupt endpoint API
+//--------------------------------------------------------------------+
+#if CFG_TUD_VENDOR_EP_INT_OUT
+bool tud_vendor_n_int_read_xfer(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_read_xfer(p_itf, p_itf->ep_int_out, _vendord_int_epbuf[idx].int_out, p_itf->int_rx_xfer_len);
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_INT_IN
+uint32_t tud_vendor_n_int_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_write(p_itf, p_itf->ep_int_in, _vendord_int_epbuf[idx].int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, buffer, bufsize);
+}
+
+uint32_t tud_vendor_n_int_write_available(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_write_available(p_itf, p_itf->ep_int_in, CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE);
+}
+#endif
+
+//--------------------------------------------------------------------+
+// Isochronous endpoint API
+//--------------------------------------------------------------------+
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+bool tud_vendor_n_iso_read_xfer(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_read_xfer(p_itf, p_itf->ep_iso_out, _vendord_iso_epbuf[idx].iso_out, p_itf->iso_rx_xfer_len);
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_IN
+uint32_t tud_vendor_n_iso_write(uint8_t idx, const void *buffer, uint32_t bufsize) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_write(p_itf, p_itf->ep_iso_in, _vendord_iso_epbuf[idx].iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, buffer, bufsize);
+}
+
+uint32_t tud_vendor_n_iso_write_available(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
+ vendord_interface_t *p_itf = &_vendord_itf[idx];
+ return vendord_ep_write_available(p_itf, p_itf->ep_iso_in, CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE);
+}
+#endif
+
+#if CFG_TUD_VENDOR_ALT_SETTINGS
+uint8_t tud_vendor_n_alt(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUD_VENDOR, 0);
+ return _vendord_itf[idx].cur_alt;
+}
+#endif
+
+//--------------------------------------------------------------------+
// USBD Driver API
//--------------------------------------------------------------------+
void vendord_init(void) {
@@ -232,17 +403,61 @@ static uint8_t find_vendor_itf(uint8_t ep_addr) {
for (uint8_t idx = 0; idx < CFG_TUD_VENDOR; idx++) {
const vendord_interface_t *p_vendor = &_vendord_itf[idx];
if (ep_addr == 0) {
- // find unused: require both ep == 0
- #if CFG_TUD_VENDOR_TXRX_BUFFERED
- if (p_vendor->rx_stream.ep_addr == 0 && p_vendor->tx_stream.ep_addr == 0) {
+ // find unused interface slot
+ #if CFG_TUD_VENDOR_ALT_SETTINGS
+ // an opened interface parked in an altsetting without endpoints (the mandatory empty alt 0)
+ // has all ep fields 0, so the endpoint fields cannot distinguish free from open: use p_itf_desc
+ if (p_vendor->p_itf_desc == NULL) {
+ return idx;
+ }
+ #elif CFG_TUD_VENDOR_TXRX_BUFFERED
+ // A slot is free only if none of its endpoints are assigned; bulk may be absent
+ // (an interrupt-only vendor interface), so check the interrupt endpoints too.
+ if (p_vendor->rx_stream.ep_addr == 0 && p_vendor->tx_stream.ep_addr == 0
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ && p_vendor->ep_int_out == 0
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ && p_vendor->ep_int_in == 0
+ #endif
+ ) {
return idx;
}
#else
- if (p_vendor->ep_out == 0 && p_vendor->ep_in == 0) {
+ // A slot is free only if none of its endpoints are assigned. Bulk may be absent (an
+ // interrupt-only vendor interface), so the interrupt endpoints must be checked too.
+ if (p_vendor->ep_out == 0 && p_vendor->ep_in == 0
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ && p_vendor->ep_int_out == 0
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ && p_vendor->ep_int_in == 0
+ #endif
+ ) {
return idx;
}
#endif
} else {
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ if (ep_addr == p_vendor->ep_int_out) {
+ return idx;
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ if (ep_addr == p_vendor->ep_int_in) {
+ return idx;
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ if (ep_addr == p_vendor->ep_iso_out) {
+ return idx;
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ if (ep_addr == p_vendor->ep_iso_in) {
+ return idx;
+ }
+ #endif
#if CFG_TUD_VENDOR_TXRX_BUFFERED
if (ep_addr == p_vendor->rx_stream.ep_addr || ep_addr == p_vendor->tx_stream.ep_addr) {
return idx;
@@ -257,6 +472,287 @@ static uint8_t find_vendor_itf(uint8_t ep_addr) {
return 0xff;
}
+#if CFG_TUD_VENDOR_ALT_SETTINGS
+
+// Reserve an isochronous endpoint at open time. Ports with a dedicated iso allocator
+// (TUP_DCD_EDPT_ISO_ALLOC) reserve the FIFO here and (re)activate on altsetting selection;
+// ports with dcd_edpt_close instead open it once here (open == allocate + activate).
+static inline bool vendord_iso_ep_alloc(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
+ #ifdef TUP_DCD_EDPT_ISO_ALLOC
+ return usbd_edpt_iso_alloc(rhport, desc_ep->bEndpointAddress, tu_edpt_packet_size(desc_ep));
+ #else
+ return usbd_edpt_open(rhport, desc_ep);
+ #endif
+}
+
+// Deactivate a de-selected altsetting's isochronous endpoint: abort any in-flight
+// transfer and release its usbd claim so a stale completion cannot fire into a
+// no-longer-tracked endpoint (iso cannot be stalled like bulk/interrupt below). With the
+// iso-alloc API, (re)activation with the endpoint's descriptor is the abort/scrub
+// primitive; without it, close does (the next selection re-opens).
+static inline void vendord_iso_ep_deactivate(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
+ if (desc_ep != NULL) {
+ #ifdef TUP_DCD_EDPT_ISO_ALLOC
+ usbd_edpt_iso_activate(rhport, desc_ep);
+ #else
+ usbd_edpt_close(rhport, desc_ep->bEndpointAddress);
+ #endif
+ }
+}
+
+// (Re)activate an isochronous endpoint on altsetting selection.
+static inline bool vendord_iso_ep_activate(uint8_t rhport, const tusb_desc_endpoint_t* desc_ep) {
+ #ifdef TUP_DCD_EDPT_ISO_ALLOC
+ return usbd_edpt_iso_activate(rhport, desc_ep); // resets ep_status, aborting any stale transfer
+ #else
+ // No iso alloc/activate API: close (which zeros ep_status and frees a stale claim left by a
+ // prior selection) then re-open, so a re-selected altsetting starts from a clean state.
+ usbd_edpt_close(rhport, desc_ep->bEndpointAddress);
+ return usbd_edpt_open(rhport, desc_ep);
+ #endif
+}
+
+// Abort any in-flight transfer on a tracked endpoint and release its usbd claim; no-op for an
+// unset (0) address. stall disables the endpoint in the dcd, clear-stall resets it to DATA0.
+static inline void vendord_abort_ep(uint8_t rhport, uint8_t ep_addr) {
+ if (ep_addr) {
+ usbd_edpt_stall(rhport, ep_addr);
+ usbd_edpt_clear_stall(rhport, ep_addr);
+ }
+}
+
+// Select an altsetting. Endpoints were hardware-opened once at vendord_open (dcds like
+// dwc2 allocate FIFO linearly and cannot close/re-open endpoints dynamically): switching
+// only re-targets the API to the selected altsetting's endpoints. Bulk/interrupt
+// endpoints get a stall/clear-stall cycle, which portably aborts any in-flight transfer
+// (stall disables the endpoint in the dcd) and resets the data toggle to DATA0 as
+// SET_INTERFACE requires. Isochronous endpoints are (re)activated, which does the same.
+// Single pass: the current altsetting's endpoints are dropped only once the target altsetting
+// is confirmed present, so a SET_INTERFACE to an unknown alt leaves the interface intact.
+static bool vendord_set_alt(uint8_t rhport, uint8_t idx, uint8_t alt) {
+ vendord_interface_t *p_vendor = &_vendord_itf[idx];
+ const uint8_t* p_desc = p_vendor->p_itf_desc;
+ const uint8_t* desc_end = p_desc + p_vendor->itf_desc_len;
+ bool in_target_alt = false;
+ bool alt_found = false;
+
+ while (tu_desc_in_bounds(p_desc, desc_end)) {
+ const uint8_t desc_type = tu_desc_type(p_desc);
+ if (desc_type == TUSB_DESC_INTERFACE) {
+ in_target_alt = (((const tusb_desc_interface_t*)p_desc)->bAlternateSetting == alt);
+ if (in_target_alt && !alt_found) {
+ alt_found = true;
+ // target altsetting confirmed present: abort then drop the previous altsetting's endpoints,
+ // so an endpoint absent from the target altsetting can't stay armed and keep its usbd
+ // claim in the dcd. (Endpoints the target altsetting reuses are reset again below;
+ // a double reset is harmless.)
+ vendord_abort_ep(rhport, p_vendor->ep_in);
+ vendord_abort_ep(rhport, p_vendor->ep_out);
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ vendord_abort_ep(rhport, p_vendor->ep_int_out);
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ vendord_abort_ep(rhport, p_vendor->ep_int_in);
+ #endif
+ p_vendor->ep_in = 0;
+ p_vendor->ep_out = 0;
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ p_vendor->ep_int_out = 0;
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ p_vendor->ep_int_in = 0;
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ vendord_iso_ep_deactivate(rhport, p_vendor->iso_out_desc);
+ p_vendor->iso_out_desc = NULL;
+ p_vendor->ep_iso_out = 0;
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ vendord_iso_ep_deactivate(rhport, p_vendor->iso_in_desc);
+ p_vendor->iso_in_desc = NULL;
+ p_vendor->ep_iso_in = 0;
+ #endif
+ }
+ } else if (in_target_alt && desc_type == TUSB_DESC_ENDPOINT) {
+ const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;
+ const uint8_t ep_addr = desc_ep->bEndpointAddress;
+ const bool is_in = tu_edpt_dir(ep_addr) == TUSB_DIR_IN;
+ (void) is_in;
+
+ switch (desc_ep->bmAttributes.xfer) {
+ case TUSB_XFER_BULK:
+ // abort in-flight transfer + reset data toggle
+ usbd_edpt_stall(rhport, ep_addr);
+ usbd_edpt_clear_stall(rhport, ep_addr);
+ if (is_in) {
+ p_vendor->ep_in = ep_addr;
+ } else {
+ p_vendor->ep_out = ep_addr;
+ p_vendor->rx_xfer_len =
+ CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep);
+ #if CFG_TUD_VENDOR_RX_MANUAL_XFER == 0
+ TU_ASSERT(usbd_edpt_xfer(rhport, p_vendor->ep_out, _vendord_epbuf[idx].epout,
+ p_vendor->rx_xfer_len, false));
+ #endif
+ }
+ break;
+
+ #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT
+ case TUSB_XFER_INTERRUPT:
+ // stall/clear only for the enabled direction (an endpoint of a disabled
+ // direction was never opened, so must not be poked in the dcd)
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ if (is_in) {
+ usbd_edpt_stall(rhport, ep_addr);
+ usbd_edpt_clear_stall(rhport, ep_addr);
+ p_vendor->ep_int_in = ep_addr;
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ if (!is_in) {
+ usbd_edpt_stall(rhport, ep_addr);
+ usbd_edpt_clear_stall(rhport, ep_addr);
+ p_vendor->ep_int_out = ep_addr;
+ p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep);
+ }
+ #endif
+ break;
+ #endif
+
+ #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT
+ case TUSB_XFER_ISOCHRONOUS:
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ if (is_in) {
+ TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep));
+ p_vendor->ep_iso_in = ep_addr;
+ p_vendor->iso_in_desc = desc_ep; // points into p_itf_desc (static app descriptor)
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ if (!is_in) {
+ TU_ASSERT(vendord_iso_ep_activate(rhport, desc_ep));
+ p_vendor->ep_iso_out = ep_addr;
+ p_vendor->iso_out_desc = desc_ep; // points into p_itf_desc (static app descriptor)
+ p_vendor->iso_rx_xfer_len = tu_edpt_packet_size(desc_ep);
+ }
+ #endif
+ break;
+ #endif
+
+ default:
+ break; // unsupported endpoint type / direction gate disabled: ignore
+ }
+ }
+ p_desc = tu_desc_next(p_desc);
+ }
+
+ TU_VERIFY(alt_found); // unknown alt: endpoints were never cleared, current altsetting intact
+ p_vendor->cur_alt = alt;
+ return true;
+}
+
+uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) {
+ TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0);
+ const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len;
+
+ const uint8_t idx = find_vendor_itf(0);
+ TU_ASSERT(idx < CFG_TUD_VENDOR, 0);
+ vendord_interface_t *p_vendor = &_vendord_itf[idx];
+ p_vendor->rhport = rhport;
+ p_vendor->itf_num = desc_itf->bInterfaceNumber;
+ // p_itf_desc is assigned only after the parse succeeds: find_vendor_itf() treats a non-NULL
+ // p_itf_desc as an occupied slot, so setting it before a mid-parse TU_ASSERT could fail would
+ // leak the slot (a retry would find no free interface until the next bus reset).
+
+ // Consume every altsetting of this interface and hardware-open each endpoint exactly once
+ // (bulk/interrupt via usbd_edpt_open, isochronous FIFO-allocated; iso activation and the
+ // toggle reset happen on altsetting selection). An endpoint address that recurs in another
+ // altsetting must carry an identical configuration, since it is opened only on first sight;
+ // reconfiguring the same address per-alt is not supported and is rejected here.
+ uint8_t seen_type[CFG_TUD_ENDPPOINT_MAX][2];
+ uint16_t seen_mps[CFG_TUD_ENDPPOINT_MAX][2];
+ tu_memclr(seen_type, sizeof(seen_type)); // 0 == TUSB_XFER_CONTROL, never used as a data ep here
+ const uint8_t* p_desc = tu_desc_next(desc_itf);
+ while (tu_desc_in_bounds(p_desc, desc_end)) {
+ const uint8_t desc_type = tu_desc_type(p_desc);
+ if (desc_type == TUSB_DESC_INTERFACE_ASSOCIATION) {
+ break;
+ }
+ if (desc_type == TUSB_DESC_INTERFACE) {
+ if (((const tusb_desc_interface_t*)p_desc)->bInterfaceNumber != p_vendor->itf_num) {
+ break; // next interface
+ }
+ } else if (desc_type == TUSB_DESC_ENDPOINT) {
+ const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;
+ const uint8_t epnum = tu_edpt_number(desc_ep->bEndpointAddress);
+ const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
+ const uint8_t xfer = desc_ep->bmAttributes.xfer;
+ const uint16_t mps = tu_edpt_packet_size(desc_ep);
+ TU_ASSERT(epnum < CFG_TUD_ENDPPOINT_MAX, 0);
+
+ if (seen_type[epnum][dir] != 0) {
+ // reused address in a later altsetting: must be an exact match (opened only once)
+ TU_ASSERT(seen_type[epnum][dir] == xfer && seen_mps[epnum][dir] == mps, 0);
+ } else {
+ switch (xfer) {
+ case TUSB_XFER_BULK:
+ TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
+ break;
+
+ #if CFG_TUD_VENDOR_EP_INT_IN || CFG_TUD_VENDOR_EP_INT_OUT
+ case TUSB_XFER_INTERRUPT:
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ if (dir == TUSB_DIR_IN) {
+ TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0);
+ TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ if (dir == TUSB_DIR_OUT) {
+ TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0);
+ TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0);
+ }
+ #endif
+ break;
+ #endif
+
+ #if CFG_TUD_VENDOR_EP_ISO_IN || CFG_TUD_VENDOR_EP_ISO_OUT
+ case TUSB_XFER_ISOCHRONOUS:
+ #if CFG_TUD_VENDOR_EP_ISO_IN
+ if (dir == TUSB_DIR_IN) {
+ TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE, 0);
+ TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0);
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_ISO_OUT
+ if (dir == TUSB_DIR_OUT) {
+ TU_ASSERT(mps <= CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE, 0);
+ TU_ASSERT(vendord_iso_ep_alloc(rhport, desc_ep), 0);
+ }
+ #endif
+ break;
+ #endif
+
+ default:
+ break; // unsupported endpoint type / direction gate disabled: ignore
+ }
+ seen_type[epnum][dir] = xfer;
+ seen_mps[epnum][dir] = mps;
+ }
+ }
+ p_desc = tu_desc_next(p_desc);
+ }
+ // parse succeeded: commit the descriptor pointer (marks the slot occupied) before selecting alt 0
+ p_vendor->p_itf_desc = (const uint8_t*) desc_itf;
+ p_vendor->itf_desc_len = (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf);
+
+ // default altsetting active until the host selects another
+ TU_ASSERT(vendord_set_alt(rhport, idx, 0), 0);
+ return p_vendor->itf_desc_len;
+}
+
+#else // !CFG_TUD_VENDOR_ALT_SETTINGS
+
uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uint16_t max_len) {
TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass, 0);
const uint8_t* desc_end = (const uint8_t*)desc_itf + max_len;
@@ -275,6 +771,31 @@ uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uin
break; // end of this interface
} else if (desc_type == TUSB_DESC_ENDPOINT) {
const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc;
+
+ #if CFG_TUD_VENDOR_EP_INT_OUT || CFG_TUD_VENDOR_EP_INT_IN
+ if (desc_ep->bmAttributes.xfer == TUSB_XFER_INTERRUPT) {
+ const bool is_int_in = tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN;
+ (void) is_int_in;
+ #if CFG_TUD_VENDOR_EP_INT_IN
+ if (is_int_in) {
+ TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE, 0);
+ TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
+ p_vendor->ep_int_in = desc_ep->bEndpointAddress;
+ }
+ #endif
+ #if CFG_TUD_VENDOR_EP_INT_OUT
+ if (!is_int_in) {
+ TU_ASSERT(tu_edpt_packet_size(desc_ep) <= CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE, 0);
+ TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
+ p_vendor->ep_int_out = desc_ep->bEndpointAddress;
+ p_vendor->int_rx_xfer_len = tu_edpt_packet_size(desc_ep);
+ }
+ #endif
+ p_desc = tu_desc_next(p_desc);
+ continue;
+ }
+ #endif
+
TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
uint16_t rx_xfer_len = CFG_TUD_VENDOR_RX_NEED_ZLP ? CFG_TUD_VENDOR_RX_EPSIZE : tu_edpt_packet_size(desc_ep);
@@ -313,6 +834,40 @@ uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *desc_itf, uin
return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_itf);
}
+#endif // CFG_TUD_VENDOR_ALT_SETTINGS
+
+// Handle interface standard requests (GET/SET_INTERFACE when altsettings are enabled),
+// delegate everything else to the application callback as before.
+bool vendord_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) {
+#if CFG_TUD_VENDOR_ALT_SETTINGS
+ if (request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD &&
+ request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_INTERFACE) {
+ const uint8_t itf_num = tu_u16_low(request->wIndex);
+ uint8_t idx;
+ for (idx = 0; idx < CFG_TUD_VENDOR; idx++) {
+ if (_vendord_itf[idx].itf_num == itf_num && _vendord_itf[idx].p_itf_desc != NULL) {
+ break;
+ }
+ }
+ if (idx < CFG_TUD_VENDOR) {
+ if (request->bRequest == TUSB_REQ_SET_INTERFACE) {
+ if (stage == CONTROL_STAGE_SETUP) {
+ TU_VERIFY(vendord_set_alt(rhport, idx, tu_u16_low(request->wValue)));
+ return tud_control_status(rhport, request);
+ }
+ return true;
+ } else if (request->bRequest == TUSB_REQ_GET_INTERFACE) {
+ if (stage == CONTROL_STAGE_SETUP) {
+ return tud_control_xfer(rhport, request, &_vendord_itf[idx].cur_alt, 1);
+ }
+ return true;
+ }
+ }
+ }
+#endif
+ return tud_vendor_control_xfer_cb(rhport, stage, request);
+}
+
bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) {
(void)rhport;
(void)result;
@@ -320,6 +875,33 @@ bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint
TU_VERIFY(idx < CFG_TUD_VENDOR);
vendord_interface_t *p_vendor = &_vendord_itf[idx];
+#if CFG_TUD_VENDOR_EP_INT_OUT
+ if (ep_addr == p_vendor->ep_int_out) {
+ // not re-armed automatically: application calls tud_vendor_n_int_read_xfer()
+ tud_vendor_int_rx_cb(idx, _vendord_int_epbuf[idx].int_out, xferred_bytes);
+ return true;
+ }
+#endif
+#if CFG_TUD_VENDOR_EP_INT_IN
+ if (ep_addr == p_vendor->ep_int_in) {
+ tud_vendor_int_tx_cb(idx, xferred_bytes);
+ return true;
+ }
+#endif
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+ if (ep_addr == p_vendor->ep_iso_out) {
+ // not re-armed automatically: application calls tud_vendor_n_iso_read_xfer()
+ tud_vendor_iso_rx_cb(idx, _vendord_iso_epbuf[idx].iso_out, xferred_bytes);
+ return true;
+ }
+#endif
+#if CFG_TUD_VENDOR_EP_ISO_IN
+ if (ep_addr == p_vendor->ep_iso_in) {
+ tud_vendor_iso_tx_cb(idx, xferred_bytes);
+ return true;
+ }
+#endif
+
#if CFG_TUD_VENDOR_TXRX_BUFFERED
if (ep_addr == p_vendor->rx_stream.ep_addr) {
// Put received data to FIFO
diff --git a/src/class/vendor/vendor_device.h b/src/class/vendor/vendor_device.h
index 32216f25f..ce33e2f62 100644
--- a/src/class/vendor/vendor_device.h
+++ b/src/class/vendor/vendor_device.h
@@ -60,6 +60,68 @@ extern "C" {
#define CFG_TUD_VENDOR_RX_NEED_ZLP 0
#endif
+// Enable support for an optional interrupt OUT / interrupt IN endpoint in the vendor
+// interface, each direction gated separately. Interrupt endpoints are non-buffered:
+// OUT is armed manually one packet at a time with tud_vendor_n_int_read_xfer() (data
+// delivered via tud_vendor_int_rx_cb), IN is a direct transfer via tud_vendor_n_int_write().
+#ifndef CFG_TUD_VENDOR_EP_INT_OUT
+ #define CFG_TUD_VENDOR_EP_INT_OUT 0
+#endif
+
+#ifndef CFG_TUD_VENDOR_EP_INT_IN
+ #define CFG_TUD_VENDOR_EP_INT_IN 0
+#endif
+
+// Buffer sizes for interrupt endpoint transfers, must be >= the endpoint max packet size
+#ifndef CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE
+ #define CFG_TUD_VENDOR_EP_INT_OUT_BUFSIZE 64
+#endif
+
+#ifndef CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE
+ #define CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE 64
+#endif
+
+// Enable support for an optional isochronous OUT / IN endpoint, each direction gated
+// separately, with the same non-buffered API shape as the interrupt pair. Isochronous
+// endpoints must not claim bandwidth in the default altsetting (USB 2.0 5.6.3): place
+// them in a non-zero altsetting and enable CFG_TUD_VENDOR_ALT_SETTINGS.
+#ifndef CFG_TUD_VENDOR_EP_ISO_OUT
+ #define CFG_TUD_VENDOR_EP_ISO_OUT 0
+#endif
+
+#ifndef CFG_TUD_VENDOR_EP_ISO_IN
+ #define CFG_TUD_VENDOR_EP_ISO_IN 0
+#endif
+
+// Buffer sizes for isochronous endpoint transfers, must be >= the endpoint max packet size
+#ifndef CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE
+ #define CFG_TUD_VENDOR_EP_ISO_OUT_BUFSIZE 64
+#endif
+
+#ifndef CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE
+ #define CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE 64
+#endif
+
+// Enable alternate-setting support: the vendor interface may carry multiple altsettings,
+// each with its own endpoint set. GET_INTERFACE is answered and SET_INTERFACE performed by
+// closing the current altsetting's endpoints and opening the requested one's (isochronous
+// endpoints are FIFO-allocated at open and activated on selection). The configuration
+// descriptor must stay valid while mounted (static, the usual TinyUSB pattern).
+// Non-buffered mode only.
+#ifndef CFG_TUD_VENDOR_ALT_SETTINGS
+ #define CFG_TUD_VENDOR_ALT_SETTINGS 0
+#endif
+
+#if CFG_TUD_VENDOR_ALT_SETTINGS && CFG_TUD_VENDOR_TXRX_BUFFERED
+ #error CFG_TUD_VENDOR_ALT_SETTINGS requires non-buffered mode (CFG_TUD_VENDOR_RX/TX_BUFSIZE = 0)
+#endif
+
+// An isochronous endpoint must not claim bandwidth in the default altsetting (USB 2.0 5.6.3),
+// so it can only live in a non-zero altsetting, which requires alternate-setting support.
+#if (CFG_TUD_VENDOR_EP_ISO_OUT || CFG_TUD_VENDOR_EP_ISO_IN) && !CFG_TUD_VENDOR_ALT_SETTINGS
+ #error CFG_TUD_VENDOR_EP_ISO_OUT/IN requires CFG_TUD_VENDOR_ALT_SETTINGS
+#endif
+
//--------------------------------------------------------------------+
// Application API (Multiple Interfaces) i.e CFG_TUD_VENDOR > 1
//--------------------------------------------------------------------+
@@ -107,6 +169,43 @@ TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_n_write_str(uint8_t idx,
return tud_vendor_n_write(idx, str, strlen(str));
}
+//------------- Interrupt endpoints -------------//
+#if CFG_TUD_VENDOR_EP_INT_OUT
+// Arm the interrupt OUT endpoint for one packet, return false if a transfer is still ongoing.
+// Received data is delivered via tud_vendor_int_rx_cb(); re-arm from the callback or by polling.
+bool tud_vendor_n_int_read_xfer(uint8_t idx);
+#endif
+
+#if CFG_TUD_VENDOR_EP_INT_IN
+// Send on the interrupt IN endpoint (direct transfer, up to CFG_TUD_VENDOR_EP_INT_IN_BUFSIZE
+// bytes). Returns number of bytes queued, 0 if the endpoint is busy or not opened.
+uint32_t tud_vendor_n_int_write(uint8_t idx, const void *buffer, uint32_t bufsize);
+
+// Return available bytes for interrupt IN write: 0 while busy, else the buffer size
+uint32_t tud_vendor_n_int_write_available(uint8_t idx);
+#endif
+
+//------------- Isochronous endpoints -------------//
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+// Arm the isochronous OUT endpoint for one packet, return false if a transfer is still ongoing.
+// Received data is delivered via tud_vendor_iso_rx_cb(); re-arm from the callback or by polling.
+bool tud_vendor_n_iso_read_xfer(uint8_t idx);
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_IN
+// Send on the isochronous IN endpoint (direct transfer, up to CFG_TUD_VENDOR_EP_ISO_IN_BUFSIZE
+// bytes). Returns number of bytes queued, 0 if the endpoint is busy or not opened.
+uint32_t tud_vendor_n_iso_write(uint8_t idx, const void *buffer, uint32_t bufsize);
+
+// Return available bytes for isochronous IN write: 0 while busy, else the buffer size
+uint32_t tud_vendor_n_iso_write_available(uint8_t idx);
+#endif
+
+#if CFG_TUD_VENDOR_ALT_SETTINGS
+// Return the currently selected alternate setting
+uint8_t tud_vendor_n_alt(uint8_t idx);
+#endif
+
// backward compatible
#define tud_vendor_n_flush(idx) tud_vendor_n_write_flush(idx)
@@ -161,6 +260,44 @@ TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_write_available(void) {
return tud_vendor_n_write_available(0);
}
+#if CFG_TUD_VENDOR_EP_INT_OUT
+TU_ATTR_ALWAYS_INLINE static inline bool tud_vendor_int_read_xfer(void) {
+ return tud_vendor_n_int_read_xfer(0);
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_INT_IN
+TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_int_write(const void *buffer, uint32_t bufsize) {
+ return tud_vendor_n_int_write(0, buffer, bufsize);
+}
+
+TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_int_write_available(void) {
+ return tud_vendor_n_int_write_available(0);
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+TU_ATTR_ALWAYS_INLINE static inline bool tud_vendor_iso_read_xfer(void) {
+ return tud_vendor_n_iso_read_xfer(0);
+}
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_IN
+TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_iso_write(const void *buffer, uint32_t bufsize) {
+ return tud_vendor_n_iso_write(0, buffer, bufsize);
+}
+
+TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_vendor_iso_write_available(void) {
+ return tud_vendor_n_iso_write_available(0);
+}
+#endif
+
+#if CFG_TUD_VENDOR_ALT_SETTINGS
+TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_vendor_alt(void) {
+ return tud_vendor_n_alt(0);
+}
+#endif
+
// backward compatible
#define tud_vendor_flush() tud_vendor_write_flush()
@@ -176,6 +313,29 @@ void tud_vendor_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize);
// Invoked when tx transfer is finished
void tud_vendor_tx_cb(uint8_t idx, uint32_t sent_bytes);
+#if CFG_TUD_VENDOR_EP_INT_OUT
+// Invoked when data is received on the interrupt OUT endpoint. The endpoint is not
+// re-armed automatically: call tud_vendor_n_int_read_xfer() to receive more.
+void tud_vendor_int_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize);
+#endif
+
+#if CFG_TUD_VENDOR_EP_INT_IN
+// Invoked when an interrupt IN transfer is finished
+void tud_vendor_int_tx_cb(uint8_t idx, uint32_t sent_bytes);
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_OUT
+// Invoked when data is received on the isochronous OUT endpoint. The endpoint is not
+// re-armed automatically: call tud_vendor_n_iso_read_xfer() to receive more.
+void tud_vendor_iso_rx_cb(uint8_t idx, const uint8_t *buffer, uint32_t bufsize);
+#endif
+
+#if CFG_TUD_VENDOR_EP_ISO_IN
+// Invoked when an isochronous IN transfer is finished (result may be a missed frame:
+// the data was not necessarily taken by the host, re-arm regardless)
+void tud_vendor_iso_tx_cb(uint8_t idx, uint32_t sent_bytes);
+#endif
+
//--------------------------------------------------------------------+
// Internal Class Driver API
//--------------------------------------------------------------------+
@@ -183,6 +343,7 @@ void vendord_init(void);
bool vendord_deinit(void);
void vendord_reset(uint8_t rhport);
uint16_t vendord_open(uint8_t rhport, const tusb_desc_interface_t *idx_desc, uint16_t max_len);
+bool vendord_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request);
bool vendord_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes);
#ifdef __cplusplus
diff --git a/src/common/tusb_mcu.h b/src/common/tusb_mcu.h
index 1f2afb03a..0959ac3a9 100644
--- a/src/common/tusb_mcu.h
+++ b/src/common/tusb_mcu.h
@@ -141,7 +141,6 @@
#elif TU_CHECK_MCU(OPT_MCU_NRF5X)
// 8 CBI + 1 ISO
#define TUP_DCD_ENDPOINT_MAX 9
- #define TUP_DCD_EDPT_CLOSE_API
#elif TU_CHECK_MCU(OPT_MCU_NRF54)
#define TUP_USBIP_DWC2
@@ -741,11 +740,9 @@
#define TU_ATTR_FAST_FUNC
#endif
-#if defined(TUP_USBIP_IP3511) || defined(TUP_USBIP_RUSB2)
- #define TUP_DCD_EDPT_CLOSE_API
-#endif
-
-// USBIP implement dcd_edpt_close() and does not support ISO alloc & activate API
+// TUP_DCD_EDPT_CLOSE_API is deprecated: these USBIPs implement dcd_edpt_close() and lack the
+// ISO alloc & activate API. IP3511, RUSB2 and NRF5X have been migrated to ISO_ALLOC; the remaining
+// CLOSE_API MCUs (mm32, pic, da1469x, f1c100s, ch32-usbhs) are pending per-board verification.
#ifndef TUP_DCD_EDPT_CLOSE_API
#define TUP_DCD_EDPT_ISO_ALLOC
#endif
diff --git a/src/device/usbd.c b/src/device/usbd.c
index 7a6e13f8d..5471e132d 100644
--- a/src/device/usbd.c
+++ b/src/device/usbd.c
@@ -256,7 +256,7 @@ static const usbd_class_driver_t _usbd_driver[] = {
.deinit = vendord_deinit,
.reset = vendord_reset,
.open = vendord_open,
- .control_xfer_cb = tud_vendor_control_xfer_cb,
+ .control_xfer_cb = vendord_control_xfer_cb,
.xfer_cb = vendord_xfer_cb,
.xfer_isr = NULL,
.sof = NULL
@@ -418,6 +418,7 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(dcd_event_t const * event,
//--------------------------------------------------------------------+
static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes);
static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request);
+static bool process_get_status(uint8_t rhport, tusb_control_request_t const * request, uint16_t status);
static bool process_set_config(uint8_t rhport, uint8_t cfg_num);
static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const * p_request);
@@ -1041,9 +1042,7 @@ static bool process_std_device_request(uint8_t rhport, tusb_control_request_t co
// Device status bit mask
// - Bit 0: Self Powered TODO must invoke callback to get actual status
// - Bit 1: Remote Wakeup enabled
- uint16_t status = (uint16_t) _usbd_dev.dev_state_bm;
- tud_control_xfer(rhport, p_request, &status, 2);
- return true;
+ return process_get_status(rhport, p_request, (uint16_t) _usbd_dev.dev_state_bm);
}
default:
@@ -1053,6 +1052,14 @@ static bool process_std_device_request(uint8_t rhport, tusb_control_request_t co
}
+// Reply to a standard GET_STATUS (device/interface/endpoint) with its 2-byte status word.
+// GET_STATUS is Device-to-host only; reject a mis-directed (OUT) request rather than handing
+// usbd the address of a stack local to write host data into after this frame has returned.
+static bool process_get_status(uint8_t rhport, tusb_control_request_t const * request, uint16_t status) {
+ TU_VERIFY(request->bmRequestType_bit.direction == TUSB_DIR_IN);
+ return tud_control_xfer(rhport, request, &status, 2);
+}
+
// This handles the actual request and its response.
// Returns false if unable to complete the request, causing caller to stall control endpoints.
static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request) {
@@ -1150,9 +1157,18 @@ static bool process_setup_received(uint8_t rhport, tusb_control_request_t const
}
case TUSB_REQ_SET_INTERFACE:
+ // A class that implements altsettings handles SET_INTERFACE itself and returns true,
+ // so reaching here means the class does not — where only alt 0 is valid. Any non-zero
+ // alt (unimplemented, or rejected as invalid by the class) is a Request Error (stall).
+ TU_VERIFY(tu_u16_low(p_request->wValue) == 0);
tud_control_status(rhport, p_request);
break;
+ case TUSB_REQ_GET_STATUS:
+ // USB 2.0 9.4.5: interface GET_STATUS returns 2 reserved (zero) bytes
+ TU_VERIFY(process_get_status(rhport, p_request, 0x0000));
+ break;
+
default: return false;
}
}
@@ -1175,35 +1191,34 @@ static bool process_setup_received(uint8_t rhport, tusb_control_request_t const
} else {
// Handle STD request to endpoint
switch (p_request->bRequest) { //-V2520
- case TUSB_REQ_GET_STATUS: {
- uint16_t status = usbd_edpt_stalled(rhport, ep_addr) ? 0x0001u : 0x0000u;
- tud_control_xfer(rhport, p_request, &status, 2);
- }
- break;
+ case TUSB_REQ_GET_STATUS:
+ // USB 2.0 9.4.5: endpoint GET_STATUS bit 0 = Halt
+ TU_VERIFY(process_get_status(rhport, p_request, usbd_edpt_stalled(rhport, ep_addr) ? 0x0001u : 0x0000u));
+ break;
case TUSB_REQ_CLEAR_FEATURE:
case TUSB_REQ_SET_FEATURE: {
- if ( TUSB_REQ_FEATURE_EDPT_HALT == p_request->wValue ) {
- if ( TUSB_REQ_CLEAR_FEATURE == p_request->bRequest ) {
- usbd_edpt_clear_stall(rhport, ep_addr);
- }else {
- usbd_edpt_stall(rhport, ep_addr);
- }
- }
+ // ENDPOINT_HALT is the only endpoint feature; it exists only on a non-control endpoint
+ // that an interface actually owns. Any other selector, the control endpoint (EP0 has no
+ // Halt feature, USB 2.0 9.4.9), or an endpoint no driver owns is a Request Error (stall).
+ TU_VERIFY(TUSB_REQ_FEATURE_EDPT_HALT == p_request->wValue);
+ TU_VERIFY(ep_num != 0);
+ TU_VERIFY(driver != NULL);
- if (driver != NULL) {
- // Some classes such as USBTMC needs to clear/re-init its buffer when receiving CLEAR_FEATURE request
- // We will also forward std request targeted endpoint to class drivers as well
+ if ( TUSB_REQ_CLEAR_FEATURE == p_request->bRequest ) {
+ usbd_edpt_clear_stall(rhport, ep_addr);
+ } else {
+ usbd_edpt_stall(rhport, ep_addr);
+ }
- // STD request must always be ACKed regardless of driver returned value
- // Also clear complete callback if driver set since it can also stall the request.
- (void) invoke_class_control(rhport, driver, p_request);
- ctrl_xfer->complete_cb = NULL;
+ // Some classes such as USBTMC need to clear/re-init their buffer on CLEAR_FEATURE.
+ // Clear complete callback if driver set since it can also stall the request.
+ (void) invoke_class_control(rhport, driver, p_request);
+ ctrl_xfer->complete_cb = NULL;
- // skip ZLP status if driver already did that
- if (!(_usbd_dev.ep_status[0][TUSB_DIR_IN] & TU_EDPT_STATE_BUSY)) {
- tud_control_status(rhport, p_request);
- }
+ // STD request must always be ACKed; skip ZLP status if driver already did that.
+ if (!(_usbd_dev.ep_status[0][TUSB_DIR_IN] & TU_EDPT_STATE_BUSY)) {
+ tud_control_status(rhport, p_request);
}
}
break;
@@ -1648,10 +1663,21 @@ void usbd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
uint8_t const epnum = tu_edpt_number(ep_addr);
uint8_t const dir = tu_edpt_dir(ep_addr);
- // only clear if currently stalled
TU_LOG_USBD(" Clear Stall EP %02X\r\n", ep_addr);
+ const bool was_stalled = (_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_STALLED) != 0;
dcd_edpt_clear_stall(rhport, ep_addr);
- _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY);
+ // Clear STALLED|BUSY unconditionally (long-standing behavior; some classes, e.g. audio's
+ // set-interface, call this on a non-stalled endpoint solely to drop a leftover BUSY bit).
+ // Only release the CLAIMED ownership bit when the endpoint was actually stalled: the stall
+ // aborts the in-flight transfer in the dcd with no completion event to release the claim, so
+ // clearing it here prevents starvation. On a non-stalled clear (e.g. a data-toggle reset) a
+ // transfer may still be legitimately claimed by another task, so keep CLAIMED to preserve the
+ // claim->xfer mutual exclusion.
+ uint8_t clear_mask = TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY;
+ if (was_stalled) {
+ clear_mask |= TU_EDPT_STATE_CLAIMED;
+ }
+ _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~clear_mask;
}
bool usbd_edpt_stalled(uint8_t rhport, uint8_t ep_addr) {
diff --git a/src/portable/chipidea/ci_hs/dcd_ci_hs.c b/src/portable/chipidea/ci_hs/dcd_ci_hs.c
index 55906e678..fa98d6882 100644
--- a/src/portable/chipidea/ci_hs/dcd_ci_hs.c
+++ b/src/portable/chipidea/ci_hs/dcd_ci_hs.c
@@ -393,7 +393,8 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
ci_hs_regs_t *dcd_reg = CI_HS_REG(rhport);
dcd_reg->ENDPTCTRL[epnum] |= ENDPTCTRL_STALL << (dir ? 16 : 0);
- // flush to abort any primed buffer
+ // flush to abort any primed buffer; the aborted transfer's dQH overlay can be left
+ // ACTIVE with mid-transfer state - qhd_start_xfer clears it before the next prime
dcd_reg->ENDPTFLUSH = TU_BIT(epnum + (dir ? 16 : 0));
}
@@ -497,6 +498,7 @@ static void qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir) {
dcd_qtd_t *p_qtd = &_dcd_data.qtd[epnum][dir];
p_qhd->qtd_overlay.halted = false; // clear any previous error
+ p_qhd->qtd_overlay.active = false; // a flushed prime leaves stale ACTIVE state; clear it so the fresh qtd loads
p_qhd->qtd_overlay.next = (uint32_t)p_qtd; // link qtd to qhd
// flush cache
diff --git a/src/portable/mentor/musb/dcd_musb.c b/src/portable/mentor/musb/dcd_musb.c
index 1ebd1fe02..17993f23a 100644
--- a/src/portable/mentor/musb/dcd_musb.c
+++ b/src/portable/mentor/musb/dcd_musb.c
@@ -292,6 +292,12 @@ static void process_epin_isr(uint8_t rhport, musb_regs_t *musb_regs, uint8_t epn
}
pipe_state_t* pipe = pipe_get(epnum, TUSB_DIR_IN);
+ // No active transfer: a halt/abort disarmed the pipe (armed=false) but may leave remaining>0.
+ // Do not keep loading the aborted transfer — that would re-fill the just-flushed FIFO and the
+ // next (re-armed) transfer's data would stack on top (host sees an oversized packet -> babble).
+ if (!pipe->armed) {
+ return;
+ }
if (pipe->remaining > 0) {
pipe_write(musb_regs, pipe, epnum);
} else {
@@ -910,6 +916,10 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
} else {
const tusb_dir_t ep_dir = tu_edpt_dir(ep_addr);
const uint8_t is_rx = (ep_dir == TUSB_DIR_OUT ? 1u : 0u);
+ // A halt aborts the transfer: flush staged FIFO packet(s) before stalling, else leftover TX data
+ // concatenates with the next transfer after un-halt -> host sees an oversized packet (babble).
+ // FLUSH must precede SEND_STALL, which clears the TXRDY that hwfifo_flush() gates on.
+ hwfifo_flush(musb_regs, epn, is_rx, false);
ep_csr->maxp_csr[is_rx].csrl = MUSB_CSRL_SEND_STALL(is_rx);
pipe_state_t* pipe = pipe_get(epn, ep_dir);
pipe->armed = false;
diff --git a/src/portable/microchip/samd/dcd_samd.c b/src/portable/microchip/samd/dcd_samd.c
index 32ddd3422..54ef34c8e 100644
--- a/src/portable/microchip/samd/dcd_samd.c
+++ b/src/portable/microchip/samd/dcd_samd.c
@@ -229,15 +229,49 @@ bool dcd_edpt_open (uint8_t rhport, tusb_desc_endpoint_t const * desc_edpt)
bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
(void) rhport;
- (void) ep_addr;
- (void)largest_packet_size;
- return false;
+ uint8_t const epnum = tu_edpt_number(ep_addr);
+ uint8_t const dir = tu_edpt_dir(ep_addr);
+
+ // Reserve the endpoint bank with the largest packet size (persists across altsettings). The
+ // buffer address/count are filled per-transfer in dcd_edpt_xfer; only the SIZE bucket is fixed.
+ UsbDeviceDescBank* bank = &sram_registers[epnum][dir];
+ uint32_t size_value = 0;
+ while (size_value < 7) {
+ if (1 << (size_value + 3) >= largest_packet_size) {
+ break;
+ }
+ size_value++;
+ }
+ if ( size_value == 7 && largest_packet_size > 1023 ) return false;
+
+ bank->PCKSIZE.bit.SIZE = size_value;
+ return true;
}
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
(void)rhport;
- (void)desc_ep;
- return false;
+ uint8_t const epnum = tu_edpt_number(desc_ep->bEndpointAddress);
+ uint8_t const dir = tu_edpt_dir(desc_ep->bEndpointAddress);
+
+ // Configure and enable the ISO endpoint on altsetting selection (bank SIZE already reserved by
+ // dcd_edpt_iso_alloc). Mirrors the per-direction setup in dcd_edpt_open(), plus a bank scrub:
+ // under ISO_ALLOC the EP is never disabled on alt0 (usbd_edpt_close is a no-op), so the bank-ready
+ // state from the previous streaming session survives into re-activation. Leave the EP un-armed so
+ // a stale bank can't move a packet before dcd_edpt_xfer re-arms it (a leftover BK1RDY with a stale
+ // BYTE_COUNT would otherwise babble on the first IN token after re-selecting alt1).
+ UsbDeviceEndpoint* ep = &USB->DEVICE.DeviceEndpoint[epnum];
+ if ( dir == TUSB_DIR_OUT ) {
+ ep->EPCFG.bit.EPTYPE0 = desc_ep->bmAttributes.xfer + 1;
+ ep->EPSTATUSCLR.reg = USB_DEVICE_EPSTATUSCLR_STALLRQ0 | USB_DEVICE_EPSTATUSCLR_DTGLOUT;
+ ep->EPSTATUSSET.reg = USB_DEVICE_EPSTATUSSET_BK0RDY; // OUT: not ready to receive until armed
+ ep->EPINTENSET.bit.TRCPT0 = true;
+ } else {
+ ep->EPCFG.bit.EPTYPE1 = desc_ep->bmAttributes.xfer + 1;
+ ep->EPSTATUSCLR.reg = USB_DEVICE_EPSTATUSCLR_STALLRQ1 | USB_DEVICE_EPSTATUSCLR_DTGLIN |
+ USB_DEVICE_EPSTATUSCLR_BK1RDY; // IN: clear stale "loaded" bank
+ ep->EPINTENSET.bit.TRCPT1 = true;
+ }
+ return true;
}
void dcd_edpt_close_all (uint8_t rhport)
diff --git a/src/portable/nordic/nrf5x/dcd_nrf5x.c b/src/portable/nordic/nrf5x/dcd_nrf5x.c
index dc85ff78e..5170e0645 100644
--- a/src/portable/nordic/nrf5x/dcd_nrf5x.c
+++ b/src/portable/nordic/nrf5x/dcd_nrf5x.c
@@ -122,8 +122,22 @@ TU_ATTR_ALWAYS_INLINE static inline bool is_in_isr(void) {
return (SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) ? true : false;
}
+// Errata 199 "USBD cannot receive tasks during DMA": while an EasyDMA transfer is in progress the
+// controller may drop an incoming SETUP/IN/OUT token (lost event -> stuck EP0, esp. under rapid
+// back-to-back control transfers). The workaround latches an undocumented "DMA in progress" test
+// register (0x40027C1C) so tokens are held instead. Gated on the anomaly being present (all
+// nRF52840 revisions; absent on other nRF52 parts). Mirrors nrfx usbd_dma_pending_set/clear().
+#define NRF_USBD_ERRATA_199_REG (*((volatile uint32_t*) 0x40027C1CUL))
+
// helper to start DMA
static void start_dma(volatile uint32_t* reg_startep) {
+ // EP0STATUS / EP0RCVOUT take the EasyDMA slot but do not transfer data, so no ERRATA-199 latch.
+ const bool no_dma = (reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT);
+
+ if (!no_dma && nrf52_errata_199()) {
+ NRF_USBD_ERRATA_199_REG = 0x00000082UL;
+ }
+
(*reg_startep) = 1;
__ISB();
__DSB();
@@ -131,7 +145,7 @@ static void start_dma(volatile uint32_t* reg_startep) {
// TASKS_EP0STATUS, TASKS_EP0RCVOUT seem to need EasyDMA to be available
// However these don't trigger any DMA transfer and got ENDED event subsequently
// Therefore dma_pending is corrected right away
- if ((reg_startep == &NRF_USBD->TASKS_EP0STATUS) || (reg_startep == &NRF_USBD->TASKS_EP0RCVOUT)) {
+ if (no_dma) {
atomic_flag_clear(&_dcd.dma_running);
}
}
@@ -146,6 +160,10 @@ static void edpt_dma_start(volatile uint32_t* reg_startep) {
// DMA is complete
static void edpt_dma_end(void) {
+ // Clear the ERRATA-199 "DMA in progress" latch set in start_dma().
+ if (nrf52_errata_199()) {
+ NRF_USBD_ERRATA_199_REG = 0x00000000UL;
+ }
atomic_flag_clear(&_dcd.dma_running);
}
@@ -377,58 +395,52 @@ void dcd_edpt_close_all(uint8_t rhport) {
dcd_int_enable(rhport);
}
-void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) {
- (void) rhport;
+bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
+ (void)rhport;
+ (void)largest_packet_size;
+ // nRF ISO endpoints are hardware-fixed to EP8 and use EasyDMA, so there is no packet buffer to
+ // pre-allocate here; the endpoint is enabled on dcd_edpt_iso_activate().
+ TU_ASSERT(tu_edpt_number(ep_addr) == EP_ISO_NUM);
+ return true;
+}
+bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
+ (void)rhport;
+ uint8_t const ep_addr = desc_ep->bEndpointAddress;
uint8_t const epnum = tu_edpt_number(ep_addr);
uint8_t const dir = tu_edpt_dir(ep_addr);
+ TU_ASSERT(epnum == EP_ISO_NUM);
- if (epnum != EP_ISO_NUM) {
- // CBI
- if (dir == TUSB_DIR_OUT) {
- NRF_USBD->INTENCLR = TU_BIT(USBD_INTEN_ENDEPOUT0_Pos + epnum);
- NRF_USBD->EPOUTEN &= ~TU_BIT(epnum);
- } else {
- NRF_USBD->INTENCLR = TU_BIT(USBD_INTEN_ENDEPIN0_Pos + epnum);
- NRF_USBD->EPINEN &= ~TU_BIT(epnum);
- }
+ // A transfer armed before SET_INTERFACE survives to here (this port has no dcd close); usbd has
+ // just reset the endpoint's claim/busy state, so drop the stale descriptor too — otherwise the
+ // class's next arm trips TU_ASSERT(!xfer->started) in dcd_edpt_xfer().
+ _dcd.xfer[epnum][dir].started = false;
+ _dcd.xfer[epnum][dir].data_received = false;
+ _dcd.xfer[epnum][dir].iso_in_transfer_ready = false;
+
+ _dcd.xfer[epnum][dir].mps = tu_edpt_packet_size(desc_ep);
+
+ if (dir == TUSB_DIR_OUT) {
+ // SPLIT ISO buffer when the ISO IN endpoint is already active.
+ if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_IN].mps) NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_HalfIN;
+ NRF_USBD->EVENTS_ENDISOOUT = 0;
+ if ((NRF_USBD->INTEN & USBD_INTEN_SOF_Msk) == 0) NRF_USBD->EVENTS_SOF = 0;
+ NRF_USBD->INTENSET = USBD_INTENSET_ENDISOOUT_Msk | USBD_INTENSET_SOF_Msk;
+ NRF_USBD->EPOUTEN |= USBD_EPOUTEN_ISOOUT_Msk;
} else {
- _dcd.xfer[EP_ISO_NUM][dir].mps = 0;
- // ISO
- if (dir == TUSB_DIR_OUT) {
- NRF_USBD->INTENCLR = USBD_INTENCLR_ENDISOOUT_Msk;
- NRF_USBD->EPOUTEN &= ~USBD_EPOUTEN_ISOOUT_Msk;
- NRF_USBD->EVENTS_ENDISOOUT = 0;
- } else {
- NRF_USBD->INTENCLR = USBD_INTENCLR_ENDISOIN_Msk;
- NRF_USBD->EPINEN &= ~USBD_EPINEN_ISOIN_Msk;
- }
- // One of the ISO endpoints closed, no need to split buffers any more.
- NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_OneDir;
- // When both ISO endpoint are close there is no need for SOF any more.
- if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_IN].mps + _dcd.xfer[EP_ISO_NUM][TUSB_DIR_OUT].mps == 0)
- NRF_USBD->INTENCLR = USBD_INTENCLR_SOF_Msk;
+ NRF_USBD->EVENTS_ENDISOIN = 0;
+ // SPLIT ISO buffer when the ISO OUT endpoint is already active.
+ if (_dcd.xfer[EP_ISO_NUM][TUSB_DIR_OUT].mps) NRF_USBD->ISOSPLIT = USBD_ISOSPLIT_SPLIT_HalfIN;
+ if ((NRF_USBD->INTEN & USBD_INTEN_SOF_Msk) == 0) NRF_USBD->EVENTS_SOF = 0;
+ NRF_USBD->INTENSET = USBD_INTENSET_ENDISOIN_Msk | USBD_INTENSET_SOF_Msk;
+ NRF_USBD->EPINEN |= USBD_EPINEN_ISOIN_Msk;
}
- _dcd.xfer[epnum][dir].started = false;
+
__ISB();
__DSB();
+ return true;
}
-#if 0
-bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
- (void)rhport;
- (void)ep_addr;
- (void)largest_packet_size;
- return false;
-}
-
-bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
- (void)rhport;
- (void)desc_ep;
- return false;
-}
-#endif
-
bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t total_bytes, bool is_isr) {
(void) rhport;
(void) is_isr;
diff --git a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
index aa0307d25..d5b03e4b1 100644
--- a/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
+++ b/src/portable/nxp/lpc_ip3511/dcd_lpc_ip3511.c
@@ -158,6 +158,10 @@ typedef struct
// - 55 usb0 (FS) has 5x2 endpoints, usb1 (HS) has 6x2 endpoints
#define MAX_EP_PAIRS 6
+// Bounded spin waiting for hardware to clear an EPSKIP bit when retiring a still-armed endpoint on
+// reopen (dcd_edpt_open). Hardware clears it within a (micro)frame; the guard only avoids a hang.
+#define IP3511_EPSKIP_SPIN 100000u
+
// NOTE data will be transferred as soon as dcd get request by dcd_pipe(_queue)_xfer using double buffering.
// current_td is used to keep track of number of remaining & xferred bytes of the current request.
typedef struct
@@ -337,13 +341,37 @@ void dcd_sof_enable(uint8_t rhport, bool en)
//--------------------------------------------------------------------+
// DCD Endpoint Port
//--------------------------------------------------------------------+
+// Retire a still-armed (Active) endpoint before reconfiguring it (reopen across SET_INTERFACE).
+// UM11126 §41.7.6/§41.8.3: write EPSKIP and wait for hardware to clear the bit, then Active is
+// safe to clear. EPSKIP raises the endpoint interrupt as it clears Active, delivered as a
+// (partial) transfer completion. Here that is sanctioned — usbd_edpt_close() documents "in
+// progress transfers may be delivered after this call", and that completion is what clears the
+// stale usbd busy flag (ISO_ALLOC close is a no-op) so the class can re-arm the reopened
+// endpoint. NOT for the stall/iso-activate paths: there the class re-arms from the completion
+// callback and the endpoint ends up Active+Stall, which never sends a STALL handshake (usbtest
+// case 13 regression on LPC11u37) — those paths must clear Active directly instead.
+// Bounded: hardware clears EPSKIP within a (micro)frame.
+static void edpt_skip_active(uint8_t rhport, uint8_t ep_id) {
+ ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id);
+ if ( ep_cs[0].cmd_sts.active || ep_cs[1].cmd_sts.active ) {
+ dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
+ dcd_reg->EPSKIP |= TU_BIT(ep_id);
+ uint32_t guard = IP3511_EPSKIP_SPIN;
+ while ( (dcd_reg->EPSKIP & TU_BIT(ep_id)) && guard-- ) {}
+ }
+ ep_cs[0].cmd_sts.active = ep_cs[1].cmd_sts.active = 0;
+}
+
void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr)
{
(void) rhport;
-
// TODO cannot able to STALL Control OUT endpoint !!!!! FIXME try some walk-around
uint8_t const ep_id = ep_addr2id(ep_addr);
- _dcd.ep[ep_id][0].cmd_sts.stall = 1;
+ // Clear Active directly before setting Stall (no EPSKIP — see edpt_skip_active): the hardware
+ // services an armed buffer instead of returning STALL, so a halt requested while a transfer is
+ // queued would not actually stall the endpoint (usbtest case 13).
+ _dcd.ep[ep_id][0].cmd_sts.active = 0;
+ _dcd.ep[ep_id][0].cmd_sts.stall = 1;
}
void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr)
@@ -362,9 +390,15 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
//------------- Prepare Queue Head -------------//
uint8_t ep_id = ep_addr2id(p_endpoint_desc->bEndpointAddress);
ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id);
+ dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
- // Check if endpoint is available
- TU_ASSERT( ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable );
+ // usbd_edpt_close() is a no-op on ISO_ALLOC ports, so an endpoint a class closed then reopened
+ // across SET_INTERFACE (e.g. the video notification or audio streaming endpoint) is still armed
+ // here rather than disabled. Retire it (edpt_skip_active) before reconfiguring.
+ if ( !(ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable) ) {
+ edpt_skip_active(rhport, ep_id);
+ ep_cs[0].cmd_sts.disable = ep_cs[1].cmd_sts.disable = 1;
+ }
edpt_reset(rhport, ep_id);
@@ -389,7 +423,6 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * p_endpoint_desc)
}
// Enable EP interrupt
- dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
dcd_reg->INTEN |= TU_BIT(ep_id);
return true;
@@ -404,29 +437,35 @@ void dcd_edpt_close_all (uint8_t rhport)
}
}
-void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr)
-{
- (void) rhport;
-
+bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
+ (void) largest_packet_size;
+ // Reserve the endpoint command/status entry once (persists across altsetting changes); the
+ // buffer pointer is filled per-transfer, so nothing to pre-allocate. Mirrors the ISO branch of
+ // dcd_edpt_open().
uint8_t ep_id = ep_addr2id(ep_addr);
- _dcd.ep[ep_id][0].cmd_sts.active = _dcd.ep[ep_id][0].cmd_sts.active = 0; // TODO proper way is to EPSKIP then wait ep[][].active then write ep[][].disable (see table 778 in LPC55S69 Use Manual)
- _dcd.ep[ep_id][0].cmd_sts.disable = _dcd.ep[ep_id][1].cmd_sts.disable = 1;
-}
+ ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id);
+ TU_ASSERT( ep_cs[0].cmd_sts.disable && ep_cs[1].cmd_sts.disable );
-#if 0
-bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
- (void)rhport;
- (void)ep_addr;
- (void)largest_packet_size;
- return false;
+ edpt_reset(rhport, ep_id);
+ ep_cs[0].cmd_sts.type = 1; // ISO
+
+ dcd_registers_t* dcd_reg = _dcd_controller[rhport].regs;
+ dcd_reg->INTEN |= TU_BIT(ep_id);
+ return true;
}
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
- (void)rhport;
- (void)desc_ep;
- return false;
+ // (Re)activate on altsetting selection: abort a transfer still armed from the previous
+ // altsetting (the hardware keeps servicing an Active buffer across SET_INTERFACE, fighting the
+ // fresh transfer the class queues), clear stall and reset the data toggle. Direct Active=0, not
+ // EPSKIP (see edpt_skip_active). The class re-arms via dcd_edpt_xfer().
+ uint8_t ep_id = ep_addr2id(desc_ep->bEndpointAddress);
+ ep_cmd_sts_t* ep_cs = get_ep_cs(ep_id);
+ ep_cs[0].cmd_sts.active = 0;
+ ep_cs[1].cmd_sts.active = 0;
+ dcd_edpt_clear_stall(rhport, desc_ep->bEndpointAddress);
+ return true;
}
-#endif
static void prepare_ep_xfer(uint8_t rhport, uint8_t ep_id, uint16_t buf_offset, uint16_t total_bytes) {
uint16_t nbytes;
diff --git a/src/portable/raspberrypi/rp2040/dcd_rp2040.c b/src/portable/raspberrypi/rp2040/dcd_rp2040.c
index 63097cd0a..a0d312b8f 100644
--- a/src/portable/raspberrypi/rp2040/dcd_rp2040.c
+++ b/src/portable/raspberrypi/rp2040/dcd_rp2040.c
@@ -550,9 +550,46 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
if (epnum != 0) {
struct hw_endpoint* ep = hw_endpoint_get(epnum, dir);
- ep->next_pid = 0; // reset data toggle
- io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
- *buf_reg = 0;
+
+ if (ep->state == EPSTATE_ACTIVE) {
+ // Clear-halt on an endpoint with an in-flight transfer is used as a data-toggle reset
+ // (e.g. usbtest case 29) rather than to recover from a real stall (a stall aborts the
+ // transfer, leaving the endpoint IDLE). Abort and re-issue the transfer with the toggle
+ // reset to DATA0 so it still completes and releases the usbd claim, instead of silently
+ // dropping it and starving the endpoint. Save the buffer/length before the abort clears them.
+ uint8_t* user_buf = ep->user_buf;
+ uint16_t remaining = ep->remaining_len;
+ const uint16_t xferred = ep->xferred_len; // bytes already moved on this submission
+ io_rw_32 *ep_reg = get_ep_ctrl(epnum, dir);
+ io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
+ // bufctrl_prepare16() subtracts each armed buffer's length from remaining_len when arming,
+ // for BOTH directions, before the host has drained (IN) or filled (OUT) it. The abort below
+ // discards those still-armed buffers, so rewind remaining_len by their lengths or the re-issue
+ // is short by 1-2 packets. IN additionally advances user_buf as packets are copied into DPRAM,
+ // so its pointer must rewind too; OUT copies out only on completion, so its pointer is intact.
+ const uint32_t bc = *buf_reg;
+ uint16_t staged = 0;
+ if (bc & USB_BUF_CTRL_AVAIL) {
+ staged = (uint16_t)(bc & USB_BUF_CTRL_LEN_MASK);
+ }
+ if ((bc >> 16) & USB_BUF_CTRL_AVAIL) {
+ staged = (uint16_t)(staged + ((bc >> 16) & USB_BUF_CTRL_LEN_MASK));
+ }
+ remaining = (uint16_t)(remaining + staged);
+ if (dir == TUSB_DIR_IN) {
+ user_buf -= staged;
+ }
+ hw_endpoint_abort_xfer(ep); // safe abort (handles RP2040-E2), resets ep transfer state
+ ep->next_pid = 0; // DATA0
+ rp2usb_xfer_start(ep, ep_reg, buf_reg, user_buf, NULL, remaining);
+ // rp2usb_xfer_start() zeroes xferred_len; add back what the aborted transfer already moved so
+ // the eventual completion reports the full length, not just the post-clear-halt remainder.
+ ep->xferred_len += xferred;
+ } else {
+ ep->next_pid = 0; // reset data toggle
+ io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
+ *buf_reg = 0; // clear the stall response
+ }
}
}
diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c
index e4eb0184e..5421b9b2b 100644
--- a/src/portable/raspberrypi/rp2040/rp2040_usb.c
+++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c
@@ -176,10 +176,15 @@ void __tusb_irq_path_func(rp2usb_buffer_start)(hw_endpoint_t *ep, io_rw_32 *ep_r
// Note: device EP0 does not have an endpoint control register
if (ep_reg != NULL) {
uint32_t ep_ctrl = *ep_reg;
+ // Isochronous endpoints get a single DPRAM buffer (hw_endpoint_open only double-sizes BULK), so
+ // they must never be double-buffered here even when a transfer spans multiple packets, or buffer
+ // 1 (at dpram_buf+64) would spill into the next endpoint's DPRAM. (Never true for BULK, so the
+ // double-buffered bulk path is unaffected.)
+ const bool is_iso = (((ep_ctrl >> EP_CTRL_BUFFER_TYPE_LSB) & 0x3u) == TUSB_XFER_ISOCHRONOUS);
#if CFG_TUH_ENABLED
- const bool force_single = (rp2usb_is_host_mode() && ep->interrupt_num > 0);
+ const bool force_single = is_iso || (rp2usb_is_host_mode() && ep->interrupt_num > 0);
#else
- const bool force_single = false;
+ const bool force_single = is_iso;
#endif
if (ep->remaining_len && !force_single) {
diff --git a/src/portable/renesas/rusb2/dcd_rusb2.c b/src/portable/renesas/rusb2/dcd_rusb2.c
index f0ef9738b..c93bab05e 100644
--- a/src/portable/renesas/rusb2/dcd_rusb2.c
+++ b/src/portable/renesas/rusb2/dcd_rusb2.c
@@ -28,6 +28,9 @@ typedef struct {
uint8_t ep; /* an assigned endpoint address */
uint8_t ff; /* `buf` is TU_FUFO or POD */
+ bool queued; /* a transfer is submitted and not yet completed (independent of `buf`, which is
+ NULL for a zero-length read) -- used to decide clear-stall re-arm */
+ bool zlp_pending; /* a zero-length IN packet couldn't be queued at submit (FIFO full); retry on BRDY */
} pipe_state_t;
typedef struct
@@ -40,6 +43,7 @@ typedef struct
static dcd_data_t _dcd;
+
//--------------------------------------------------------------------+
// INTERNAL OBJECT & FUNCTION DECLARATION
//--------------------------------------------------------------------+
@@ -121,9 +125,19 @@ static uint16_t edpt_max_packet_size(rusb2_reg_t *rusb, unsigned num) {
return rusb->PIPEMAXP;
}
-static inline void pipe_wait_for_ready(rusb2_reg_t * rusb, unsigned num) {
- while ( rusb->D0FIFOSEL_b.CURPIPE != num ) {}
- while ( !rusb->D0FIFOCTR_b.FRDY ) {}
+// Select the D0FIFO for `num` and wait until its buffer is ready for CPU access. Both flags
+// normally settle within a few cycles (the pipe was just armed, or a BRDY freed a plane). But an
+// IN pipe whose double buffer is already full stalls FRDY until the host drains it, and a
+// no-handshake iso IN endpoint the host has stopped polling never drains at all — so FRDY would
+// hang forever. This runs with the USB IRQ masked, so a naked spin freezes the whole stack; bound
+// it and let the caller abort the FIFO access. Returns false on timeout.
+#define RUSB2_FIFO_READY_SPIN 100000u
+static inline bool pipe_wait_for_ready(rusb2_reg_t *rusb, unsigned num) {
+ uint32_t spin = RUSB2_FIFO_READY_SPIN;
+ while ( rusb->D0FIFOSEL_b.CURPIPE != num ) { if (!spin--) return false; }
+ spin = RUSB2_FIFO_READY_SPIN;
+ while ( !rusb->D0FIFOCTR_b.FRDY ) { if (!spin--) return false; }
+ return true;
}
//--------------------------------------------------------------------+
@@ -177,6 +191,15 @@ static bool pipe0_xfer_out(rusb2_reg_t *rusb) {
pipe_state_t *pipe = &_dcd.pipe[0];
const unsigned rem = pipe->remaining;
+ // BRDY with no armed transfer: a back-to-back data-stage packet beat the PID=NAK below (the
+ // host has already ACKed it). Park it in the DCP buffer — an unread buffer NAKs further OUTs —
+ // and let process_pipe0_xfer deliver it when usbd arms the next chunk. BCLR here would silently
+ // drop the packet and shift every later chunk by one (usbtest ctrl_out corruption at ra4m1).
+ if (pipe->buf == NULL && rem == 0) {
+ rusb->DCPCTR = RUSB2_PIPE_CTR_PID_NAK;
+ return false;
+ }
+
const uint16_t mps = edpt0_max_packet_size(rusb);
const uint16_t vld = rusb->CFIFOCTR_b.DTLN;
const uint16_t len = tu_min16(tu_min16(rem, mps), vld);
@@ -201,6 +224,12 @@ static bool pipe0_xfer_out(rusb2_reg_t *rusb) {
pipe->remaining = rem - len;
if ((len < mps) || (rem == len)) {
pipe->buf = NULL;
+ // Flow-control the single-buffer control pipe: NAK further OUT until usbd arms the next
+ // data-stage chunk. usbd receives a multi-packet control-OUT one CFG_TUD_ENDPOINT0_SIZE
+ // packet per submit; without this the DCP auto-accepts the next back-to-back packet into the
+ // just-emptied buffer and the following BRDY (remaining==0) BCLR-discards it, dropping 64
+ // bytes mid-transfer (e.g. usbtest ctrl_out 512B). RA4M1 UM R01UH0887 DCPCTR.PID.
+ rusb->DCPCTR = RUSB2_PIPE_CTR_PID_NAK;
return true;
}
@@ -226,7 +255,12 @@ static bool pipe_xfer_in(rusb2_reg_t* rusb, unsigned num)
}
const uint16_t mps = edpt_max_packet_size(rusb, num);
- pipe_wait_for_ready(rusb, num);
+ if (!pipe_wait_for_ready(rusb, num)) {
+ // Buffer never came ready (double-buffered IN pipe full, host not draining). Drop this load;
+ // the transfer stays pending and is retried when a BRDY frees a plane or the pipe is re-armed.
+ rusb->D0FIFOSEL = 0;
+ return false;
+ }
uint16_t len = tu_min16(rem, mps);
void *buf = pipe->buf;
@@ -267,7 +301,10 @@ static bool pipe_xfer_out(rusb2_reg_t* rusb, unsigned num)
rusb->D0FIFOSEL = fifo_sel;
const uint16_t mps = edpt_max_packet_size(rusb, num);
- pipe_wait_for_ready(rusb, num);
+ if (!pipe_wait_for_ready(rusb, num)) {
+ rusb->D0FIFOSEL = 0;
+ return false; // FIFO not ready; leave the receive pending (BRDY re-enters when data arrives)
+ }
const uint16_t vld = (uint16_t)rusb->D0FIFOCTR_b.DTLN;
const uint16_t len = tu_min16(tu_min16(rem, mps), vld);
@@ -333,7 +370,16 @@ static void process_status_completion(uint8_t rhport)
dcd_event_xfer_complete(rhport, ep_addr, 0, XFER_RESULT_SUCCESS, true);
}
-static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer,
+// Report a completed transfer on `num` and reset its bookkeeping. Single completion path for the
+// BRDY handler and the EP0 parked-packet drain, so they can't diverge (e.g. on clearing `queued`).
+static void pipe_xfer_complete(uint8_t rhport, unsigned num, bool in_isr) {
+ pipe_state_t *pipe = &_dcd.pipe[num];
+ pipe->queued = false;
+ dcd_event_xfer_complete(rhport, pipe->ep, pipe->length - pipe->remaining,
+ XFER_RESULT_SUCCESS, in_isr);
+}
+
+static bool process_pipe0_xfer(uint8_t rhport, rusb2_reg_t *rusb, int buffer_type, uint8_t ep_addr, void *buffer,
uint16_t total_bytes) {
uint16_t fifo_sel =
(rusb2_is_highspeed_reg(rusb) ? RUSB2_FIFOSEL_MBW_32BIT : RUSB2_FIFOSEL_MBW_16BIT) | FIFOSEL_BIGEND;
@@ -359,6 +405,15 @@ static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_ad
/* IN */
TU_ASSERT(rusb->DCPCTR_b.BSTS && (rusb->USBREQ & 0x80));
pipe0_xfer_in(rusb);
+ } else if (rusb->CFIFOCTR_b.DTLN > 0) {
+ /* OUT: a back-to-back packet parked by pipe0_xfer_out already sits in the DCP buffer (its
+ BRDY has fired and been cleared) — deliver it into this chunk now; no new BRDY will come
+ for it. Runs with the USB IRQ masked (dcd_edpt_xfer). Detected via the hardware DTLN
+ rather than a driver flag: the BCLR at SETUP/bus-reset then self-heals any parked state. */
+ if (pipe0_xfer_out(rusb)) {
+ pipe_xfer_complete(rhport, 0, false);
+ return true; // PID stays NAK (set by pipe0_xfer_out) until the next chunk is armed
+ }
}
rusb->DCPCTR = RUSB2_PIPE_CTR_PID_BUF;
} else {
@@ -370,6 +425,24 @@ static bool process_pipe0_xfer(rusb2_reg_t *rusb, int buffer_type, uint8_t ep_ad
return true;
}
+// Queue a zero-length IN packet. Returns false if the FIFO buffer wasn't free (double-buffered pipe
+// full, host not draining) so BVAL couldn't be written -- the caller retries on the next BRDY.
+static bool pipe_zlp_in(rusb2_reg_t *rusb, unsigned num) {
+ rusb->D0FIFOSEL = (uint16_t) num;
+ const bool ready = pipe_wait_for_ready(rusb, num);
+ if (ready) {
+ rusb->D0FIFOCTR = RUSB2_CFIFOCTR_BVAL_Msk;
+ }
+ rusb->D0FIFOSEL = 0;
+ // deselect completes within a few bus cycles (not host-dependent), but bound it anyway: this
+ // runs with the USB IRQ masked, where any stuck spin freezes the whole stack
+ uint32_t spin = RUSB2_FIFO_READY_SPIN;
+ while (rusb->D0FIFOSEL_b.CURPIPE) {
+ if (!spin--) { break; }
+ }
+ return ready;
+}
+
static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes)
{
const unsigned epn = tu_edpt_number(ep_addr);
@@ -379,23 +452,20 @@ static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_add
TU_ASSERT(num);
pipe_state_t *pipe = &_dcd.pipe[num];
- pipe->ff = buffer_type;
- pipe->buf = buffer;
- pipe->length = total_bytes;
- pipe->remaining = total_bytes;
+ pipe->ff = buffer_type;
+ pipe->buf = buffer;
+ pipe->length = total_bytes;
+ pipe->remaining = total_bytes;
+ pipe->queued = true;
+ pipe->zlp_pending = false;
if (dir) {
/* IN */
if (total_bytes) {
pipe_xfer_in(rusb, num);
} else {
- /* ZLP */
- rusb->D0FIFOSEL = num;
- pipe_wait_for_ready(rusb, num);
- rusb->D0FIFOCTR = RUSB2_CFIFOCTR_BVAL_Msk;
- rusb->D0FIFOSEL = 0;
- /* if CURPIPE bits changes, check written value */
- while (rusb->D0FIFOSEL_b.CURPIPE) {}
+ /* ZLP: if the FIFO buffer isn't free yet, defer the queue to the next BRDY (see process_pipe_brdy) */
+ pipe->zlp_pending = !pipe_zlp_in(rusb, num);
}
} else {
// OUT
@@ -418,11 +488,11 @@ static bool process_pipe_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_add
return true;
}
-static bool process_edpt_xfer(rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes)
+static bool process_edpt_xfer(uint8_t rhport, rusb2_reg_t* rusb, int buffer_type, uint8_t ep_addr, void* buffer, uint16_t total_bytes)
{
const unsigned epn = tu_edpt_number(ep_addr);
if (0 == epn) {
- return process_pipe0_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes);
+ return process_pipe0_xfer(rhport, rusb, buffer_type, ep_addr, buffer, total_bytes);
} else {
return process_pipe_xfer(rusb, buffer_type, ep_addr, buffer, total_bytes);
}
@@ -448,7 +518,15 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num)
if (dir) {
/* IN */
- completed = pipe_xfer_in(rusb, num);
+ if (pipe->zlp_pending) {
+ // The submit-time ZLP couldn't be queued (FIFO full); a freed buffer plane lets us queue it
+ // now. Don't report completion until the ZLP is actually queued (and then sent, next BRDY),
+ // otherwise a spurious BRDY would complete a zero-length IN the host never received.
+ pipe->zlp_pending = !pipe_zlp_in(rusb, num);
+ completed = false;
+ } else {
+ completed = pipe_xfer_in(rusb, num);
+ }
} else {
// OUT
if (num) {
@@ -458,9 +536,7 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num)
}
}
if (completed) {
- dcd_event_xfer_complete(rhport, pipe->ep,
- pipe->length - pipe->remaining,
- XFER_RESULT_SUCCESS, true);
+ pipe_xfer_complete(rhport, num, true);
// TU_LOG1("C %d %d\r\n", num, pipe->length - pipe->remaining);
}
}
@@ -585,19 +661,8 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
#ifdef RUSB2_SUPPORT_HIGHSPEED
if ( rusb2_is_highspeed_rhport(rhport) ) {
- rusb->SYSCFG_b.HSE = 1;
-
- // leave CLKSEL as default (0x11) 24Mhz
-
- // Power and reset UTMI Phy
- uint16_t physet = (rusb->PHYSET | RUSB2_PHYSET_PLLRESET_Msk) & ~RUSB2_PHYSET_DIRPD_Msk;
- rusb->PHYSET = physet;
- R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS);
- rusb->PHYSET_b.PLLRESET = 0;
-
- // set UTMI to operating mode and wait for PLL lock confirmation
- rusb->LPSTS_b.SUSPENDM = 1;
- while (!rusb->PLLSTA_b.PLLLOCK) {}
+ rusb->SYSCFG_b.HSE = TUD_OPT_HIGH_SPEED ? 1 : 0; // FS-only build: no HS chirp
+ rusb2_utmi_phy_powerup(rusb);
rusb->SYSCFG_b.DRPD = 0;
rusb->SYSCFG_b.USBE = 1;
@@ -702,10 +767,20 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc)
if ( !rusb2_is_highspeed_rhport(rhport) && mps > 256) {
return false;
}
+ } else if (xfer == TUSB_XFER_INTERRUPT) {
+ // Interrupt pipes (6-9) have a fixed 64-byte buffer even in high speed (RA6M5 UM 29.1);
+ // a larger PIPEMAXP would enumerate, then silently truncate every transfer
+ TU_ASSERT(mps <= 64);
}
- const unsigned num = find_pipe(xfer);
- TU_ASSERT(num);
+ // Re-opening an endpoint must reuse its pipe: usbd_edpt_close() is a no-op on ISO_ALLOC ports,
+ // so a class's close/open across SET_INTERFACE (e.g. video's notification endpoint) would
+ // otherwise allocate a second pipe with the same EPNUM and leak pipes until exhaustion.
+ unsigned num = _dcd.ep[dir][epn];
+ if (num == 0) {
+ num = find_pipe(xfer);
+ TU_ASSERT(num);
+ }
_dcd.pipe[num].ep = ep_addr;
_dcd.ep[dir][epn] = num;
@@ -713,13 +788,12 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc)
/* setup pipe */
dcd_int_disable(rhport);
+ rusb->PIPESEL = num;
if ( rusb2_is_highspeed_rhport(rhport) ) {
- // FIXME shouldn't be after pipe selection and config, also the BUFNMB should be changed
- // depending on the allocation scheme
+ // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe.
+ // FIXME BUFNMB is a fixed 0x08 for every pipe; a real per-pipe allocation scheme is needed.
rusb->PIPEBUF = 0x7C08;
}
-
- rusb->PIPESEL = num;
rusb->PIPEMAXP = mps;
volatile uint16_t *ctr = get_pipectr(rusb, num);
*ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk;
@@ -748,6 +822,8 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc)
return true;
}
+static void edpt_close(uint8_t rhport, uint8_t ep_addr);
+
void dcd_edpt_close_all(uint8_t rhport)
{
unsigned i = TU_ARRAY_SIZE(_dcd.pipe);
@@ -757,12 +833,14 @@ void dcd_edpt_close_all(uint8_t rhport)
if (!ep_addr) {
continue;
}
- dcd_edpt_close(rhport, (uint8_t)ep_addr);
+ edpt_close(rhport, (uint8_t)ep_addr);
}
dcd_int_enable(rhport);
}
-void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr)
+// Internal helper: on this (ISO_ALLOC) IP the stack no longer calls dcd_edpt_close(); only
+// dcd_edpt_close_all() uses it to tear down each pipe.
+static void edpt_close(uint8_t rhport, uint8_t ep_addr)
{
rusb2_reg_t * rusb = RUSB2_REG(rhport);
const unsigned epn = tu_edpt_number(ep_addr);
@@ -774,24 +852,73 @@ void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr)
*ctr = 0;
rusb->PIPESEL = (uint16_t)num;
rusb->PIPECFG = 0;
- _dcd.pipe[num].ep = 0;
+ _dcd.pipe[num].ep = 0;
+ _dcd.pipe[num].queued = false;
+ _dcd.pipe[num].zlp_pending = false;
_dcd.ep[dir][epn] = 0;
}
-#if 0
bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
- (void)rhport;
- (void)ep_addr;
- (void)largest_packet_size;
- return false;
+ rusb2_reg_t * rusb = RUSB2_REG(rhport);
+ const unsigned epn = tu_edpt_number(ep_addr);
+ const unsigned dir = tu_edpt_dir(ep_addr);
+
+ // Fullspeed ISO is limited to 256 bytes
+ if (!rusb2_is_highspeed_rhport(rhport) && largest_packet_size > 256) {
+ return false;
+ }
+
+ // Reserve an ISO-capable pipe (1 or 2) once; it persists across altsetting changes so
+ // dcd_edpt_iso_activate() only has to re-arm it in place (no pipe free/realloc, which on this
+ // shared-register IP would churn PIPESEL/PIPECFG and disturb the other pipes).
+ const unsigned num = find_pipe(TUSB_XFER_ISOCHRONOUS);
+ TU_ASSERT(num);
+ _dcd.pipe[num].ep = ep_addr;
+ _dcd.ep[dir][epn] = num;
+
+ dcd_int_disable(rhport);
+ rusb->PIPESEL = (uint16_t) num;
+ if (rusb2_is_highspeed_rhport(rhport)) {
+ // PIPEBUF is PIPESEL-windowed (RA6M5 UM 29.2.35): write it after selecting the pipe.
+ // FIXME (as in dcd_edpt_open): BUFNMB is a fixed 0x08 for every pipe; a real allocator is needed.
+ rusb->PIPEBUF = 0x7C08;
+ }
+ rusb->PIPEMAXP = largest_packet_size;
+ volatile uint16_t *ctr = get_pipectr(rusb, num);
+ *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk;
+ *ctr = 0; // leave the pipe NAKing until activated
+ rusb->PIPECFG = (uint16_t) ((dir << 4) | epn | RUSB2_PIPECFG_TYPE_ISO | RUSB2_PIPECFG_DBLB_Msk);
+ rusb->BRDYSTS = (uint16_t) (0x3FFu ^ TU_BIT(num));
+ rusb->BRDYENB |= TU_BIT(num);
+ dcd_int_enable(rhport);
+ return true;
}
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
- (void)rhport;
- (void)desc_ep;
- return false;
+ rusb2_reg_t * rusb = RUSB2_REG(rhport);
+ const uint8_t ep_addr = desc_ep->bEndpointAddress;
+ const unsigned epn = tu_edpt_number(ep_addr);
+ const unsigned dir = tu_edpt_dir(ep_addr);
+ const unsigned num = _dcd.ep[dir][epn];
+ TU_ASSERT(num); // must have been iso-alloc'd
+
+ dcd_int_disable(rhport);
+ rusb->PIPESEL = (uint16_t) num;
+ rusb->PIPEMAXP = tu_edpt_packet_size(desc_ep);
+ volatile uint16_t *ctr = get_pipectr(rusb, num);
+ *ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk; // abort in-flight + reset data toggle
+ *ctr = 0;
+ // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the
+ // stale bookkeeping so a BRDY firing before the class re-arms can't replay it
+ pipe_state_t *pipe = &_dcd.pipe[num];
+ pipe->buf = NULL;
+ pipe->remaining = 0;
+ pipe->queued = false;
+ pipe->zlp_pending = false;
+ *ctr = RUSB2_PIPE_CTR_PID_BUF; // enable
+ dcd_int_enable(rhport);
+ return true;
}
-#endif
bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t total_bytes, bool is_isr)
{
@@ -799,7 +926,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t
rusb2_reg_t* rusb = RUSB2_REG(rhport);
dcd_int_disable(rhport);
- bool r = process_edpt_xfer(rusb, 0, ep_addr, buffer, total_bytes);
+ bool r = process_edpt_xfer(rhport, rusb, 0, ep_addr, buffer, total_bytes);
dcd_int_enable(rhport);
return r;
@@ -812,7 +939,7 @@ bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t * ff, uint16_
rusb2_reg_t* rusb = RUSB2_REG(rhport);
dcd_int_disable(rhport);
- bool r = process_edpt_xfer(rusb, 1, ep_addr, ff, total_bytes);
+ bool r = process_edpt_xfer(rhport, rusb, 1, ep_addr, ff, total_bytes);
dcd_int_enable(rhport);
return r;
@@ -847,7 +974,19 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr)
} else {
const unsigned num = _dcd.ep[0][tu_edpt_number(ep_addr)];
rusb->PIPESEL = (uint16_t)num;
- if (rusb->PIPECFG_b.TYPE != 1) {
+ // Drop any packet parked in the buffer while halted: a data-OUT packet the host sent before
+ // aborting its transfer would otherwise be delivered into the next read after recovery
+ // (BOT reset + clear-halt re-arms a 31-byte CBW read which then receives stale WRITE data,
+ // "SCSI CBW is not valid" -> stall -> reset loop; ra6m5 msc write wedge).
+ *ctr = RUSB2_PIPE_CTR_ACLRM_Msk;
+ *ctr = 0;
+ // Non-bulk OUT re-enables straight away. Bulk OUT is normally armed together with its transaction
+ // counter (TRE) by process_pipe_xfer(), so we don't blindly re-enable it here — but if a receive
+ // was already armed (still queued), SQCLR above just left it NAKing. Re-assert BUF so it keeps
+ // receiving; the class driver still considers that read submitted and never re-arms it, so
+ // otherwise the endpoint NAKs forever (usbtest toggle test 29 clears the halt on an armed pipe).
+ // `queued` (not `buf`) is the armed test: a zero-length OUT read has buf==NULL yet is armed.
+ if (rusb->PIPECFG_b.TYPE != 1 || _dcd.pipe[num].queued) {
*ctr = RUSB2_PIPE_CTR_PID_BUF;
}
}
diff --git a/src/portable/renesas/rusb2/hcd_rusb2.c b/src/portable/renesas/rusb2/hcd_rusb2.c
index 849551d27..489162e54 100644
--- a/src/portable/renesas/rusb2/hcd_rusb2.c
+++ b/src/portable/renesas/rusb2/hcd_rusb2.c
@@ -454,11 +454,9 @@ bool hcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
if (rusb2_is_highspeed_rhport(rhport) ) {
rusb->SYSCFG_b.HSE = 1;
rusb->PHYSET_b.HSEB = 0;
- rusb->PHYSET_b.DIRPD = 0;
- R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS);
- rusb->PHYSET_b.PLLRESET = 0;
- rusb->LPSTS_b.SUSPENDM = 1;
- while ( !rusb->PLLSTA_b.PLLLOCK );
+ // same PHY reference-clock + power-up requirements as dcd_init: without CLKSEL matching the
+ // board XTAL the PLL never locks and the wait below would spin forever (e.g. EK-RA8M1, 20 MHz)
+ rusb2_utmi_phy_powerup(rusb);
rusb->SYSCFG_b.DRPD = 1;
rusb->SYSCFG_b.DCFM = 1;
rusb->SYSCFG_b.DPRPU = 0;
diff --git a/src/portable/renesas/rusb2/rusb2_ra.h b/src/portable/renesas/rusb2/rusb2_ra.h
index e5945ffe2..0954d0d25 100644
--- a/src/portable/renesas/rusb2/rusb2_ra.h
+++ b/src/portable/renesas/rusb2/rusb2_ra.h
@@ -49,6 +49,23 @@ typedef struct {
#define rusb2_is_highspeed_rhport(_p) (_p == 1)
#define rusb2_is_highspeed_reg(_reg) (_reg == RUSB2_REG(1))
+
+ // UTMI PHY reference clock is the main oscillator: PHYSET.CLKSEL must match the board XTAL
+ // before the PHY PLL is released (RA6M5 UM R01UH0891 29.2.17: 00=12 MHz, 10=20 MHz,
+ // 11=24 MHz reset default). EK-RA6M5 runs 24 MHz (default works); EK-RA8M1 runs 20 MHz and
+ // never locks/chirps on the default. A board with a non-standard USB clocking scheme can
+ // pre-define RUSB2_PHYSET_CLKSEL_VALUE to override this selection.
+ #ifndef RUSB2_PHYSET_CLKSEL_VALUE
+ #if BSP_CFG_XTAL_HZ == 12000000
+ #define RUSB2_PHYSET_CLKSEL_VALUE 0u
+ #elif BSP_CFG_XTAL_HZ == 20000000
+ #define RUSB2_PHYSET_CLKSEL_VALUE 2u
+ #elif BSP_CFG_XTAL_HZ == 24000000
+ #define RUSB2_PHYSET_CLKSEL_VALUE 3u
+ #else
+ #error "USBHS UTMI PHY: no PHYSET.CLKSEL encoding for this BSP_CFG_XTAL_HZ; define RUSB2_PHYSET_CLKSEL_VALUE"
+ #endif
+ #endif
#else
#define RUSB2_CONTROLLER_COUNT 1
@@ -84,6 +101,31 @@ TU_ATTR_ALWAYS_INLINE static inline void rusb2_int_disable(uint8_t rhport) {
TU_ATTR_ALWAYS_INLINE static inline void rusb2_phy_init(void) {
}
+#ifdef RUSB2_SUPPORT_HIGHSPEED
+// UTMI PHY power-up per the FSP reference sequence (r_usb_preg_access.c), shared by dcd_init and
+// hcd_init: program CLKSEL to the board XTAL while the PHY is powered down (DIRPD=1), 1 us,
+// release DIRPD, 1 ms, release PLLRESET, then wait for PLL lock. Changing CLKSEL as the PHY
+// powers up gets mis-sampled (EK-RA8M1, 20 MHz).
+static inline void rusb2_utmi_phy_powerup(rusb2_reg_t* rusb) {
+ uint16_t physet = rusb->PHYSET | RUSB2_PHYSET_DIRPD_Msk;
+ rusb->PHYSET = physet;
+ #ifdef RUSB2_PHYSET_CLKSEL_VALUE
+ physet = (uint16_t) ((physet & ~RUSB2_PHYSET_CLKSEL_Msk) |
+ (RUSB2_PHYSET_CLKSEL_VALUE << RUSB2_PHYSET_CLKSEL_Pos));
+ rusb->PHYSET = physet;
+ #endif
+ R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MICROSECONDS);
+ physet &= (uint16_t) ~RUSB2_PHYSET_DIRPD_Msk;
+ rusb->PHYSET = physet;
+ R_BSP_SoftwareDelay((uint32_t) 1, BSP_DELAY_UNITS_MILLISECONDS);
+ rusb->PHYSET_b.PLLRESET = 0;
+
+ // set UTMI to operating mode and wait for PLL lock confirmation
+ rusb->LPSTS_b.SUSPENDM = 1;
+ while (!rusb->PLLSTA_b.PLLLOCK) {}
+}
+#endif
+
#ifdef __cplusplus
}
#endif
diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c
index aecd689b3..ba05818b6 100644
--- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c
+++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c
@@ -835,7 +835,17 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
ep_reg &= U_EPREG_MASK | EP_STAT_MASK(dir) | EP_DTOG_MASK(dir);
if (!ep_is_iso(ep_reg)) {
- ep_change_status(&ep_reg, dir, EP_STAT_NAK);
+ // Only knock a genuinely STALLED endpoint down to NAK (the class then re-arms it). If the
+ // endpoint is armed (VALID) - e.g. a clear-halt used purely to reset the data toggle, as in
+ // usbtest case 29 - leave STAT untouched so the in-flight transfer isn't disarmed with no
+ // completion, which would leak the usbd claim and starve the endpoint. Masking the STAT bits
+ // to 0 writes no toggle, so an armed/idle endpoint keeps its current status.
+ const uint8_t stat_pos = (uint8_t) (U_EPTX_STAT_Pos + (dir == TUSB_DIR_IN ? 0u : 8u));
+ if (((ep_reg >> stat_pos) & 0x3u) == EP_STAT_STALL) {
+ ep_change_status(&ep_reg, dir, EP_STAT_NAK);
+ } else {
+ ep_reg &= ~EP_STAT_MASK(dir);
+ }
}
ep_change_dtog(&ep_reg, dir, 0); // Reset to DATA0
ep_write(ep_idx, ep_reg, true);
diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c
index 6c88b4f27..86aa54510 100644
--- a/src/portable/synopsys/dwc2/dcd_dwc2.c
+++ b/src/portable/synopsys/dwc2/dcd_dwc2.c
@@ -393,10 +393,6 @@ static void edpt_schedule_packets(uint8_t rhport, const uint8_t epnum, const uin
}
dep->diepdma = (uintptr_t) xfer->buffer;
dep->diepctl = depctl.value; // enable endpoint
- // Advance buffer pointer for EP0
- if (epnum == 0) {
- xfer->buffer += total_bytes;
- }
} else
#endif
{
@@ -732,6 +728,8 @@ static void handle_bus_reset(uint8_t rhport) {
tu_memclr(xfer_status, sizeof(xfer_status));
+ _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0;
+ _dcd_data.ep0_pending[TUSB_DIR_IN] = 0;
_dcd_data.sof_en = false;
_dcd_data.allocated_epin_count = 0;
@@ -1009,6 +1007,10 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi
if (edpt_is_enabled(epin0)) {
edpt_disable(rhport, 0x80, false);
}
+ // a new SETUP aborts any in-progress control transfer: drop leftover EP0 chunking state so a
+ // stale latched completion cannot re-arm from it
+ _dcd_data.ep0_pending[TUSB_DIR_OUT] = 0;
+ _dcd_data.ep0_pending[TUSB_DIR_IN] = 0;
dcd_dcache_invalidate(_dcd_usbbuf.setup_buffer, sizeof(_dcd_usbbuf.setup_buffer));
@@ -1029,24 +1031,37 @@ static void handle_epout_dma(uint8_t rhport, uint8_t epnum, dwc2_doepint_t doepi
// only handle data skip if it is setup or status related
// Normal OUT transfer complete
if (!doepint_bm.status_phase_rx && !doepint_bm.setup_packet_rx) {
+ xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT);
if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_OUT]) {
- // EP0 can only handle one packet Schedule another packet to be received.
+ // EP0 can only handle one packet: invalidate and advance past the received bytes, then
+ // schedule the next.
+ if (xfer->buffer != NULL) {
+ dcd_dcache_invalidate(xfer->buffer, CFG_TUD_ENDPOINT0_SIZE);
+ xfer->buffer += CFG_TUD_ENDPOINT0_SIZE;
+ }
edpt_schedule_packets(rhport, epnum, TUSB_DIR_OUT);
} else {
dwc2_dep_t* epout = &dwc2->epout[epnum];
- xfer_ctl_t* xfer = XFER_CTL_BASE(epnum, TUSB_DIR_OUT);
// determine actual received bytes
const dwc2_ep_tsize_t tsiz = {.value = epout->tsiz};
const uint16_t remain = tsiz.xfer_size;
xfer->total_len -= remain;
+ // EP0 invalidates only this (final) chunk's DMA-written bytes: DOEPDMA "is incremented on
+ // every AHB transaction" (databook 7.1.83), i.e. it points past the last word written.
+ // Read it before dma_setup_prepare() re-targets it at the setup buffer
+ uint16_t inval_len = xfer->total_len;
+ if (epnum == 0) {
+ inval_len = (uint16_t)(epout->doepdma - (uintptr_t)xfer->buffer);
+ }
+
// prepare EP0 for next setup
if(epnum == 0) {
dma_setup_prepare(rhport);
}
- dcd_dcache_invalidate(xfer->buffer, xfer->total_len);
+ dcd_dcache_invalidate(xfer->buffer, inval_len);
dcd_event_xfer_complete(rhport, epnum, xfer->total_len, XFER_RESULT_SUCCESS, true);
}
}
@@ -1058,7 +1073,10 @@ static void handle_epin_dma(uint8_t rhport, uint8_t epnum, dwc2_diepint_t diepin
if (diepint_bm.xfer_complete) {
if ((epnum == 0) && _dcd_data.ep0_pending[TUSB_DIR_IN]) {
- // EP0 can only handle one packet. Schedule another packet to be transmitted.
+ // EP0 can only handle one packet: advance past the sent bytes, then schedule the next.
+ if (xfer->buffer != NULL) {
+ xfer->buffer += CFG_TUD_ENDPOINT0_SIZE;
+ }
edpt_schedule_packets(rhport, epnum, TUSB_DIR_IN);
} else {
dcd_event_xfer_complete(rhport, epnum | TUSB_DIR_IN_MASK, xfer->total_len, XFER_RESULT_SUCCESS, true);
diff --git a/src/portable/wch/ch32_usbfs_reg.h b/src/portable/wch/ch32_usbfs_reg.h
index 8bac103fe..5b037281f 100644
--- a/src/portable/wch/ch32_usbfs_reg.h
+++ b/src/portable/wch/ch32_usbfs_reg.h
@@ -179,6 +179,14 @@
#endif
#endif
+// CH32V20x/V30x/F20x USBFS gives endpoint 3 a 1023-byte isochronous packet (CH32FV2x_V3xRM ch23:
+// every endpoint is 64 B except EP3 = 1023 B, from EP3's 10-bit R16_UEP3_T_LEN field plus a single
+// contiguous >=1023 B DMA buffer — NOT double-buffering, which only yields 2x64 B).
+// CH32V103/X035/CH58x cap every endpoint at 64 B. dcd_ch32_usbfs.c reads this to size EP3's buffer.
+#if CFG_TUSB_MCU == OPT_MCU_CH32V20X || CFG_TUSB_MCU == OPT_MCU_CH32V307 || CFG_TUSB_MCU == OPT_MCU_CH32F20X
+ #define CH32_USBFS_EP3_1023_BUFSIZE 1
+#endif
+
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
diff --git a/src/portable/wch/dcd_ch32_usbfs.c b/src/portable/wch/dcd_ch32_usbfs.c
index a6458748a..ec521224e 100644
--- a/src/portable/wch/dcd_ch32_usbfs.c
+++ b/src/portable/wch/dcd_ch32_usbfs.c
@@ -16,6 +16,17 @@
/* private defines */
#define EP_MAX (8)
+ // EP3 IN buffer size. CH32V20x/V30x/F20x USBFS support full-speed iso packets up to 1023 B on
+ // endpoint 3 (every other endpoint is 64 B); those parts set CH32_USBFS_EP3_1023_BUFSIZE in
+ // ch32_usbfs_reg.h. V103/X035/CH58x cap every endpoint at 64 B. Overridable per project.
+ #ifndef CFG_TUD_WCH_USBFS_EP3_BUFSIZE
+ #ifdef CH32_USBFS_EP3_1023_BUFSIZE
+ #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 1023
+ #else
+ #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 64
+ #endif
+ #endif
+
// Struct-based EP register access (uniform layout). CH58X has a different register map and
// defines EP_DMA/EP_TX_LEN/EP_CTRL itself in ch32_usbfs_reg.h.
#if CFG_TUSB_MCU == OPT_MCU_CH583
@@ -107,7 +118,7 @@ struct usb_xfer {
static struct {
bool ep0_tog;
- bool isochronous[EP_MAX];
+ bool isochronous[EP_MAX][2]; // per [ep][dir]: an ep number may be iso in one direction
struct usb_xfer xfer[EP_MAX][2];
#ifdef CH32_USBFS_EP4_SHARES_EP0
// CH58X buffers laid out by hand so EP0/EP4 don't burn two unused buffer[] slots. EP0 and EP4
@@ -123,21 +134,23 @@ static struct {
TU_ATTR_ALIGNED(4) uint8_t ep6_buffer[2][64];
TU_ATTR_ALIGNED(4) uint8_t ep7_buffer[2][64];
#else
+ // Every endpoint gets a 64-byte OUT + 64-byte IN buffer.
TU_ATTR_ALIGNED(4) uint8_t buffer[EP_MAX][2][64];
- // EP3 IN gets an enlarged buffer for full-speed isochronous (packets up to 1023 B).
+ #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
+ // ...except EP3, which supports full-speed iso packets up to 1023 B on CH32V20x/V30x/F20x, so its
+ // IN buffer is enlarged (OUT stays 64 B; an OUT transfer >64 B on EP3 would overwrite queued IN).
TU_ATTR_ALIGNED(4) struct {
- // OUT transfers >64 bytes will overwrite queued IN data!
uint8_t out[64];
- uint8_t in[1023];
+ uint8_t in[CFG_TUD_WCH_USBFS_EP3_BUFSIZE];
uint8_t pad;
} ep3_buffer;
+ #endif
#endif
} data;
// DMA / copy buffer pointers per endpoint. The WCH USBFS buffer holds OUT (RX) at offset 0 and
-// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN; EP3 has an enlarged IN
-// buffer for throughput. On CH58X, EP0/EP4 share ep0_ep4_buffer and the regular endpoints use
-// their own named buffer (see the struct above).
+// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN. On CH58X, EP0/EP4 share
+// ep0_ep4_buffer and the regular endpoints use their own named buffer (see the struct above).
#ifdef CH32_USBFS_EP4_SHARES_EP0
// OUT base of the regular CH58X endpoints (EP1/2/3/5/6/7; EP0/EP4 share ep0_ep4_buffer).
static inline uint8_t* ch58x_ep_buffer(uint8_t ep) {
@@ -157,7 +170,9 @@ static inline uint32_t ep_dma_addr(uint8_t ep) {
if (ep == 0 || ep == 4) { return (uint32_t) &data.ep0_ep4_buffer[0]; } // EP4 shares EP0's DMA
return (uint32_t) ch58x_ep_buffer(ep);
#else
- if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; }
+ #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
+ if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; } // EP3 has an enlarged IN buffer
+ #endif
return (uint32_t) &data.buffer[ep][0];
#endif
}
@@ -168,7 +183,9 @@ static inline uint8_t* ep_out_buf(uint8_t ep) {
if (ep == 4) { return &data.ep0_ep4_buffer[64]; }
return ch58x_ep_buffer(ep);
#else
+ #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
if (ep == 3) { return data.ep3_buffer.out; }
+ #endif
return data.buffer[ep][TUSB_DIR_OUT];
#endif
}
@@ -180,7 +197,9 @@ static inline uint8_t* ep_in_buf(uint8_t ep) {
return ch58x_ep_buffer(ep) + 64; // IN at +64 within the endpoint's 128-byte buffer
#else
if (ep == 0) { return data.buffer[0][TUSB_DIR_OUT]; } // EP0 half-duplex: IN reuses OUT chunk
- if (ep == 3) { return data.ep3_buffer.in; }
+ #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
+ if (ep == 3) { return data.ep3_buffer.in; } // enlarged IN buffer for full-speed iso
+ #endif
return data.buffer[ep][TUSB_DIR_IN];
#endif
}
@@ -202,9 +221,8 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) {
if (force || xfer->len) {
size_t len = TU_MIN(xfer->max_size, xfer->len);
#if CFG_TUSB_MCU == OPT_MCU_CH583
- // Every CH58x endpoint buffer is 64 bytes. Isochronous (which would push max_size up to 1023)
- // is refused in dcd_edpt_iso_alloc(), but some classes (e.g. video) ignore that result, so cap
- // the copy here to guarantee we never write past the buffer into a neighbouring endpoint's.
+ // Every CH58x endpoint buffer is 64 bytes; cap the copy so an iso mps a class mistakenly set
+ // larger can't write past the buffer into a neighbouring endpoint's.
len = TU_MIN(len, 64u);
#endif
memcpy(ep_in_buf(ep), xfer->buffer, len);
@@ -216,7 +234,7 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) {
if (ep == 0) {
ep_tx_ctrl_set(0, USBFS_EP_T_RES_ACK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
data.ep0_tog = !data.ep0_tog;
- } else if (data.isochronous[ep]) {
+ } else if (data.isochronous[ep][TUSB_DIR_IN]) {
ep_tx_set_response(ep, USBFS_EP_T_RES_NYET);
} else {
ep_tx_set_response(ep, USBFS_EP_T_RES_ACK);
@@ -225,7 +243,7 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) {
xfer->valid = false;
if (ep == 0) {
ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
- } else if (!data.isochronous[ep]) {
+ } else if (!data.isochronous[ep][TUSB_DIR_IN]) {
ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
}
dcd_event_xfer_complete(rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, XFER_RESULT_SUCCESS, true);
@@ -254,7 +272,7 @@ static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) {
ep_rx_set_response(0, USBFS_EP_R_RES_NAK);
} else {
uint8_t rx_res =
- data.isochronous[ep] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK);
+ data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK);
ep_rx_set_response(ep, rx_res);
}
}
@@ -319,12 +337,14 @@ void dcd_int_handler(uint8_t rhport) {
// Drop an OUT packet whose data toggle doesn't match what we expect -- a host retransmit
// after a lost ACK, or a host that doesn't alternate DATA0/DATA1. The hardware auto-toggle
// does not reject these on its own, so the check is needed on every variant. EP0 keeps its
- // own toggle via the SETUP/status flow and is exempt.
- if (ep != 0 && !(int_st & USBFS_INT_ST_TOG_OK)) { break; }
+ // own toggle via the SETUP/status flow and is exempt; isochronous is DATA0-only (no toggle),
+ // so its packets must not be toggle-checked.
+ if (ep != 0 && !data.isochronous[ep][TUSB_DIR_OUT] && !(int_st & USBFS_INT_ST_TOG_OK)) { break; }
#ifdef CH32_USBFS_EP_MANUAL_TOG
// CH58x has no hardware auto-toggle: advance the expected RX toggle after each accepted packet
// (EP0 included -- it also has no auto-toggle and a control-OUT data stage can span packets).
- EP_CTRL(ep) ^= USBFS_EPC_R_TOG;
+ // Iso endpoints are DATA0-only, so leave them alone (matches the PID_IN path).
+ if (!data.isochronous[ep][TUSB_DIR_OUT]) { EP_CTRL(ep) ^= USBFS_EPC_R_TOG; }
#endif
update_out(rhport, ep, rx_len);
break;
@@ -333,7 +353,8 @@ void dcd_int_handler(uint8_t rhport) {
case PID_IN:
#ifdef CH32_USBFS_EP_MANUAL_TOG
// Manual toggle: flip the TX toggle after each ACK'd IN packet (EP0 manages its own).
- if (ep != 0) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; }
+ // Isochronous transfers are DATA0-only (no toggle), so leave iso endpoints alone.
+ if (ep != 0 && !data.isochronous[ep][TUSB_DIR_IN]) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; }
#endif
update_in(rhport, ep, false);
break;
@@ -443,6 +464,7 @@ bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
TU_ASSERT(ep < EP_MAX);
+ data.isochronous[ep][dir] = false; // (re)opening as a non-iso endpoint clears any stale iso flag
data.xfer[ep][dir].max_size = tu_edpt_packet_size(desc_ep);
if (ep != 0) {
@@ -464,31 +486,38 @@ void dcd_edpt_close_all(uint8_t rhport) {
bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
(void)rhport;
- (void)ep_addr;
- (void)largest_packet_size;
-#if CFG_TUSB_MCU == OPT_MCU_CH583
- // No isochronous support on CH58x: its 8-bit T_LEN caps a packet at 255B and the endpoints use
- // plain 64-byte buffers, so accepting an iso max_size (up to 1023) would let update_in()/
- // update_out() run off the end of the buffer into neighbouring ones. Refuse it outright.
- return false;
-#else
uint8_t ep = tu_edpt_number(ep_addr);
uint8_t dir = tu_edpt_dir(ep_addr);
+ TU_ASSERT(ep < EP_MAX);
- data.isochronous[ep] = true;
+ // Endpoint buffers are 64 B, except EP3 IN which is enlarged for full-speed iso on the parts that
+ // support 1023-byte EP3 packets (CH32V20x/V30x/F20x; CFG_TUD_WCH_USBFS_EP3_BUFSIZE). Reject a
+ // larger mps rather than running off the end into the neighbouring endpoint's memory.
+ uint16_t max_packet = 64;
+#if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
+ if (ep == 3 && dir == TUSB_DIR_IN) { max_packet = CFG_TUD_WCH_USBFS_EP3_BUFSIZE; }
+#endif
+ TU_VERIFY(largest_packet_size <= max_packet);
+
+ data.isochronous[ep][dir] = true;
data.xfer[ep][dir].max_size = largest_packet_size;
return true;
-#endif
}
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
(void)rhport;
- (void)desc_ep;
-#if CFG_TUSB_MCU == OPT_MCU_CH583
- return false; // CH58x has no isochronous support (see dcd_edpt_iso_alloc)
-#else
+ const uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress);
+ const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
+
+ // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the
+ // stale descriptor and NAK the endpoint so the ISR can't complete it against the old buffer
+ data.xfer[ep][dir].valid = false;
+ if (dir == TUSB_DIR_IN) {
+ ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
+ } else {
+ ep_rx_set_response(ep, USBFS_EP_R_RES_NAK);
+ }
return true;
-#endif
}
bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) {
@@ -510,7 +539,7 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t to
if (dir == TUSB_DIR_IN) {
update_in(rhport, ep, true);
} else {
- uint8_t rx_res = data.isochronous[ep] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK;
+ uint8_t rx_res = data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK;
ep_rx_set_response(ep, rx_res);
}
dcd_int_enable(rhport);
@@ -546,9 +575,15 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);
}
} else {
- // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR
+ // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR.
+ // Preserve an in-flight receive: if a read is still armed (the class driver considers it
+ // submitted and won't re-arm), fall back to ACK, not NAK, or the endpoint NAKs forever and the
+ // host times out (usbtest toggle test 29 clears the halt between bulk writes on an armed EP).
if (dir == TUSB_DIR_OUT) {
- ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK);
+ uint8_t res = data.xfer[ep][TUSB_DIR_OUT].valid
+ ? (data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK)
+ : USBFS_EP_R_RES_NAK;
+ ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | res);
} else {
ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK);
}
diff --git a/src/portable/wch/dcd_ch32_usbhs.c b/src/portable/wch/dcd_ch32_usbhs.c
index 0c154f5ce..577f86582 100644
--- a/src/portable/wch/dcd_ch32_usbhs.c
+++ b/src/portable/wch/dcd_ch32_usbhs.c
@@ -348,8 +348,16 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
const tusb_dir_t dir = tu_edpt_dir(ep_addr);
if (dir == TUSB_DIR_OUT) {
- EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0;
- ep_data_tog[ep_num][TUSB_DIR_OUT] = false;
+ ep_data_tog[ep_num][TUSB_DIR_OUT] = false; // clear-halt resets the toggle to DATA0
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_OUT);
+ if (xfer->valid) {
+ // A receive is still armed (the class driver considers it submitted and won't re-arm it);
+ // re-queue it (ACK/NYET) instead of leaving it NAKing, or the endpoint NAKs forever after
+ // clear-halt (usbtest toggle test 29 clears the halt on an armed bulk-OUT pipe).
+ queue_out_packet(ep_num, xfer);
+ } else {
+ EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0;
+ }
} else {
EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0;
ep_data_tog[ep_num][TUSB_DIR_IN] = false;