From 8456821fc2bf54297932fd7e8381c3fe50359b0a Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 17 Jun 2026 16:23:19 +0700 Subject: dcd/stm32_fsdev: make CH32 ep0_set_type read-modify-write atomic ep0_set_type() read the EP0 register outside any critical section and only ep_write() masked the USB IRQ around the store. A USB interrupt landing between the read and the write (e.g. a new SETUP whose handler installs the BULK gate) was silently undone when the task resumed and wrote back its pre-interrupt snapshot with the type forced to CONTROL, re-exposing the unsolicited EP0 OUT ACK the workaround blocks. Bracket the whole read-modify-write with fsdev_int_disable/enable when called with need_exclusive (task context). ISR-context callers pass false and are unaffected (the ISR cannot preempt itself). Co-Authored-By: Claude Fable 5 --- src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c index 957795e09..9c3431c20 100644 --- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c +++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c @@ -159,12 +159,19 @@ TU_ATTR_ALWAYS_INLINE static inline xfer_ctl_t *xfer_ctl_ptr(uint8_t epnum, uint } #if defined(TUP_USBIP_FSDEV_CH32) -// CH32 FSDEV workaround: gate EP0 handshakes by switching type between CONTROL and BULK. +// CH32 EP0 workaround: gate handshakes by switching EP0 type CONTROL<->BULK. +// need_exclusive brackets the read-modify-write so the USB ISR can't race it. TU_ATTR_ALWAYS_INLINE static inline void ep0_set_type(uint32_t ep_type, bool need_exclusive) { + if (need_exclusive) { + fsdev_int_disable(0); + } uint32_t ep_reg = ep_read(0) | U_EP_CTR_TX | U_EP_CTR_RX; ep_reg &= U_EPREG_MASK; ep_reg = (ep_reg & ~U_EP_T_FIELD) | ep_type; - ep_write(0, ep_reg, need_exclusive); + ep_write(0, ep_reg, false); + if (need_exclusive) { + fsdev_int_enable(0); + } } #endif -- cgit v1.3.1 From 47f534680215a68391a46cd45decde9b6f0a6573 Mon Sep 17 00:00:00 2001 From: hathach Date: Thu, 18 Jun 2026 15:35:55 +0700 Subject: dcd/stm32_fsdev: tidy CH32 EP0 setup parsing and whitespace Parse the setup packet via tusb_control_request_t instead of hand-rolled byte indexing; drop a stray blank line and fix indentation. No functional change. Co-Authored-By: Claude Fable 5 --- src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c index 9c3431c20..8dd790404 100644 --- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c +++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c @@ -291,7 +291,7 @@ static void handle_ctr_tx(uint32_t ep_id) { #if defined(TUP_USBIP_FSDEV_CH32) // Control read: block unsolicited EP0 OUT ACK. if ((ep_num == 0u) && ep0_ctrl_dir_in && ep0_ctrl_has_data) { - ep0_set_type(U_EP_BULK, false); + ep0_set_type(U_EP_BULK, false); } #endif dcd_event_xfer_complete(0, ep_num | TUSB_DIR_IN_MASK, xfer->queued_len, XFER_RESULT_SUCCESS, true); @@ -311,9 +311,9 @@ static void handle_ctr_setup(uint32_t ep_id) { // Setup packet should always be 8 bytes. If not, we probably missed the packet if (rx_count == 8) { #if defined(TUP_USBIP_FSDEV_CH32) - uint16_t const setup_w_length = (uint16_t) setup_packet[6] | ((uint16_t) setup_packet[7] << 8); - ep0_ctrl_dir_in = (setup_packet[0] & TUSB_DIR_IN_MASK) != 0u; - ep0_ctrl_has_data = (setup_w_length != 0u); + tusb_control_request_t const *request = (tusb_control_request_t const *) (void *) setup_packet; + ep0_ctrl_dir_in = (request->bmRequestType_bit.direction == TUSB_DIR_IN); + ep0_ctrl_has_data = (request->wLength != 0u); // For control write, block unsolicited EP0 OUT ACK until transfer is armed in edpt_xfer(). if (!ep0_ctrl_dir_in && ep0_ctrl_has_data) { @@ -366,7 +366,6 @@ static void handle_ctr_rx(uint32_t ep_id) { } xfer->queued_len += rx_count; - if ((rx_count < xfer->max_packet_size) || (xfer->queued_len >= xfer->total_len)) { // all bytes received or short packet -- cgit v1.3.1 From de341b14abe3bc3e25962cbb26aa61bc0126adcf Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 17 Jun 2026 16:26:38 +0700 Subject: dcd/stm32_fsdev: restore CH32 EP0 CONTROL type atomically with arming edpt_xfer() restored EP0 to CONTROL at the top of the function, before the OUT stage was armed (rx bufsize + STAT_RX=VALID written further down, with interrupts enabled in between). That re-enabled the CH32 blind OUT ACK while STAT_RX was still NAK and the buffer size stale, so a host-retried DATA OUT / status ZLP could be ACKed into the wrong buffer in the gap. Fold the CONTROL restore into the single exclusive write that programs STAT_RX=VALID for the OUT direction, after the buffer size is set, so type and arming go live together. The IN direction keeps the (now atomic) early restore, where no pending OUT exists to be blind-ACKed. Co-Authored-By: Claude Fable 5 --- src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c index 8dd790404..6279f8102 100644 --- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c +++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c @@ -754,14 +754,13 @@ static bool edpt_xfer(uint8_t rhport, uint8_t ep_num, tusb_dir_t dir) { xfer_ctl_t *xfer = xfer_ctl_ptr(ep_num, dir); const uint8_t ep_idx = xfer->ep_idx; + if (dir == TUSB_DIR_IN) { #if defined(TUP_USBIP_FSDEV_CH32) - // Re-enable normal control transfer semantics when EP0 transfer is explicitly armed. - if (ep_num == 0u) { - ep0_set_type(U_EP_CONTROL, true); - } + // Safe to restore CONTROL before arming IN: no pending OUT for the errata to blind-ACK. + if (ep_num == 0u) { + ep0_set_type(U_EP_CONTROL, true); + } #endif - - if (dir == TUSB_DIR_IN) { dcd_transmit_packet(xfer, ep_idx); } else { uint32_t ep_reg = ep_read(ep_idx) | U_EP_CTR_TX | U_EP_CTR_RX; // reserve CTR @@ -781,6 +780,13 @@ static bool edpt_xfer(uint8_t rhport, uint8_t ep_num, tusb_dir_t dir) { btable_set_rx_bufsize(ep_idx, BTABLE_BUF_RX, cnt); } +#if defined(TUP_USBIP_FSDEV_CH32) + // Restore CONTROL in the same write as STAT_RX=VALID (after bufsize): a separate earlier + // write would re-enable the blind OUT ACK while still NAK'd with a stale buffer. + if (ep_num == 0u) { + ep_reg = (ep_reg & ~U_EP_T_FIELD) | U_EP_CONTROL; + } +#endif ep_change_status(&ep_reg, dir, EP_STAT_VALID); ep_write(ep_idx, ep_reg, true); } -- cgit v1.3.1 From 6d74543ce6b3d79c2ba24e11f23fccad52571464 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 17 Jun 2026 16:27:03 +0700 Subject: dcd/stm32_fsdev: restore CH32 EP0 CONTROL type when stalling The CH32 workaround only restored EP0 to CONTROL in edpt_xfer(). When the stack rejects a control write with data (handle_ctr_setup having already switched EP0 to BULK) it stalls EP0 via dcd_edpt_stall() without ever calling dcd_edpt_xfer(), leaving EP0 typed BULK+STALL. As SETUP recognition is tied to CONTROL-typed endpoints, the host's recovery SETUP (which should auto-clear the stall) would be ignored, wedging EP0 until a bus reset. Restore CONTROL for EP0 in the same exclusive write that sets the STALL status. DFU download-in-wrong-state is a concrete trigger. Co-Authored-By: Claude Fable 5 --- src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c index 6279f8102..f5a8beb65 100644 --- a/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c +++ b/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c @@ -833,6 +833,14 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { ep_reg &= U_EPREG_MASK | EP_STAT_MASK(dir); ep_change_status(&ep_reg, dir, EP_STAT_STALL); +#if defined(TUP_USBIP_FSDEV_CH32) + // Stall ends the transfer without edpt_xfer() (the only other CONTROL restore); else a rejected + // control-write leaves EP0 typed BULK and the host's recovery SETUP is ignored until bus reset. + if (ep_num == 0u) { + ep_reg = (ep_reg & ~U_EP_T_FIELD) | U_EP_CONTROL; + } +#endif + ep_write(ep_idx, ep_reg, true); } -- cgit v1.3.1