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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeF2gZ1M7GWkz6Av4BpKPg
This commit is contained in:
hathach
2026-07-13 15:30:44 +07:00
parent 23242accff
commit 24f8bce0bc
6 changed files with 116 additions and 39 deletions

View File

@ -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

View File

@ -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_ */

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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