summaryrefslogtreecommitdiff
path: root/src
AgeCommit message (Collapse)Author
2026-06-29Bump version to 0.21.0; rework changelog, contributors, and docshathach
Release: - Bump version to 0.21.0 and regenerate tusb_option.h, library.json, repository.yml, sonar-project.properties, boards.rst, dependencies.rst - Add 0.21.0 release notes and split the changelog into per-release files under docs/info/changelog/ (date out of title, driver/class groups as sub-headings, DCD & HCD section after Device/Host stacks) Contributors: - Credit each release's PR authors in a Contributors section - Drop the curated contributors page; add MAINTAINERS.rst Docs: - Update Code of Conduct to Contributor Covenant 3.0 (keep it in the repo, remove it from the built docs) - Sidebar: add a home entry, rename the group to "Documentation", move Changelog into it, add a GitHub Sponsor button, merge external links, rename FAQ; hide the inline toctree on the landing page - Add the make-release skill and update the AGENTS.md release process Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26Merge branch 'hathach:master' into fix/dwc2-hcd-ls-hfir代码人生
2026-06-25hcd/dwc2: update wording on hfirHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-25Merge pull request #3616 from michaelajax/fix-stm32-usbcZixun LI
typec/stm32: fix UCPD sink bugs affecting compatibility with newer PD sources
2026-06-25fix(dwc2_hcd): correct HFIR for Low-Speed devices via internal FS PHYZhang, Zhenjiang
When a Low-Speed device is connected through the internal FS PHY, the effective PHY clock is 6MHz (HCFG_FSLS_PHYCLK_SEL_6MHZ), but phy_clock was incorrectly left at 48. This caused HFIR to be calculated as 47999 (~8ms SOF interval) instead of the correct 5999 (1ms SOF interval), breaking periodic endpoint scheduling. Fixes: LS mouse only receiving first HID report on OTG_HS + FS PHY
2026-06-24skip cable-plug trafficHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-24apply review suggestionsHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-24typec: add tuc_connect/tuc_disconnect apiHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-24typec: add stm32u5 supportHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-24cap tud_msc_scsi_cb bufsize to endpoint buffer sizedxbjavid
2026-06-24Merge pull request #3648 from hathach/ch32_fsdevHa Thach
dcd/stm32_fsdev: workaround CH32 EP0 premature OUT ACK
2026-06-23Merge remote-tracking branch 'tinyusb/master' into fix-stm32-usbcHiFiPhile
2026-06-23Merge pull request #3698 from dxbjavid/hid-report-desc-boundsZixun LI
bound item size to remaining length in hid report descriptor parser
2026-06-22address reviewHiFiPhile
2026-06-22Merge remote-tracking branch 'tinyusb/master' into pr-osal-spin-deinitHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-22usbd: add osal_spin_deinit() to tud_deinit()HiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-22usbh: remove Pico specific commentHiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-22osal: add missing osal_spin_deinit()HiFiPhile
Signed-off-by: HiFiPhile <[email protected]>
2026-06-22Merge remote-tracking branch 'origin/master' into add-ch58x-usbfshathach
2026-06-22tusb_option: dedupe OPT_MCU_CH583 / make OPT_MCU_CH582 a token aliashathach
The CH58x->CH583 rename left OPT_MCU_CH583 defined twice at 2240 plus a second literal 2240 for OPT_MCU_CH582 -- a duplicate public MCU option macro (redefinition). Collapse to a single canonical OPT_MCU_CH583 and define OPT_MCU_CH582 as a token alias of it (matching the OPT_MCU_SAML21/MIMXRT10XX alias style), so there is one value and CH582 still selects the CH583 code path. Found by Codex review. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22hw/bsp+wch: rename the CH58x family to ch583 and OPT_MCU_CH58X to OPT_MCU_CH583hathach
The BSP family and MCU option were named "ch58x"/"CH58X", but the supported part is the CH583/CH582 (and the SDK repo is openwch/ch583); CH585 is a separate MCU family, so the CH58x umbrella was misleading. Rename to the specific family: - hw/bsp/ch58x -> hw/bsp/ch583 (dir), and the BSP-local files ch58x_it.* -> ch583_it.*, system_ch58x.* -> system_ch583.* (include guards/refs updated). The vendor SDK files (CH58x_common.h, CH58x_*.c in hw/mcu/wch/ch583) keep their names. - OPT_MCU_CH58X -> OPT_MCU_CH583 in tusb_option.h, tusb_mcu.h, and the shared WCH USBFS driver (ch32_usbfs_reg.h, dcd_ch32_usbfs.c). OPT_MCU_CH582 is kept as an alias (same value), so either name selects the same code. - FAMILY_MCUS CH58X -> CH583, CFG_TUSB_MCU=OPT_MCU_CH583, mcu:CH58X -> mcu:CH583 in the example skip lists, the CI build matrix (ci_set_matrix.py), the get_deps family tag, and docs/reference/boards.rst. Board names (ch582m_evt, yd-ch582m) are unchanged. Verified: make + cmake build for ch582m_evt, and ci.lan HIL (all device examples pass). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-21fix(host): release spinlock in tuh_deinit to fix multi-rebuild panicrenjie
tuh_rhport_init() calls osal_spin_init(&_usbh_spin), which under OPT_OS_PICO claims a hardware spinlock via critical_section_init(). There was no osal_spin_deinit(), so tuh_deinit() never released it: every host init/deinit cycle leaked one spinlock. RP2350 has a small spinlock pool, so a few usb_host rebuilds exhaust it and hw_claim_unused_from_range() panics (the long-standing "crashes on the 4th rebuild" bug). Add osal_spin_deinit() to all OSAL backends (critical_section_deinit for pico; no-op for none/freertos) and call it in tuh_deinit(). Verified 20/20 deinit+rebuild cycles on RP2350-Zero and Waveshare RP2350-USB-A (previously panicked on the 4th). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-20dcd/wch: drop toggle-mismatched OUT packets on all USBFS variantshathach
The OUT data-toggle check -- drop a packet whose DATA0/DATA1 doesn't match the expected toggle (a host retransmit after a lost ACK, or a host that doesn't alternate the toggle) -- only ran on CH58x. The auto-toggle parts (V103/V20x/V307/ X035) never checked it, so a duplicate/retransmitted OUT was processed twice. HiFiPhile confirmed it: a host patched to send DATA0-only had CH32V305 accept every packet. Move the TOG_OK gate out of the CH58x-only block so it runs on every variant; the manual toggle flip stays CH58x-only. EP0 keeps its own toggle via the SETUP/status flow and is exempt. Verified on ci.lan HIL: ch582m_evt (CH58x), ch32v103r_r1_1v0 (V103), nanoch32v203 (V203) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: preserve the DEV_ADDR general-purpose bit on SET_ADDRESShathach
dcd_edpt0_status_complete() wrote the full SET_ADDRESS wValue into R8_USB_DEV_AD, clobbering bit 7, which on CH58x is a user general-purpose flag (only bits [6:0] are the device address). Mask to 7 bits and preserve bit 7, matching the removed dcd_ch58x_usbfs.c. CH58x-scoped; other parts keep the full write. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: drop stale EP0 transfer state on SETUPhathach
The PID_SETUP handler armed the new control transfer but left any in-flight EP0 transfer from the previous request marked valid, so a spurious EP0 IN/OUT could run update_in()/update_out() against stale state (the removed dcd_ch58x_usbfs.c invalidated both EP0 directions on every SETUP). Clear xfer[0] IN/OUT validity when a SETUP arrives. Applies to all WCH USBFS parts -- a new SETUP always supersedes a pending control xfer. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: report bus resume instead of a second suspendhathach
The USBFS SUSPEND interrupt fires on both the suspend and the resume edge, but the handler unconditionally posted DCD_EVENT_SUSPEND. On CH58x tud_resume_cb() therefore never ran, and a device that lowered clocks/power in tud_suspend_cb() was never told to restore them. Read MIS_ST's suspend bit (1 while suspended, 0 once resumed) to emit DCD_EVENT_RESUME on the wake edge -- what the removed dcd_ch58x_usbfs.c did. Scoped to CH58x via #if; the CH32 parts keep their existing behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: advance EP0 OUT data toggle for multi-packet control transfershathach
The manual-toggle ISR skipped EP0 entirely (if (ep != 0)), so EP0's RX data toggle was set to DATA1 once at SETUP and never advanced. A control-OUT whose data stage exceeds the EP0 packet size (a vendor/WebUSB OUT, a large HID SET_REPORT, or an HS DFU download) desynced on the second packet and stalled. EP0 has no hardware auto-toggle on CH58x (per the datasheet RB_UEP_AUTO_TOG applies only to EP1/2/3/5/6/7), so flip its RX toggle on every OUT and always process the packet -- restoring what the removed dcd_ch58x_usbfs.c did. The HIL examples keep their control-OUT data stages within a single packet, so this was latent. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: keep IRQ masked across the EP-arming RMW in dcd_edpt_xferhathach
dcd_edpt_xfer() re-enabled the USB interrupt before update_in() / ep_rx_set_response(), which read-modify-write the (combined) EP control register. On CH58x the ISR RMWs that same register to flip the manual data toggle, so a transfer interrupt landing mid-RMW could drop the toggle flip and desync the endpoint. Move dcd_int_enable() to after the arming so the whole sequence is atomic w.r.t. the ISR (matching the CH32X035 port #3703). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19dcd/ch58x: complete the EP register map and right-size EP buffershathach
Tidy the CH58x register/buffer layout the initial port left rough. Register map (USBOTG_FS_TypeDef): - Extend the struct to the full CH583/582 datasheet Table 17-2 map instead of stopping at UEP567_MOD (0x0E) with the per-endpoint registers living only in raw-address macros. - Express the per-endpoint DMA/length/control registers as arrays of 4-byte slots (ch58x_ep_dma_t / ch58x_ep_ctrl_t): EP0-3 DMA at 0x10, EP0-4 ctrl at 0x20, EP5-7 DMA/ctrl split to 0x54/0x64 (EP4 has no DMA register of its own; it shares EP0's). TU_VERIFY_STATIC pins the slot sizes and block offsets, so the EP_TX_LEN/EP_CTRL/EP_DMA macros walk each block by the 4-byte stride (pointer arithmetic off slot 0, so the unused ternary branch can't trip -Warray-bounds). - Gate the two driver sites on CFG_TUSB_MCU == OPT_MCU_CH58X directly rather than the CH32_USBFS_EP_REGS_CUSTOM alias, which was only ever defined in the CH58x branch. EP buffers (the data struct): - Replace buffer[EP_MAX][2][64] on CH58x with named per-endpoint buffers: EP0/EP4 use the dedicated 192B ep0_ep4_buffer, so the old array left buffer[0]/buffer[4] allocated-but-unused. - Drop EP3's oversized iso buffer (out[64] + in[1023]); EP3 is bulk-only on CH58x, so it uses a plain 128-byte buffer like the others. The data struct shrinks from ~2636 to 1292 bytes. - Keep the now uniformly-64-byte buffers safe: dcd_edpt_iso_alloc()/iso_activate() refuse isochronous on CH58x (no iso support; 8-bit T_LEN caps a packet at 255B), and update_in()/update_out() additionally cap each packet copy to 64 bytes so a class that ignores the iso-alloc result cannot run a memcpy past a buffer into a neighbour's. Non-CH58x parts (e.g. ch32v103) keep the struct-based macros, buffer[EP_MAX], and the iso buffer unchanged. Verified on ch582m_evt HIL (ci.lan): all device examples pass; ch32v103 build unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19video: assert usbd_edpt_iso_activate() result in _open_vs_itf()hathach
The isochronous streaming endpoint was activated with the usbd_edpt_iso_activate() return value ignored, unlike every neighbouring open in the same function (usbd_edpt_open() is wrapped in TU_ASSERT on both the non-ISO-alloc fallback and the bulk branch). When a DCD refuses the iso endpoint -- e.g. it has no isochronous support, or the requested packet size does not fit its endpoint buffers -- that failure was silently swallowed and the alternate setting was reported as opened, leaving the host streaming to an endpoint the device never armed. Wrap it in TU_ASSERT so the open fails cleanly and the refusal propagates, matching the adjacent endpoint-open calls. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-19Merge pull request #3709 from morse-cedricvandenbergh/ncm-default-link-state-cbZixun LI
ncm: add weak callback for initial link state
2026-06-18Merge pull request #3618 from hathach/copilot/fix-audio-bitfield-order-issuesHa Thach
fix: BE bitfield guards for audio structs and endian-safe wValue/wIndex extraction in UAC2 examples
2026-06-18Merge pull request #3701 from hathach/claude/issue-3696-20260612-0344Ha Thach
fix(stm32_fsdev): don't enable the unused USB wakeup EXTI IRQ (F1/F3/G4/L1)
2026-06-18fix(stm32_fsdev): don't enable the unused USB wakeup EXTI IRQ (F1/F3/G4/L1)hathach
The classic-USB STM32 fsdev driver enabled the EXTI-line USB wakeup interrupt (USBWakeUp_IRQn, and USBWakeUp_RMP_IRQn on the F3 remap path) in the NVIC, but never uses it: resume is serviced in-band via ISTR.WKUP in the USB_LP/HP ISR, and the driver never arms or clears that EXTI line. The wakeup EXTI interrupt is only needed to wake the core from STOP mode, which TinyUSB does not implement. Leaving its NVIC vector enabled lets it fire spuriously into an unhandled or looping vector -- the freeze reported in #3696 on STM32G473. USBWakeUp_IRQn is a valid, dedicated USB-wakeup-via-EXTI interrupt (e.g. stm32g473xx.h: =42 "USB Wakeup through EXTI line"), not an "unrelated interrupt"; it is simply unused here. - Comment out USBWakeUp_IRQn for F1/F3/G4/L1 and USBWakeUp_RMP_IRQn on the F3 remap path, kept in place so STOP-mode wakeup is a one-line re-enable. - Keep the STM32L1 USBWakeUp_IRQn -> USB_FS_WKUP_IRQn alias for that re-enable. - Document the rationale in fsdev_stm32.h with a TODO. - Comment out the matching USBWakeUp(_RMP)_IRQHandler in the F1/F3/G4 BSPs, and the FreeRTOS NVIC_SetPriority(USBWakeUp_IRQn) on F1/G4. Fixes #3696 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-18dcd/ch58x: drive CH582/583 with shared dcd_ch32_usbfs.chathach
Replace PR #3515's separate dcd_ch58x_usbfs.c / hcd_ch58x_usbfs.c with the shared WCH USBFS device driver (combined per-endpoint control, like CH32V103), adding two CH58x-specific behaviors guarded so CH32V103/V20x/V307 are unchanged: - CH32_USBFS_EP_MANUAL_TOG: CH58x's hardware AUTO_TOG does not stay in sync, so the ISR toggles DATA0/DATA1 manually and discards toggle-mismatched OUT packets. Fixes multi-packet bulk-IN (e.g. MSC READ10) that otherwise hung. - CH32_USBFS_EP4_SHARES_EP0: EP4 has no DMA register and overlays EP0's region (EP0[0:63] + EP4 OUT[64:127] + EP4 IN[128:191]); add a 192-byte shared buffer and buffer-pointer helpers (transparent for the other parts). Fixes cdc_dual_ports (Port1 is on EP4). Add the ch582m_evt board. Device only on USB0 (rhport 0): the shared hcd_ch32_usbfs.c is CH32V20x-specific and cannot drive CH58x, so host / USB2 (rhport 1) is left commented out in the BSP for easy re-add. Verified on ch582m_evt via local HIL: all device examples pass. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-18class/audio: remove unused audio20_control_request_thathach
After the UAC2 examples switched to tusb_control_request_t with TU_U16_HIGH/LOW() extraction, audio20_control_request_t is no longer referenced anywhere in the tree. It is a byte-overlay of the setup packet whose bChannelNumber/bControlSelector/bInterface/bEntityID sub-byte fields silently misread on big-endian once wValue/wIndex are converted to host order (tu_le16toh in dcd.h), so leaving it in the public header is a latent BE trap; the BE bitfield guard previously added to its bmRequestType_bit only masked that by guarding byte 0. Drop the struct entirely. Callers should use tusb_control_request_t and TU_U16_LOW/HIGH(wValue|wIndex), matching audio_device.c and the examples. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-18dcd/stm32_fsdev: restore CH32 EP0 CONTROL type when stallinghathach
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 <[email protected]>
2026-06-18dcd/stm32_fsdev: restore CH32 EP0 CONTROL type atomically with arminghathach
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 <[email protected]>
2026-06-18Merge remote-tracking branch 'origin/master' into pr-3618hathach
2026-06-18dcd/stm32_fsdev: tidy CH32 EP0 setup parsing and whitespacehathach
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 <[email protected]>
2026-06-18dcd/stm32_fsdev: make CH32 ep0_set_type read-modify-write atomichathach
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 <[email protected]>
2026-06-18Merge remote-tracking branch 'origin/master' into add-ch58x-usbfshathach
2026-06-18dcd/ch32_usbfs: support CH32V103 combined endpoint control registerhathach
CH32V103 uses the older USBFS IP: a single combined UEPn_CTRL register per endpoint (IN response in bits [1:0], OUT response in [3:2], shared auto-toggle, separate IN/OUT toggles) instead of the separate UEPn_TX_CTRL/UEPn_RX_CTRL bytes of the newer IP (CH32V20x/V307). The shared driver was written for the newer IP, so EP0 control transfers never worked on V103: the OUT response was written to a reserved byte and the IN write clobbered the OUT bits. - ch32_usbfs_reg.h: annotate the V103 register struct with byte offsets and add a union exposing the combined UEPn_CTRL at the UEPn_TX_CTRL offset; define CH32_USBFS_EP_CTRL_COMBINED and the combined-register bit positions. - dcd_ch32_usbfs.c: abstract EP control access behind ep_tx/rx_ctrl_set() (full write) and ep_tx/rx_set_response() (response-only RMW). The newer-IP path is unchanged; the combined path read-modify-writes the single register and arms the post-SETUP data stage at DATA1. - bsp/ch32v10x: implement board_get_unique_id() (real chip UID) and drop the CSR 0x800 writes that corrupted the QingKe V3 interrupt config and left all interrupts disabled (the USB ISR never ran). Verified on ch32v103r_r1_1v0: enumerates and passes HIL for cdc_msc, hid, msc, midi, mtp, dfu, etc. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-17Merge remote-tracking branch 'origin/master' into ch32_fsdevhathach
2026-06-17Merge pull request #3643 from hathach/musb_ep0_raceHa Thach
dcd/musb: defer EP0 SETUP during DATA_IN/STATUS race
2026-06-16ncm: add weak callback for initial link stateCedric Van den Bergh
netd_init resets link_is_up to a compile-time default, which is incorrect when the host reboots without power-cycling the device. Add tud_network_default_link_state_cb() so applications can return the actual physical link state. The weak default preserves existing CFG_TUD_NCM_DEFAULT_LINK_UP behaviour.
2026-06-16device: clamp EP0 OUT data copy to the control transfer buffer (#3705)Ha Thach
* device: clamp EP0 OUT data copy to the control transfer buffer usbd_control_xfer_cb() copied xferred_bytes from the EP0 bounce buffer into the requester's buffer with no bound. A non-compliant host that sends an OUT data packet larger than the control transfer's data_len (= min(len, wLength), the buffer capacity) would overflow that buffer and over-count total_xferred. Clamp xferred_bytes to the remaining buffer space before the memcpy and accounting.
2026-06-16dcd/musb: extract pipe0_data_stage_done() and fix two EP0 commentshathach
Cleanup from a code-review pass, no behavior change: - Replace the open-coded "last DATA packet" test (remain_wlength == 0 || len < CFG_TUD_ENDPOINT0_SIZE), duplicated in the edpt0_xfer DATA IN arm, pipe0_process_xfer_state_isr, and the DATA OUT drain, with one inline pipe0_data_stage_done() so IN and OUT can't drift. - Correct the xact_len comment (only the IN path reports it; OUT reports count0) and the dcd_edpt_stall comment (a deferred SETUP means the old transfer ended on the wire, not that its status stage was "seen"). Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-15dcd/musb: rename pipe0_process_status_isr -> pipe0_process_xfer_state_isrhathach
The helper advances the whole EP0 control state machine on a completion/confirmation IRQ — it dispatches on pipe0->state and also fires the DATA_IN completion, not just the status stage — so "process_status" undersold it. Matches the process_*_isr family. Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-15dcd/musb: harden EP0 DATA_OUT against short packet and host overrunhathach
Mirror the IN-side short-packet fix on the OUT drain: end the data stage (-> STATUS_IN) when wLength is received OR a short OUT packet (count0 < CFG_TUD_ENDPOINT0_SIZE) signals the host's end-of-data, not only when remain_wlength hits exactly 0. Also clamp the remain_wlength subtraction so a host that overruns wLength can't underflow it and strand the transfer. Without this, a control-OUT whose host sends fewer bytes than wLength left pipe0 in DATA_OUT; usbd then armed STATUS IN and tripped the split's TU_ASSERT(!dir_in). Found by /code-review; conformant hosts send exactly wLength so HIL was already green. Verified: HIL pass on ek_tm4c123gxl and max32666fthr (13/13 each). Co-Authored-By: Claude Fable 5 <[email protected]>