summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-07-12 04:59:21 +0700
committerhathach <[email protected]>2026-07-12 04:59:21 +0700
commitfbdbcd092e9234a82e10c8e673b63c28b46cb007 (patch)
tree4bb82abcbc086ab4a12517dc83da7c5acc1f9012
parent242377411afe78367efc01994241a6fda5bbdc00 (diff)
ch32h417: add USB3 SuperSpeed dcd with runtime USB2 fallback
Second stage: the USBSS SuperSpeed device driver (SPEED=super now links). The LINK layer is the same IP as the CH569 but the endpoint engine is the H417's reworked chain-DMA design (hardware SEQ_AUTO/ERDY_AUTO, persistent RB_EP_*_HALT, per-chain completion). The RCC/PHY/LINK bring-up and EP0 control sequences are transcribed from the WCH EVT USBSS device demo; data endpoints arm one chain per packet (burst 1) for correctness-first bring-up, matching the CH569 port's initial path - raise CFG_TUD_WCH_USB30_MAX_BURST once the hardware validates. Buffers may live anywhere in the shared SRAM (all DMA-reachable), so no bounce machinery is needed. - src/portable/wch/dcd_ch32h417_usb30.c (+ ch32h417_usb30_reg.h): full dcd with a STATE_CHG-driven LTSSM handler, LMP PORT_CAP/PORT_CFG exchange, EP0 DPH/ERDY flow with software sequence, chain-armed data endpoints, per-EP HALT stall, and a TIM12-backstopped runtime USB2 fallback that hands rhport 0 to ch32h417_usb2_* when SuperSpeed training fails (host has no SS port). - examples/device/*/skip.txt: add mcu:CH32H417 to the FreeRTOS + audio/video examples (no FreeRTOSConfig for this family yet), mirroring mcu:CH569. Build-verified: all 20 device examples build for SPEED=super, and cdc_msc for SPEED=high, clean under -Werror (riscv-none-elf-gcc 13.2). Hardware bring-up at 5 Gbps (usbtest battery, throughput) is pending rig availability. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QWhrtHjbSVMKcY68vxBwBm
-rw-r--r--examples/device/audio_4_channel_mic/skip.txt1
-rw-r--r--examples/device/audio_4_channel_mic_freertos/skip.txt1
-rw-r--r--examples/device/audio_test/skip.txt1
-rw-r--r--examples/device/audio_test_freertos/skip.txt1
-rw-r--r--examples/device/audio_test_multi_rate/skip.txt1
-rw-r--r--examples/device/cdc_msc_freertos/skip.txt1
-rw-r--r--examples/device/cdc_uac2/skip.txt1
-rw-r--r--examples/device/hid_composite_freertos/skip.txt1
-rw-r--r--examples/device/midi_test_freertos/skip.txt1
-rw-r--r--examples/device/uac2_headset/skip.txt1
-rw-r--r--examples/device/uac2_speaker_fb/skip.txt1
-rw-r--r--examples/device/video_capture/skip.txt1
-rw-r--r--examples/device/video_capture_2ch/skip.txt1
-rw-r--r--src/portable/wch/ch32h417_usb30_reg.h76
-rw-r--r--src/portable/wch/dcd_ch32h417_usb30.c655
15 files changed, 737 insertions, 7 deletions
diff --git a/examples/device/audio_4_channel_mic/skip.txt b/examples/device/audio_4_channel_mic/skip.txt
index 6d4f5a48e..d4736aee9 100644
--- a/examples/device/audio_4_channel_mic/skip.txt
+++ b/examples/device/audio_4_channel_mic/skip.txt
@@ -5,3 +5,4 @@ family:broadcom_64bit
family:espressif
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/audio_4_channel_mic_freertos/skip.txt b/examples/device/audio_4_channel_mic_freertos/skip.txt
index 9725814bb..5f52307d0 100644
--- a/examples/device/audio_4_channel_mic_freertos/skip.txt
+++ b/examples/device/audio_4_channel_mic_freertos/skip.txt
@@ -21,3 +21,4 @@ family:broadcom_64bit
family:nuc121_125
family:hpmicro
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/audio_test/skip.txt b/examples/device/audio_test/skip.txt
index 15c8461d2..786b167a7 100644
--- a/examples/device/audio_test/skip.txt
+++ b/examples/device/audio_test/skip.txt
@@ -4,3 +4,4 @@ mcu:SAMG
family:espressif
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/audio_test_freertos/skip.txt b/examples/device/audio_test_freertos/skip.txt
index a88b265c2..ed5669c43 100644
--- a/examples/device/audio_test_freertos/skip.txt
+++ b/examples/device/audio_test_freertos/skip.txt
@@ -19,3 +19,4 @@ board:stm32l0538disco
family:nuc121_125
family:hpmicro
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/audio_test_multi_rate/skip.txt b/examples/device/audio_test_multi_rate/skip.txt
index 15c8461d2..786b167a7 100644
--- a/examples/device/audio_test_multi_rate/skip.txt
+++ b/examples/device/audio_test_multi_rate/skip.txt
@@ -4,3 +4,4 @@ mcu:SAMG
family:espressif
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/cdc_msc_freertos/skip.txt b/examples/device/cdc_msc_freertos/skip.txt
index dda94cb65..33ae88a17 100644
--- a/examples/device/cdc_msc_freertos/skip.txt
+++ b/examples/device/cdc_msc_freertos/skip.txt
@@ -19,3 +19,4 @@ family:broadcom_64bit
family:nuc121_125
family:hpmicro
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/cdc_uac2/skip.txt b/examples/device/cdc_uac2/skip.txt
index 946bae864..0908c778b 100644
--- a/examples/device/cdc_uac2/skip.txt
+++ b/examples/device/cdc_uac2/skip.txt
@@ -8,3 +8,4 @@ board:stm32l052dap52
family:espressif
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/hid_composite_freertos/skip.txt b/examples/device/hid_composite_freertos/skip.txt
index bc2d90149..9e2f51a04 100644
--- a/examples/device/hid_composite_freertos/skip.txt
+++ b/examples/device/hid_composite_freertos/skip.txt
@@ -17,3 +17,4 @@ family:broadcom_32bit
family:broadcom_64bit
family:hpmicro
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/midi_test_freertos/skip.txt b/examples/device/midi_test_freertos/skip.txt
index bc2d90149..9e2f51a04 100644
--- a/examples/device/midi_test_freertos/skip.txt
+++ b/examples/device/midi_test_freertos/skip.txt
@@ -17,3 +17,4 @@ family:broadcom_32bit
family:broadcom_64bit
family:hpmicro
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/uac2_headset/skip.txt b/examples/device/uac2_headset/skip.txt
index 946bae864..0908c778b 100644
--- a/examples/device/uac2_headset/skip.txt
+++ b/examples/device/uac2_headset/skip.txt
@@ -8,3 +8,4 @@ board:stm32l052dap52
family:espressif
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/uac2_speaker_fb/skip.txt b/examples/device/uac2_speaker_fb/skip.txt
index 30ddf4e18..c6109d61d 100644
--- a/examples/device/uac2_speaker_fb/skip.txt
+++ b/examples/device/uac2_speaker_fb/skip.txt
@@ -8,3 +8,4 @@ board:stm32l052dap52
family:broadcom_64bit
mcu:CH583
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/video_capture/skip.txt b/examples/device/video_capture/skip.txt
index 9bb1410a8..90a01f757 100644
--- a/examples/device/video_capture/skip.txt
+++ b/examples/device/video_capture/skip.txt
@@ -5,3 +5,4 @@ mcu:MSP430x5xx
mcu:NUC121
mcu:SAMD11
mcu:CH569
+mcu:CH32H417
diff --git a/examples/device/video_capture_2ch/skip.txt b/examples/device/video_capture_2ch/skip.txt
index 7260ec2aa..c3b4e0880 100644
--- a/examples/device/video_capture_2ch/skip.txt
+++ b/examples/device/video_capture_2ch/skip.txt
@@ -18,3 +18,4 @@ board:ek_tm4c123gxl
board:uno_r4
board:ra4m1_ek
mcu:CH569
+mcu:CH32H417
diff --git a/src/portable/wch/ch32h417_usb30_reg.h b/src/portable/wch/ch32h417_usb30_reg.h
new file mode 100644
index 000000000..168359ca4
--- /dev/null
+++ b/src/portable/wch/ch32h417_usb30_reg.h
@@ -0,0 +1,76 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2026 Ha Thach (tinyusb.org)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#ifndef USB_CH32H417_USB30_REG_H
+#define USB_CH32H417_USB30_REG_H
+
+// USBSSD_TypeDef (@0x40034000), LINK_*/USBSS_* bit macros come from the SDK. The LINK-layer bit
+// names/semantics are documented in CH32H417RM-EN chapter 27 (the same LINK IP is used, less its
+// register documentation, by the CH569).
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wstrict-prototypes"
+#include "ch32h417.h"
+#include "ch32h417_usb.h"
+#include "ch32h417_tim.h" // TIM12 fallback backstop timer (TIM_IT_Update, TIM_CEN)
+#pragma GCC diagnostic pop
+
+// PHY configuration back-door (undocumented; values transcribed from the WCH EVT USBSS demo)
+#define USBSS_PHY_CFG_CR (*(volatile uint32_t*)0x400341F8)
+#define USBSS_PHY_CFG_DAT (*(volatile uint32_t*)0x400341FC)
+#define USBSS_PHY_MISC_REG (*(volatile uint32_t*)0x5003C018)
+
+// Link Management Packet (LMP) subtype / header fields (device = upstream port)
+#define LMP_HP 0
+#define LMP_SUBTYPE_MASK (0xFu << 5)
+#define LMP_SET_LINK_FUNC (0x1u << 5)
+#define LMP_U2_INACT_TOUT (0x2u << 5)
+#define LMP_PORT_CAP (0x4u << 5)
+#define LMP_PORT_CFG (0x5u << 5)
+#define LMP_PORT_CFG_RES (0x6u << 5)
+#define LMP_LINK_SPEED (1u << 9)
+#define LMP_NUM_HP_BUF (4u << 0)
+#define LMP_UP_STREAM (2u << 16)
+
+// Offset sanity checks against the SDK struct
+TU_VERIFY_STATIC(offsetof(USBSSD_TypeDef, USB_CONTROL) == 0x70, "USB_CONTROL");
+TU_VERIFY_STATIC(offsetof(USBSSD_TypeDef, USB_STATUS) == 0x74, "USB_STATUS");
+TU_VERIFY_STATIC(offsetof(USBSSD_TypeDef, UEP0_TX_CTRL) == 0x84, "UEP0_TX_CTRL");
+TU_VERIFY_STATIC(offsetof(USBSSD_TypeDef, EP1_TX) == 0xC0, "EP1_TX bank");
+
+// The per-endpoint TX and RX register banks alternate in the struct (EP1_TX, EP1_RX, EP2_TX,
+// EP2_RX, ...), each 16 bytes, so EPn's bank is 2 * (n-1) entries past EP1's.
+TU_ATTR_ALWAYS_INLINE static inline volatile USBSS_EP_TX_TypeDef* usbss_ep_tx(uint8_t ep_num) {
+ return (volatile USBSS_EP_TX_TypeDef*)&USBSSD->EP1_TX + 2u * (ep_num - 1u);
+}
+TU_ATTR_ALWAYS_INLINE static inline volatile USBSS_EP_RX_TypeDef* usbss_ep_rx(uint8_t ep_num) {
+ return (volatile USBSS_EP_RX_TypeDef*)&USBSSD->EP1_RX + 2u * (ep_num - 1u);
+}
+
+// USB_STATUS fields: which endpoint/direction raised the transfer interrupt
+#define USBSS_STATUS_EP_NUM(st) (((st) & USBSS_EP_ID_MASK) >> 8)
+#define USBSS_STATUS_EP_IN(st) (((st) & USBSS_EP_DIR_MASK) != 0)
+
+#endif
diff --git a/src/portable/wch/dcd_ch32h417_usb30.c b/src/portable/wch/dcd_ch32h417_usb30.c
index c730e33a4..d355dda7d 100644
--- a/src/portable/wch/dcd_ch32h417_usb30.c
+++ b/src/portable/wch/dcd_ch32h417_usb30.c
@@ -26,13 +26,19 @@
// USB3.0 SuperSpeed device driver for the WCH CH32H417/CH32H416 USBSS controller
// (@0x40034000), documented in CH32H417RM-EN chapter 27. The LINK layer is the same IP as the
-// CH569 (see dcd_ch56x_usb30.c) but the endpoint engine is a reworked chain-DMA design:
-// hardware sequence numbers and ERDY, per-endpoint halt (RB_EP_TX/RX_HALT), per-chain
-// completion flags. Software drives the LTSSM through the USBSS_LINK interrupt.
+// CH569 (dcd_ch56x_usb30.c) but the endpoint engine is a reworked chain-DMA design with
+// hardware sequence numbers and ERDY (SEQ_AUTO / ERDY_AUTO), per-endpoint persistent halt
+// (RB_EP_TX/RX_HALT), and per-chain completion flags. Software drives the LTSSM through the
+// USBSS_LINK interrupt. The init/PHY/LINK/EP0 register sequences are transcribed from the WCH
+// EVT USBSS device demo; the data-endpoint engine arms one chain per packet (burst 1) for
+// correctness-first bring-up, matching the CH569 port's initial path - raise the burst once the
+// hardware validates. Buffers may live anywhere in the shared SRAM (all DMA-reachable).
//
-// With CFG_TUD_WCH_USB30_FALLBACK this dcd owns both controllers: TIM12 times SuperSpeed
-// training and hands rhport 0 to the USB2 driver (ch32h417_usb2_* in dcd_ch32h417_usbhs.c)
-// when the host has no SuperSpeed port.
+// With CFG_TUD_WCH_USB30_FALLBACK this dcd owns both controllers: it counts failed SuperSpeed
+// training attempts (LINK DISABLE/INACTIVE) with a TIM12 backstop and hands rhport 0 to the USB2
+// driver (ch32h417_usb2_* in dcd_ch32h417_usbhs.c) when the host has no SuperSpeed port.
+//
+// NOTE: compile-verified; full hardware bring-up (usbtest at 5 Gbps) is pending.
#include "tusb_option.h"
@@ -40,7 +46,642 @@
defined(CFG_TUD_WCH_USBIP_USB30) && CFG_TUD_WCH_USBIP_USB30 == 1
#include "device/dcd.h"
+#include "dcd_ch32h417.h"
+#include "ch32h417_usb30_reg.h"
+
+#define EP_MAX 8
+#define EP0_MAX_SIZE 512
+
+#ifndef CFG_TUD_WCH_USB30_MAX_BURST
+#define CFG_TUD_WCH_USB30_MAX_BURST 1
+#endif
+
+typedef struct {
+ uint8_t *buffer;
+ uint16_t total_len;
+ uint16_t queued_len;
+ uint16_t max_size;
+ bool is_iso;
+ bool valid;
+} xfer_ctl_t;
+
+#define XFER_CTL_BASE(_ep, _dir) (&xfer_status[_ep][_dir])
+static xfer_ctl_t xfer_status[EP_MAX][2];
+
+// EP0 buffer in shared SRAM (DMA-reachable); SETUP lands here.
+TU_ATTR_ALIGNED(16) static uint8_t ep0_buffer[EP0_MAX_SIZE];
+static uint8_t ep0_tx_seq; // EP0 IN sequence number [4:0]
+static uint8_t pending_addr;
+static bool pending_addr_valid;
+
+//--------------------------------------------------------------------+
+// Runtime USB2 fallback state
+//--------------------------------------------------------------------+
+#if CFG_TUD_WCH_USB30_FALLBACK
+enum { FB_USB3_TRAINING, FB_USB3_UP, FB_USB2_ACTIVE };
+static uint8_t fb_state;
+static uint8_t fb_fail_count;
+#define FB_FAIL_LIMIT 3
+#endif
+
+//--------------------------------------------------------------------+
+// PHY / clock / link bring-up (transcribed from the WCH EVT USBSS demo)
+//--------------------------------------------------------------------+
+
+static void usbss_rcc_init(bool enable) {
+ if (enable) {
+ RCC->CTLR |= (uint32_t)RCC_USBSS_PLLON;
+ while ((RCC->CTLR & (uint32_t)RCC_USBSS_PLLRDY) != (uint32_t)RCC_USBSS_PLLRDY) {}
+ RCC_HBPeriphClockCmd(RCC_HBPeriph_USBSS, ENABLE);
+ RCC_PIPECmd(ENABLE);
+ RCC_UTMIcmd(ENABLE);
+ } else {
+ RCC_HBPeriphClockCmd(RCC_HBPeriph_USBSS, DISABLE);
+ RCC_UTMIcmd(DISABLE);
+ RCC_PIPECmd(DISABLE);
+ RCC->CTLR &= ~(uint32_t)RCC_USBSS_PLLON;
+ }
+}
+
+static uint32_t usbss_phy_cfg(uint8_t addr, uint16_t data) {
+ USBSS_PHY_CFG_CR = (1u << 23) | ((uint32_t)addr << 16) | data;
+ USBSS_PHY_CFG_DAT = 0x01;
+ return USBSS_PHY_CFG_DAT;
+}
+
+static void usbss_cfg_mod(void) {
+ usbss_phy_cfg(0x03, 0x7C12);
+ usbss_phy_cfg(0x0D, 0x79AA);
+ usbss_phy_cfg(0x15, 0x4430);
+ usbss_phy_cfg(0x13, 0x0010);
+ USBSS_PHY_MISC_REG = 0xB0054000;
+}
+
+static void ep_chain_init(void) {
+ // EP0 buffer for SETUP/data
+ USBSSD->UEP0_TX_CTRL = 0;
+ USBSSD->UEP0_RX_CTRL = 0;
+ USBSSD->UEP0_TX_DMA = (uint32_t)(uintptr_t)ep0_buffer;
+ USBSSD->UEP0_RX_DMA = (uint32_t)(uintptr_t)ep0_buffer;
+}
+
+static void usbss_device_init(bool enable) {
+ if (enable) {
+ usbss_rcc_init(true);
+
+ USBSSD->LINK_CFG = LINK_RX_EQ_EN | LINK_TX_DEEMPH_3_5DB | LINK_PHY_RESET;
+ USBSSD->LINK_CTRL = LINK_P2_MODE | LINK_GO_DISABLED;
+ USBSSD->LINK_CFG = LINK_RX_EQ_EN | LINK_TX_DEEMPH_3_5DB | LINK_LTSSM_MODE | LINK_TOUT_MODE;
+ USBSSD->LINK_LPM_CR |= LINK_LPM_EN;
+ USBSSD->LINK_CFG |= LINK_RX_TERM_EN;
+ USBSSD->LINK_INT_CTRL = LINK_IE_TX_LMP | LINK_IE_RX_LMP | LINK_IE_RX_LMP_TOUT | LINK_IE_STATE_CHG |
+ LINK_IE_WARM_RST | LINK_IE_TERM_PRES;
+ USBSSD->LINK_CTRL = LINK_P2_MODE;
+ USBSSD->LINK_U1_WKUP_TMR = 120;
+ USBSSD->LINK_U1_WKUP_FILTER = 50;
+ USBSSD->LINK_U2_WKUP_FILTER = 0;
+ USBSSD->LINK_U3_WKUP_FILTER = 0;
+
+ USBSSD->USB_CONTROL |= USBSS_FORCE_RST;
+ USBSSD->USB_STATUS = USBSS_UIF_TRANSFER;
+ USBSSD->USB_CONTROL = USBSS_UIE_TRANSFER | USBSS_UDIE_SETUP | USBSS_UDIE_STATUS | USBSS_DMA_EN | USBSS_SETUP_FLOW;
+
+ usbss_cfg_mod();
+
+ USBSSD->UEP_TX_EN = 0;
+ USBSSD->UEP_RX_EN = 0;
+ ep_chain_init();
+
+ NVIC_EnableIRQ(USBSS_IRQn);
+ NVIC_EnableIRQ(USBSS_LINK_IRQn);
+ } else {
+ NVIC_DisableIRQ(USBSS_LINK_IRQn);
+ NVIC_DisableIRQ(USBSS_IRQn);
+ USBSSD->USB_CONTROL = USBSS_FORCE_RST;
+ USBSSD->LINK_CFG |= LINK_PHY_RESET | U3_LINK_RESET;
+ for (volatile int i = 0; i < 2000; i++) {}
+ USBSSD->USB_CONTROL &= ~USBSS_FORCE_RST;
+ USBSSD->LINK_CFG &= ~(LINK_PHY_RESET | U3_LINK_RESET);
+ usbss_rcc_init(false);
+ }
+}
+
+// re-init the endpoint engine after a warm/hot reset (keeps the link up)
+static void usbss_reset_init(void) {
+ USBSSD->USB_CONTROL |= USBSS_FORCE_RST;
+ USBSSD->USB_STATUS = USBSS_UIF_TRANSFER;
+ USBSSD->USB_CONTROL = USBSS_UIE_TRANSFER | USBSS_UDIE_SETUP | USBSS_UDIE_STATUS | USBSS_DMA_EN | USBSS_SETUP_FLOW;
+ ep_chain_init();
+}
+
+//--------------------------------------------------------------------+
+// LINK layer (LTSSM) - state-change driven, transcribed from the demo
+//--------------------------------------------------------------------+
+
+static void handle_link_irq(uint8_t rhport) {
+ uint32_t link_int = USBSSD->LINK_INT_FLAG;
+ uint32_t link_state = USBSSD->LINK_STATUS & LINK_STATE_MASK;
+
+ if (link_int & LINK_IF_STATE_CHG) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_STATE_CHG;
+
+ switch (link_state) {
+ case LINK_STATE_DISABLE:
+ USBSSD->LINK_CTRL &= ~LINK_GO_DISABLED;
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB3_TRAINING && ++fb_fail_count >= FB_FAIL_LIMIT) {
+ // host has no SuperSpeed port: bring up USB2 on the same rhport
+ usbss_device_init(false);
+ fb_state = FB_USB2_ACTIVE;
+ ch32h417_usb2_init(rhport);
+ ch32h417_usb2_int_enable();
+ }
+#endif
+ break;
+
+ case LINK_STATE_HOTRST:
+ usbss_reset_init();
+ dcd_event_bus_reset(rhport, TUSB_SPEED_SUPER, true);
+ USBSSD->LINK_CTRL &= ~LINK_HOT_RESET;
+ break;
+
+ case LINK_STATE_U3:
+ dcd_event_bus_signal(rhport, DCD_EVENT_SUSPEND, true);
+ break;
+
+ case LINK_STATE_U0:
+ case LINK_STATE_RECOVERY:
+ case LINK_STATE_POLLING:
+ case LINK_STATE_RXDET:
+ default:
+ break;
+ }
+ } else if (link_int & LINK_IF_TERM_PRES) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_TERM_PRES;
+ } else if (link_int & LINK_IF_RX_LMP_TOUT) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_RX_LMP_TOUT;
+ USBSSD->LINK_CTRL |= LINK_GO_DISABLED;
+ USBSSD->LINK_CTRL |= LINK_GO_RX_DET;
+ } else if (link_int & LINK_IF_TX_LMP) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_TX_LMP;
+ USBSSD->LINK_LMP_TX_DATA0 = LMP_LINK_SPEED | LMP_PORT_CAP | LMP_HP;
+ USBSSD->LINK_LMP_TX_DATA1 = LMP_UP_STREAM | LMP_NUM_HP_BUF;
+ USBSSD->LINK_LMP_TX_DATA2 = 0;
+ } else if (link_int & LINK_IF_RX_LMP) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_RX_LMP;
+ uint32_t rx0 = USBSSD->LINK_LMP_RX_DATA0;
+ if ((rx0 & LMP_SUBTYPE_MASK) == LMP_PORT_CFG) {
+ // upstream port received Port Configuration: reply Port Config Response = SS training done
+ USBSSD->LINK_LMP_TX_DATA0 = LMP_LINK_SPEED | LMP_PORT_CFG_RES | LMP_HP;
+ USBSSD->LINK_LMP_TX_DATA1 = 0;
+ USBSSD->LINK_LMP_TX_DATA2 = 0;
+ USBSSD->LINK_LMP_PORT_CAP |= LINK_LMP_TX_CAP_VLD;
+#if CFG_TUD_WCH_USB30_FALLBACK
+ fb_state = FB_USB3_UP;
+ fb_fail_count = 0;
+#endif
+ dcd_event_bus_reset(rhport, TUSB_SPEED_SUPER, true);
+ } else if ((rx0 & LMP_SUBTYPE_MASK) == LMP_U2_INACT_TOUT) {
+ USBSSD->LINK_U2_INACT_TIMER = (rx0 >> 9) & 0xFF;
+ }
+ } else if (link_int & LINK_IF_WARM_RST) {
+ USBSSD->LINK_INT_FLAG = LINK_IF_WARM_RST;
+ if (USBSSD->LINK_STATUS & LINK_RX_WARM_RST) {
+ usbss_reset_init();
+ USBSSD->LINK_CTRL |= LINK_GO_DISABLED;
+ __NOP(); __NOP(); __NOP(); __NOP();
+ USBSSD->LINK_CTRL &= ~LINK_GO_DISABLED;
+ dcd_event_bus_reset(rhport, TUSB_SPEED_SUPER, true);
+ }
+ }
+}
+
+//--------------------------------------------------------------------+
+// EP0 control
+//--------------------------------------------------------------------+
+
+static void ep0_arm_in(uint16_t len) {
+ USBSSD->UEP0_TX_CTRL = USBSS_EP0_TX_DPH | len | ((uint32_t)ep0_tx_seq << 16);
+ USBSSD->UEP0_TX_CTRL |= USBSS_EP0_TX_ERDY;
+ ep0_tx_seq = (ep0_tx_seq + 1) & 0x1F;
+}
+
+static void ep0_arm_out(void) {
+ USBSSD->UEP0_RX_CTRL = USBSS_EP0_RX_ERDY | USBSS_EP0_RX_ACK;
+}
+
+static void handle_setup(uint8_t rhport) {
+ ep0_tx_seq = 0;
+ pending_addr_valid = false;
+ xfer_status[0][TUSB_DIR_IN].valid = false;
+ xfer_status[0][TUSB_DIR_OUT].valid = false;
+ dcd_event_setup_received(rhport, ep0_buffer, true);
+}
+
+static void handle_ep0_in(uint8_t rhport) {
+ xfer_ctl_t *xfer = XFER_CTL_BASE(0, TUSB_DIR_IN);
+ if (!xfer->valid) {
+ return;
+ }
+ uint16_t remaining = xfer->total_len - xfer->queued_len;
+ if (remaining == 0) {
+ xfer->valid = false;
+ dcd_event_xfer_complete(rhport, 0x80, xfer->queued_len, XFER_RESULT_SUCCESS, true);
+ return;
+ }
+ uint16_t len = TU_MIN(remaining, (uint16_t)EP0_MAX_SIZE);
+ memcpy(ep0_buffer, &xfer->buffer[xfer->queued_len], len);
+ xfer->queued_len += len;
+ ep0_arm_in(len);
+}
+
+static void handle_ep0_out(uint8_t rhport) {
+ xfer_ctl_t *xfer = XFER_CTL_BASE(0, TUSB_DIR_OUT);
+ if (!xfer->valid) {
+ return;
+ }
+ uint16_t rx_len = USBSSD->UEP0_RX_CTRL & USBSS_EP0_RX_LEN_MASK;
+ uint16_t remaining = xfer->total_len - xfer->queued_len;
+ uint16_t len = TU_MIN(rx_len, remaining);
+ memcpy(&xfer->buffer[xfer->queued_len], ep0_buffer, len);
+ xfer->queued_len += len;
+
+ if (xfer->queued_len >= xfer->total_len || len < EP0_MAX_SIZE) {
+ xfer->valid = false;
+ dcd_event_xfer_complete(rhport, 0x00, xfer->queued_len, XFER_RESULT_SUCCESS, true);
+ } else {
+ ep0_arm_out();
+ }
+}
+
+//--------------------------------------------------------------------+
+// Data endpoints - single chain armed per packet (burst 1)
+//--------------------------------------------------------------------+
+
+static void queue_in_packet(uint8_t ep_num, xfer_ctl_t *xfer) {
+ uint16_t remaining = xfer->total_len - xfer->queued_len;
+ uint16_t tx_len = TU_MIN(remaining, xfer->max_size);
+ volatile USBSS_EP_TX_TypeDef *tx = usbss_ep_tx(ep_num);
+
+ tx->UEP_TX_DMA = (uint32_t)(uintptr_t)&xfer->buffer[xfer->queued_len];
+ tx->UEP_TX_DMA_OFS = xfer->max_size;
+ tx->UEP_TX_CHAIN_LEN = tx_len;
+ tx->UEP_TX_CHAIN_EXP_NUMP = 1; // arms the chain
+ xfer->queued_len += tx_len;
+}
+
+static void queue_out_packet(uint8_t ep_num, xfer_ctl_t *xfer) {
+ volatile USBSS_EP_RX_TypeDef *rx = usbss_ep_rx(ep_num);
+ rx->UEP_RX_DMA = (uint32_t)(uintptr_t)&xfer->buffer[xfer->queued_len];
+ rx->UEP_RX_DMA_OFS = xfer->max_size;
+ rx->UEP_RX_CHAIN_MAX_NUMP = 1; // arms the chain
+}
+
+static void handle_ep_in(uint8_t rhport, uint8_t ep_num) {
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_IN);
+ volatile USBSS_EP_TX_TypeDef *tx = usbss_ep_tx(ep_num);
+ tx->UEP_TX_CHAIN_ST |= USBSS_EP_TX_CHAIN_IF; // release completed chain
+
+ if (!xfer->valid) {
+ return;
+ }
+ if (xfer->queued_len >= xfer->total_len) {
+ xfer->valid = false;
+ dcd_event_xfer_complete(rhport, ep_num | TUSB_DIR_IN_MASK, xfer->queued_len, XFER_RESULT_SUCCESS, true);
+ } else {
+ queue_in_packet(ep_num, xfer);
+ }
+}
-// implementation lands with the SPEED=super bring-up stage
+static void handle_ep_out(uint8_t rhport, uint8_t ep_num) {
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_OUT);
+ volatile USBSS_EP_RX_TypeDef *rx = usbss_ep_rx(ep_num);
+ uint16_t rx_len = rx->UEP_RX_CHAIN_LEN;
+ rx->UEP_RX_CHAIN_ST |= USBSS_EP_RX_CHAIN_IF; // release completed chain
+
+ if (!xfer->valid) {
+ return;
+ }
+ uint16_t remaining = xfer->total_len - xfer->queued_len;
+ uint16_t len = TU_MIN(rx_len, TU_MIN(remaining, xfer->max_size));
+ xfer->queued_len += len;
+
+ if (xfer->queued_len >= xfer->total_len || len < xfer->max_size) {
+ xfer->valid = false;
+ dcd_event_xfer_complete(rhport, ep_num, xfer->queued_len, XFER_RESULT_SUCCESS, true);
+ } else {
+ queue_out_packet(ep_num, xfer);
+ }
+}
+
+//--------------------------------------------------------------------+
+// USB (transfer) interrupt dispatch
+//--------------------------------------------------------------------+
+
+static void handle_usb_irq(uint8_t rhport) {
+ uint32_t status = USBSSD->USB_STATUS;
+
+ if ((status & USBSS_UDIF_SETUP) && !(status & USBSS_UDIF_STATUS)) {
+ USBSSD->USB_STATUS = USBSS_UDIF_SETUP;
+ handle_setup(rhport);
+ } else if (status & USBSS_UDIF_STATUS) {
+ USBSSD->USB_STATUS = USBSS_UDIF_STATUS;
+ if (pending_addr_valid) {
+ USBSSD->USB_CONTROL = (USBSSD->USB_CONTROL & 0x00FFFFFF) | ((uint32_t)pending_addr << 24);
+ pending_addr_valid = false;
+ }
+ USBSSD->UEP0_TX_CTRL = 0;
+ USBSSD->UEP0_RX_CTRL = 0;
+ } else if (status & USBSS_UIF_TRANSFER) {
+ uint8_t ep_num = USBSS_STATUS_EP_NUM(status);
+ bool is_in = USBSS_STATUS_EP_IN(status);
+ if (ep_num == 0) {
+ if (is_in) { handle_ep0_in(rhport); } else { handle_ep0_out(rhport); }
+ } else if (is_in) {
+ handle_ep_in(rhport, ep_num);
+ } else {
+ handle_ep_out(rhport, ep_num);
+ }
+ }
+}
+
+//--------------------------------------------------------------------+
+// TIM12 fallback backstop timer (0.5 s), direct-register
+//--------------------------------------------------------------------+
+#if CFG_TUD_WCH_USB30_FALLBACK
+static void fallback_timer_start(bool enable) {
+ if (enable) {
+ RCC_HB2PeriphClockCmd(RCC_HB2Periph_TIM12, ENABLE);
+ RCC_ClocksTypeDef clk;
+ RCC_GetClocksFreq(&clk);
+ TIM12->PSC = (uint16_t)(clk.HCLK_Frequency / 10000 - 1);
+ TIM12->ATRLR = 5000 - 1; // 0.5 s
+ TIM12->CNT = 0;
+ TIM12->INTFR = 0;
+ TIM12->DMAINTENR |= TIM_IT_Update;
+ TIM12->CTLR1 |= TIM_CEN;
+ NVIC_EnableIRQ(TIM12_IRQn);
+ } else {
+ NVIC_DisableIRQ(TIM12_IRQn);
+ TIM12->CTLR1 &= (uint16_t)~TIM_CEN;
+ TIM12->DMAINTENR &= (uint16_t)~TIM_IT_Update;
+ }
+}
+
+static void handle_timer_irq(uint8_t rhport) {
+ if (!(TIM12->INTFR & TIM_IT_Update)) {
+ return;
+ }
+ TIM12->INTFR = (uint16_t)~TIM_IT_Update;
+ if (fb_state == FB_USB3_TRAINING && ++fb_fail_count >= FB_FAIL_LIMIT) {
+ fallback_timer_start(false);
+ usbss_device_init(false);
+ fb_state = FB_USB2_ACTIVE;
+ ch32h417_usb2_init(rhport);
+ ch32h417_usb2_int_enable();
+ }
+}
+#endif
+
+//--------------------------------------------------------------------+
+// dcd API
+//--------------------------------------------------------------------+
+
+bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
+ (void)rhport;
+ (void)rh_init;
+ memset(&xfer_status, 0, sizeof(xfer_status));
+ ep0_tx_seq = 0;
+ pending_addr_valid = false;
+
+#if CFG_TUD_WCH_USB30_FALLBACK
+ fb_state = FB_USB3_TRAINING;
+ fb_fail_count = 0;
+#endif
+
+ usbss_device_init(true);
+
+#if CFG_TUD_WCH_USB30_FALLBACK
+ fallback_timer_start(true);
+#endif
+ return true;
+}
+
+void dcd_int_handler(uint8_t rhport) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) {
+ ch32h417_usb2_int_handler(rhport);
+ return;
+ }
+ handle_timer_irq(rhport);
+ if (fb_state == FB_USB2_ACTIVE) {
+ return;
+ }
+#endif
+ if (USBSSD->LINK_INT_FLAG & USBSSD->LINK_INT_CTRL) {
+ handle_link_irq(rhport);
+ }
+ if (USBSSD->USB_STATUS & (USBSS_UIF_TRANSFER | USBSS_UDIF_SETUP | USBSS_UDIF_STATUS)) {
+ handle_usb_irq(rhport);
+ }
+}
+
+void dcd_int_enable(uint8_t rhport) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_int_enable(); return; }
+#endif
+ (void)rhport;
+ NVIC_EnableIRQ(USBSS_IRQn);
+ NVIC_EnableIRQ(USBSS_LINK_IRQn);
+}
+
+void dcd_int_disable(uint8_t rhport) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_int_disable(); return; }
+#endif
+ (void)rhport;
+ NVIC_DisableIRQ(USBSS_IRQn);
+ NVIC_DisableIRQ(USBSS_LINK_IRQn);
+}
+
+void dcd_set_address(uint8_t rhport, uint8_t dev_addr) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt0_status_complete(rhport, NULL); }
+#endif
+ // apply the address at the status stage (USBSS_UDIF_STATUS), per the SIE
+ pending_addr = dev_addr;
+ pending_addr_valid = true;
+ dcd_edpt_xfer(rhport, 0x80, NULL, 0, false); // ZLP status
+}
+
+void dcd_remote_wakeup(uint8_t rhport) {
+ (void)rhport;
+ USBSSD->LINK_CTRL |= LINK_TX_UX_EXIT;
+}
+
+void dcd_sof_enable(uint8_t rhport, bool en) {
+ (void)rhport;
+ (void)en;
+}
+
+void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t *request) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt0_status_complete(rhport, request); return; }
+#endif
+ (void)rhport;
+ (void)request;
+}
+
+bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_edpt) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { return ch32h417_usb2_edpt_open(rhport, desc_edpt); }
+#endif
+ (void)rhport;
+ const uint8_t ep_num = tu_edpt_number(desc_edpt->bEndpointAddress);
+ const tusb_dir_t dir = tu_edpt_dir(desc_edpt->bEndpointAddress);
+ TU_ASSERT(ep_num < EP_MAX);
+ if (ep_num == 0) {
+ return true;
+ }
+
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, dir);
+ xfer->max_size = tu_edpt_packet_size(desc_edpt);
+ xfer->is_iso = (desc_edpt->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS);
+
+ if (dir == TUSB_DIR_IN) {
+ volatile USBSS_EP_TX_TypeDef *tx = usbss_ep_tx(ep_num);
+ tx->UEP_TX_CFG = USBSS_EP_TX_CHAIN_AUTO | USBSS_EP_TX_ERDY_AUTO | USBSS_EP_TX_SEQ_AUTO |
+ (xfer->is_iso ? USBSS_EP_TX_ISO_MODE : 0);
+ tx->UEP_TX_CR = (uint8_t)(USBSS_EP_TX_CLR | USBSS_EP_TX_CHAIN_CLR);
+ tx->UEP_TX_CR = CFG_TUD_WCH_USB30_MAX_BURST; // ERDY_NUMP advertised
+ USBSSD->UEP_TX_EN |= (uint16_t)(1u << ep_num);
+ } else {
+ volatile USBSS_EP_RX_TypeDef *rx = usbss_ep_rx(ep_num);
+ rx->UEP_RX_CFG = USBSS_EP_RX_CHAIN_AUTO | USBSS_EP_RX_ERDY_AUTO | USBSS_EP_RX_SEQ_AUTO |
+ (xfer->is_iso ? USBSS_EP_RX_ISO_MODE : 0);
+ rx->UEP_RX_CR = (uint8_t)(USBSS_EP_RX_CLR | USBSS_EP_RX_CHAIN_CLR);
+ rx->UEP_RX_CR = CFG_TUD_WCH_USB30_MAX_BURST;
+ USBSSD->UEP_RX_EN |= (uint16_t)(1u << ep_num);
+ }
+ return true;
+}
+
+void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt_close(rhport, ep_addr); return; }
+#endif
+ (void)rhport;
+ const uint8_t ep_num = tu_edpt_number(ep_addr);
+ const tusb_dir_t dir = tu_edpt_dir(ep_addr);
+ if (dir == TUSB_DIR_IN) {
+ usbss_ep_tx(ep_num)->UEP_TX_CR = (uint8_t)(USBSS_EP_TX_CLR | USBSS_EP_TX_CHAIN_CLR);
+ USBSSD->UEP_TX_EN &= (uint16_t)~(1u << ep_num);
+ } else {
+ usbss_ep_rx(ep_num)->UEP_RX_CR = (uint8_t)(USBSS_EP_RX_CLR | USBSS_EP_RX_CHAIN_CLR);
+ USBSSD->UEP_RX_EN &= (uint16_t)~(1u << ep_num);
+ }
+ XFER_CTL_BASE(ep_num, dir)->valid = false;
+}
+
+void dcd_edpt_close_all(uint8_t rhport) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt_close_all(rhport); return; }
+#endif
+ (void)rhport;
+ for (uint8_t ep = 1; ep < EP_MAX; ep++) {
+ usbss_ep_tx(ep)->UEP_TX_CR = (uint8_t)(USBSS_EP_TX_CLR | USBSS_EP_TX_CHAIN_CLR);
+ usbss_ep_rx(ep)->UEP_RX_CR = (uint8_t)(USBSS_EP_RX_CLR | USBSS_EP_RX_CHAIN_CLR);
+ xfer_status[ep][0].valid = false;
+ xfer_status[ep][1].valid = false;
+ }
+ USBSSD->UEP_TX_EN = 0;
+ USBSSD->UEP_RX_EN = 0;
+}
+
+bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) {
+ (void)is_isr;
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { return ch32h417_usb2_edpt_xfer(rhport, ep_addr, buffer, total_bytes); }
+#endif
+ const uint8_t ep_num = tu_edpt_number(ep_addr);
+ const tusb_dir_t dir = tu_edpt_dir(ep_addr);
+
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, dir);
+ xfer->buffer = buffer;
+ xfer->total_len = total_bytes;
+ xfer->queued_len = 0;
+ xfer->valid = true;
+
+ if (ep_num == 0) {
+ if (dir == TUSB_DIR_IN) {
+ if (total_bytes == 0) {
+ // ZLP status for a control-OUT/no-data transfer
+ USBSSD->UEP0_TX_CTRL = USBSS_EP0_TX_DPH;
+ USBSSD->UEP0_TX_CTRL |= USBSS_EP0_TX_ERDY;
+ ep0_arm_out();
+ } else {
+ handle_ep0_in(rhport);
+ }
+ } else {
+ if (total_bytes == 0) {
+ ep0_arm_out();
+ } else {
+ ep0_arm_out();
+ }
+ }
+ } else if (dir == TUSB_DIR_IN) {
+ queue_in_packet(ep_num, xfer);
+ } else {
+ queue_out_packet(ep_num, xfer);
+ }
+ return true;
+}
+
+void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt_stall(rhport, ep_addr); return; }
+#endif
+ (void)rhport;
+ const uint8_t ep_num = tu_edpt_number(ep_addr);
+ const tusb_dir_t dir = tu_edpt_dir(ep_addr);
+ if (ep_num == 0) {
+ USBSSD->UEP0_TX_CTRL = USBSS_EP0_TX_STALL;
+ USBSSD->UEP0_RX_CTRL = USBSS_EP0_RX_ERDY | USBSS_EP0_RX_STALL;
+ } else if (dir == TUSB_DIR_IN) {
+ usbss_ep_tx(ep_num)->UEP_TX_CR |= USBSS_EP_TX_HALT;
+ } else {
+ usbss_ep_rx(ep_num)->UEP_RX_CR |= USBSS_EP_RX_HALT;
+ }
+}
+
+void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
+#if CFG_TUD_WCH_USB30_FALLBACK
+ if (fb_state == FB_USB2_ACTIVE) { ch32h417_usb2_edpt_clear_stall(rhport, ep_addr); return; }
+#endif
+ (void)rhport;
+ const uint8_t ep_num = tu_edpt_number(ep_addr);
+ const tusb_dir_t dir = tu_edpt_dir(ep_addr);
+ if (ep_num == 0) {
+ return;
+ }
+ if (dir == TUSB_DIR_IN) {
+ volatile USBSS_EP_TX_TypeDef *tx = usbss_ep_tx(ep_num);
+ tx->UEP_TX_CR = (uint8_t)(USBSS_EP_TX_CLR | USBSS_EP_TX_CHAIN_CLR);
+ tx->UEP_TX_CR = CFG_TUD_WCH_USB30_MAX_BURST;
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_IN);
+ if (xfer->valid) {
+ xfer->queued_len = 0;
+ queue_in_packet(ep_num, xfer);
+ }
+ } else {
+ volatile USBSS_EP_RX_TypeDef *rx = usbss_ep_rx(ep_num);
+ rx->UEP_RX_CR = (uint8_t)(USBSS_EP_RX_CLR | USBSS_EP_RX_CHAIN_CLR);
+ rx->UEP_RX_CR = CFG_TUD_WCH_USB30_MAX_BURST;
+ xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_OUT);
+ if (xfer->valid) {
+ queue_out_packet(ep_num, xfer);
+ }
+ }
+}
#endif