summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-07-13 15:30:44 +0700
committerhathach <[email protected]>2026-07-13 15:30:44 +0700
commit24f8bce0bc4a07a69f242ff1e790da90719e984d (patch)
treeae289f3c49c4738d84233d993e531be30bf5eb52
parent23242accfff0cfde25cc6c089d2e8dcd4d5560e4 (diff)
rusb2: EP0 OUT reliability, HS UTMI PHY power-up, FS-only build support
- EP0 OUT: park a back-to-back data-stage packet the DCP accepted before PID could go NAK and deliver it into the next armed chunk; flow-control the single-buffer control pipe between chunks (usbtest ctrl_out corruption); discard a packet parked while an OUT pipe was halted so BOT reset recovery's fresh CBW read can't receive stale WRITE data - HS UTMI PHY power-up per the FSP sequence, shared by dcd/hcd: CLKSEL programmed from the board XTAL (EK-RA8M1 runs 20 MHz; the 24 MHz reset default never locks) while DIRPD holds the PHY down, then timed release - hw/bsp(ra8m1_ek): fix U60CK divider macro - BSP_CFG_U60CK_DIV used the generic USB_CLOCK_DIV_8 encoding (7), which USB60CKDIVCR rejects, leaving the USBHS link domain at 480 MHz; the USB60-specific BSP_CLOCKS_USB60_CLOCK_DIV_8 (4) sticks and yields the required 60 MHz from PLL1P - support FS-only builds on the high-speed port: gate SYSCFG.HSE on TUD_OPT_HIGH_SPEED (RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED was a silent no-op) and always compile both hwfifo access widths - the FIFO width belongs to the module, not the link speed (FS builds corrupted odd-length tails: 16-bit access against MBW-32) - iso activate: reset stale pipe bookkeeping so a BRDY firing before the class re-arms can't replay a pre-SET_INTERFACE transfer; write PIPEBUF after PIPESEL selects the pipe (PIPESEL-windowed register) - clear-halt: re-assert BUF on a still-armed OUT pipe (usbtest case 29) - bound the D0FIFO ready spin so an undrained double-buffered IN pipe can't freeze the stack with the IRQ masked - usbtest example: cap interrupt mps at 64 on RUSB2 high speed (pipes 6-9 have a fixed 64-byte buffer, RA6M5 UM 29.1) Verified: usbtest 30/30 on ra6m5_ek (HS), ra4m1_ek (FS) and ra8m1_ek (FS-forced build on the HS port). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg
-rw-r--r--examples/device/usbtest/src/usb_descriptors.h8
-rw-r--r--hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h5
-rw-r--r--src/portable/renesas/rusb2/dcd_rusb2.c90
-rw-r--r--src/portable/renesas/rusb2/hcd_rusb2.c8
-rw-r--r--src/portable/renesas/rusb2/rusb2_ra.h42
-rw-r--r--src/tusb_option.h2
6 files changed, 116 insertions, 39 deletions
diff --git a/examples/device/usbtest/src/usb_descriptors.h b/examples/device/usbtest/src/usb_descriptors.h
index 8a033eca9..61b931bd0 100644
--- a/examples/device/usbtest/src/usb_descriptors.h
+++ b/examples/device/usbtest/src/usb_descriptors.h
@@ -57,7 +57,13 @@
#define USBTEST_INT_EP_MPS_FS 64
#define USBTEST_ISO_EP_MPS_FS 128
#endif
-#define USBTEST_INT_EP_MPS_HS 512
+// RUSB2 (Renesas RA) interrupt pipes 6-9 have a fixed 64-byte single buffer at any speed
+// (RA6M5 UM R01UH0891 sec 29.1: "Pipes 6 to 9: Interrupt transfer with 64-byte single buffer").
+#if TU_CHECK_MCU(OPT_MCU_RAXXX)
+ #define USBTEST_INT_EP_MPS_HS 64
+#else
+ #define USBTEST_INT_EP_MPS_HS 512
+#endif
#define USBTEST_ISO_EP_MPS_HS 512
// Compile-time capability maximum: sizes the source buffers / vendor epbufs for the largest
diff --git a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h
index f2f1ae0c9..25638f02b 100644
--- a/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h
+++ b/hw/bsp/ra/boards/ra8m1_ek/ra_gen/bsp_clock_cfg.h
@@ -51,6 +51,9 @@
#define BSP_CFG_CANFDCLK_DIV (BSP_CLOCKS_CANFD_CLOCK_DIV_8) /* CANFDCLK Div /8 */
#define BSP_CFG_I3CCLK_DIV (BSP_CLOCKS_I3C_CLOCK_DIV_3) /* I3CCLK Div /3 */
#define BSP_CFG_UCK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_5) /* UCK Div /5 */
-#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB_CLOCK_DIV_8) /* U60CK Div /8 */
+/* U60CK Div /8: PLL1P 480 MHz -> 60 MHz. Hand-fixed: Smart Configurator emitted the USB_ macro
+ * namespace (BSP_CLOCKS_USB_CLOCK_DIV_8 = 7, rejected by USB60CKDIVCR -> link clock ran at
+ * 480 MHz); configuration.xml already says u60ck.div.8, so keep the USB60_ macro if regenerating. */
+#define BSP_CFG_U60CK_DIV (BSP_CLOCKS_USB60_CLOCK_DIV_8)
#define BSP_CFG_OCTA_DIV (BSP_CLOCKS_OCTA_CLOCK_DIV_4) /* OCTASPICLK Div /4 */
#endif /* BSP_CLOCK_CFG_H_ */
diff --git a/src/portable/renesas/rusb2/dcd_rusb2.c b/src/portable/renesas/rusb2/dcd_rusb2.c
index 5e42f63f5..c93bab05e 100644
--- a/src/portable/renesas/rusb2/dcd_rusb2.c
+++ b/src/portable/renesas/rusb2/dcd_rusb2.c
@@ -43,6 +43,7 @@ typedef struct
static dcd_data_t _dcd;
+
//--------------------------------------------------------------------+
// INTERNAL OBJECT & FUNCTION DECLARATION
//--------------------------------------------------------------------+
@@ -190,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);
@@ -360,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;
@@ -386,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 {
@@ -460,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);
}
@@ -508,10 +536,7 @@ static void process_pipe_brdy(uint8_t rhport, unsigned num)
}
}
if (completed) {
- pipe->queued = false;
- 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);
}
}
@@ -636,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;
@@ -753,6 +767,10 @@ 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);
}
// Re-opening an endpoint must reuse its pipe: usbd_edpt_close() is a no-op on ISO_ALLOC ports,
@@ -770,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;
@@ -860,14 +877,12 @@ bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet
_dcd.ep[dir][epn] = num;
dcd_int_disable(rhport);
+ rusb->PIPESEL = (uint16_t) num;
if (rusb2_is_highspeed_rhport(rhport)) {
- // FIXME (as in dcd_edpt_open): PIPEBUF is a PIPESEL-windowed register (RA6M5 UM §29.2.35) so it
- // must be written AFTER PIPESEL selects this pipe, and the fixed BUFNMB=0x08 overlaps every
- // HS pipe — a real per-pipe buffer allocator is needed. Left as-is: no RA6M5 HS board on
- // the HIL rig to validate a change, and the current mis-ordered write is inert on FS/RA4M1.
+ // 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->PIPESEL = (uint16_t) num;
rusb->PIPEMAXP = largest_packet_size;
volatile uint16_t *ctr = get_pipectr(rusb, num);
*ctr = RUSB2_PIPE_CTR_ACLRM_Msk | RUSB2_PIPE_CTR_SQCLR_Msk;
@@ -893,6 +908,13 @@ bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *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;
@@ -904,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;
@@ -917,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;
@@ -952,6 +974,12 @@ 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;
+ // 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
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/tusb_option.h b/src/tusb_option.h
index e19ee1629..65cf747e2 100644
--- a/src/tusb_option.h
+++ b/src/tusb_option.h
@@ -376,7 +376,7 @@
//------------ RUSB2 --------------//
#if defined(TUP_USBIP_RUSB2)
#define CFG_TUD_EDPT_DEDICATED_HWFIFO 1
- #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | (TUD_OPT_HIGH_SPEED ? 4 : 0)) // 16 bit and 32 bit if highspeed
+ #define CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE (2 | 4) // HS module uses 32-bit access at any link speed (e.g. FS-forced build)
#define CFG_TUSB_FIFO_HWFIFO_ADDR_STRIDE 0
#define CFG_TUSB_FIFO_HWFIFO_CUSTOM_WRITE // custom write since rusb2 can change access width 32 -> 16 and can write
// odd byte with byte access