Skip to content

hcd(ch32_usbfs): pace isochronous transfers with SOF - #3839

Open
zjzhang-cn wants to merge 1 commit into
hathach:masterfrom
zjzhang-cn:feat/ch32-usbfs-sof-iso-pacing
Open

hcd(ch32_usbfs): pace isochronous transfers with SOF#3839
zjzhang-cn wants to merge 1 commit into
hathach:masterfrom
zjzhang-cn:feat/ch32-usbfs-sof-iso-pacing

Conversation

@zjzhang-cn

Copy link
Copy Markdown
Contributor

Summary

The USBFS SIE executes one transaction the moment HOST_EP_PID is
written, with no frame-boundary alignment, so a re-submitted
isochronous endpoint is polled at the task-loop rate instead of once
per 1 ms USB frame. This reworks the CH32V20x USBFS host driver so
isochronous transfers are paced by the SOF interrupt.

What changed

  • SOF-paced isochronous scheduling — each ISO endpoint is allowed
    at most one transaction per USB frame, armed from the SOF tick and
    from the ISO completion handler, with a one-slot pending queue per
    endpoint so an early same-frame re-submission cannot starve the
    opposite direction.
  • NAK retry with progressive backoff — NAK'd control/bulk
    transfers are re-armed once per frame from the SOF interrupt
    (1 → 2 → … → 64 frames) instead of a tight task-loop retry that
    floods the device's control endpoint and starves both sides.
  • Direct DMA to the application buffer — the 64-byte internal
    TX/RX buffers would be overflowed by larger packets (e.g. FS
    isochronous); they are now used only for setup, status-stage and
    zero-length transfers.
  • Port-reset DETECT edge suppression — DETECT(attach=0) edges
    asserted while the bus is in reset (SE0) are ignored, otherwise the
    reset is reported as a device removal and kills enumeration.
  • Transfer-flag disciplineUIE_TRANSFER stays enabled for the
    driver lifetime; the ISR STOPS (HOST_EP_PID = 0) before clearing
    UIF_TRANSFER to avoid re-arming the SIE with a stale PID;
    completions are routed by an xfer_type snapshot so a control
    transfer can never complete through the ISO path with a stale
    RX_LEN.
  • API completeness — implement hcd_edpt_close() and
    hcd_edpt_abort_xfer(); hcd_frame_number() returns the SOF frame
    counter.

Motivation

Host enumeration on CH32V20x USBFS was unreliable after the 0.20.0 →
0.21.0 window: a HID device that mounted fine with 0.20.0 failed to
mount with 0.21.0 on the same hardware.

Test / build evidence

  • ./tools/build.py -b nanoch32v203 -s make: 34 OK, 0 failed
  • Verified on hardware (nanoch32v203): host_info_to_device_cdc,
    host_hid_to_device_cdc, device_info, msc_file_explorer
  • clang-format and codespell clean; fresh host/device_info build
    with riscv32-wch-elf-gcc 15 passes

The USBFS SIE executes one transaction the moment HOST_EP_PID is
written, with no frame-boundary alignment, so a re-submitted
isochronous endpoint is polled at the task-loop rate instead of once
per 1 ms USB frame. Rework the host driver:

- Schedule isochronous transfers from the SOF interrupt: each ISO
  endpoint is allowed at most one transaction per USB frame, armed
  from the SOF tick and from the ISO completion handler, with a
  one-slot pending queue per endpoint so an early same-frame
  re-submission cannot starve the opposite direction.
- Retry NAK'd control/bulk transfers once per frame from the SOF
  interrupt with progressive backoff (1 -> 2 -> ... -> 64 frames)
  instead of a tight task-loop retry that floods the device.
- Point the DMA directly at the application buffer instead of the
  64-byte internal TX/RX buffers, which would be overflowed by larger
  packets (e.g. full-speed isochronous); the internal buffers are kept
  only for setup, status-stage and zero-length transfers.
- Ignore DETECT edges asserted during port reset (SE0) so the reset is
  not reported as a device removal that kills enumeration.
- Keep UIE_TRANSFER enabled for the driver lifetime and STOP
  (HOST_EP_PID = 0) before clearing UIF_TRANSFER to avoid re-arming
  the SIE with a stale PID; route completions by an xfer_type snapshot
  so a control transfer can never complete through the ISO path with a
  stale RX_LEN.
- Implement hcd_edpt_close() and hcd_edpt_abort_xfer(); return the SOF
  frame counter from hcd_frame_number().

Validated on nanoch32v203: all 34 board targets build (34 OK, 0
failed) and the host_info_to_device_cdc, host_hid_to_device_cdc,
device_info and msc_file_explorer examples run on hardware.
Copilot AI lite review requested due to automatic review settings August 21, 2026 03:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks the CH32V20x USBFS host driver with SOF-paced isochronous scheduling and improved transfer lifecycle handling.

Changes:

  • Adds SOF-based ISO scheduling, queuing, and frame tracking.
  • Adds NAK retry backoff, direct DMA, and reset/interrupt safeguards.
  • Implements endpoint close/abort APIs and frame-number reporting.
Suppressed comments (10)

src/portable/wch/hcd_ch32_usbfs.c:446

  • For a NAKed SETUP stage, hcd_int_handler identifies the record as EP0 OUT, so this derives USB_PID_OUT for the retry. A SETUP token must be retried as USB_PID_SETUP; otherwise a NAK during enumeration is retried as an ordinary data OUT packet and the control transfer cannot progress. Preserve the armed request PID in the NAK stash/current slot and use it here.
  uint8_t pid                       = (tu_edpt_dir(best->ep_addr) == TUSB_DIR_IN) ? USB_PID_IN : USB_PID_OUT;
  bool    prev                      = usbfs_irq_save();

src/portable/wch/hcd_ch32_usbfs.c:472

  • Isochronous transfers have no ACK/NAK handshake, but they can still complete with a CRC, timeout, or other host error. This path ignores the response/status entirely and always reports success; for a failed IN transaction RX_LEN can also be stale, so old data may be delivered as a fresh packet. Check the transfer status and report a failed/zero-length completion for non-success results.
// ISO transfer completion. ISO has no handshake (no NAK/STALL): every armed
// transaction completes successfully. RX_LEN holds received bytes for IN;
// for OUT the whole queued packet was sent.
static void iso_transfer_complete(uint8_t request_pid, usb_edpt_t *edpt) {
  uint16_t done_len = (request_pid == USB_PID_IN) ? (uint16_t)USBOTG_H_FS->RX_LEN : usb_current_xfer_info.bufferlen;

src/portable/wch/hcd_ch32_usbfs.c:373

  • The frame gate only compares against the current frame, and the endpoint record does not retain bInterval. Full-speed isochronous endpoints may specify service intervals greater than one frame, so this schedules tokens every frame instead of the interval from the descriptor. Store and honor the endpoint interval when deciding whether a queued ISO transfer is due.
      if (cur->configured && cur->xfer_type == TUSB_XFER_ISOCHRONOUS && cur->iso_queued && !cur->iso_active &&
          cur->iso_last_frame != g_sof_frame) {

src/portable/wch/hcd_ch32_usbfs.c:740

  • On disconnect this only clears the global in-flight slot; endpoint records still retain iso_queued/iso_active and is_nak_pending, while SOF generation remains enabled. Before the deferred remove event calls hcd_device_close(), a subsequent SOF can therefore arm a transfer for the detached device and emit a stale completion. Cancel the endpoint queues or gate the SOF armers as part of disconnect handling.
      // Drop any in-flight / queued ISO on disconnect.
      bool prev                     = usbfs_irq_save();
      usb_current_xfer_info.is_busy = false;
      USBOTG_H_FS->HOST_EP_PID      = 0;
      usbfs_irq_restore(prev);

src/portable/wch/hcd_ch32_usbfs.c:898

  • Resetting nak_backoff here covers data/status transfers, but hcd_setup_send() does not reset the same endpoint record's backoff. After one NAKed SETUP, subsequent control SETUP requests inherit the old exponential delay instead of starting with the documented one-frame retry.
  edpt_info->nak_backoff = 1; // fresh transfer: reset the progressive NAK backoff

src/portable/wch/hcd_ch32_usbfs.c:889

  • The non-ISO path does not reserve the shared in-flight slot atomically with its busy check. An SOF can arrive after the while exits but before is_busy is set, arm a queued ISO transfer, and then this code overwrites the global slot and USB registers with the control/bulk transfer, losing the ISO completion. Publish the non-ISO slot under usbfs_irq_save() before any preemptible work, then arm after restoring the IRQ.
  while (usb_current_xfer_info.is_busy) {}

  hardware_set_port_address_speed(dev_addr);

  usb_current_xfer_info.is_busy     = true;

src/portable/wch/hcd_ch32_usbfs.c:933

  • This setup submission has the same check-then-publish race as the endpoint transfer path: a SOF can arm a queued ISO between the busy-wait and this assignment, after which the setup state overwrites the shared slot. Reserve is_busy atomically with the wait before returning to interruptible code.
  while (usb_current_xfer_info.is_busy) {}

  usb_current_xfer_info.is_busy = true;

src/portable/wch/hcd_ch32_usbfs.c:756

  • hcd_int_handler() supports in_isr == false for polling, but this always passes true. A polled SOF handler consequently skips arm_iso_drain()'s IRQ critical section and can race an actual USB IRQ while publishing and arming the ISO slot. Propagate the handler's context flag instead of assuming every invocation is an ISR.
    arm_iso_drain(true);

src/portable/wch/hcd_ch32_usbfs.c:757

  • With a continuously queued ISO endpoint, arm_iso_drain() sets is_busy at every SOF, so arm_nak_retry() immediately returns. Since ISO completion only calls arm_iso_drain() again, a NAK-pending control/bulk transfer can be starved indefinitely whenever ISO traffic is continuous. The scheduler needs fair arbitration or a reserved retry opportunity.
    // ISO has priority over the NAK retry: isochronous transfers must keep
    // their 1/frame cadence even while a control transfer is NAKing (the retry
    // happens in the gaps). Reversing this starves ISO under a persistent
    // control NAK.
    arm_iso_drain(true);
    arm_nak_retry();

src/portable/wch/hcd_ch32_usbfs.c:419

  • The ISO path treats iso_len as one hardware packet and reports the whole requested length on OUT, but it never checks it against max_packet_size or retains a remaining length. A caller using the HCD's total-length transfer API with more than one packet will program an oversized ISO transaction or complete after only the first packet with an incorrect byte count. Either explicitly restrict ISO submissions to one packet or implement per-frame remainder handling.
      USBOTG_H_FS->HOST_TX_LEN = arm_len;
      hardware_start_xfer(USB_PID_OUT, arm_ep, 0, true);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +588 to +592
edpt->is_nak_pending = true;
edpt->buflen = usb_current_xfer_info.bufferlen;
edpt->buf = usb_current_xfer_info.buffer;
edpt->nak_xferred = usb_current_xfer_info.xferred_len;
edpt->nak_backoff = TU_MIN(edpt->nak_backoff * 2, 64);
Comment on lines +913 to +915
usb_edpt_t *edpt = get_edpt_record(dev_addr, ep_addr);
if (edpt == NULL || edpt->xfer_type != TUSB_XFER_ISOCHRONOUS) {
return false;
edpt->iso_active = false;
usbfs_irq_restore(prev);

hcd_event_xfer_complete(dev_addr, ep_addr, done_len, XFER_RESULT_SUCCESS, true);
Comment on lines +639 to +641
// Drop any in-flight / queued ISO so it does not resume after the reset.
usb_current_xfer_info.is_busy = false;
USBOTG_H_FS->HOST_EP_PID = 0;
} usb_edpt_t;

static usb_edpt_t usb_edpt_list[CFG_TUH_DEVICE_MAX * 6] = {};
static usb_edpt_t usb_edpt_list[CFG_TUH_DEVICE_MAX * CFG_TUH_ENDPOINT_MAX] = {};
Comment on lines +283 to +288
if (pid == USB_PID_IN) {
USBOTG_H_FS->HOST_RX_DMA =
(uint32_t)(usb_current_xfer_info.buffer != NULL ? usb_current_xfer_info.buffer : (uint8_t *)USBFS_RX_Buf);
} else {
USBOTG_H_FS->HOST_TX_DMA =
(uint32_t)(usb_current_xfer_info.buffer != NULL ? usb_current_xfer_info.buffer : (uint8_t *)USBFS_TX_Buf);
Comment on lines +408 to +412
if (!in_isr) {
usbfs_irq_restore(prev);
}

if (armed) {
@github-actions

Copy link
Copy Markdown

Size Difference Report

Because TinyUSB code size varies by port and configuration, the metrics below represent the averaged totals across all example builds.

Note: If there is no change, only one value is shown.

Changes >1% in size

file .text .rodata .data .bss size % diff
dcd_ci_hs.c 1771 ➙ 1941 (+170) 0 0 1344 ➙ 779 (-565) 2549 ➙ 2721 (+172) +6.7%
dcd_lpc_ip3511.c 1538 ➙ 1580 (+42) 0 0 264 1758 ➙ 1800 (+42) +2.4%
hcd_ch32_usbfs.c 2491 ➙ 3714 (+1223) 0 0 502 ➙ 2133 (+1631) 2993 ➙ 5847 (+2854) +95.4%
hcd_ci_hs.c 181 ➙ 186 (+5) 0 0 0 181 ➙ 186 (+5) +2.8%
midi2_device.c 3001 ➙ 3608 (+607) 34 1342 ➙ 1600 (+258) 555 ➙ 563 (+8) 3569 ➙ 4183 (+614) +17.2%
mtp_device.c 1715 ➙ 1781 (+66) 22 743 ➙ 771 (+28) 589 2312 ➙ 2378 (+66) +2.9%
ohci.c 1925 ➙ 2157 (+232) 0 0 2503 4428 ➙ 4660 (+232) +5.2%
usbd.c 3611 ➙ 3708 (+97) 57 90 ➙ 92 (+2) 354 4028 ➙ 4126 (+98) +2.4%
TOTAL 16233 ➙ 18675 (+2442) 113 2175 ➙ 2463 (+288) 6111 ➙ 7185 (+1074) 21818 ➙ 25901 (+4083) +18.7%
Changes <1% in size
file .text .rodata .data .bss size % diff
dcd_ch32_usbfs.c 1698 ➙ 1708 (+10) 0 0 1364 3062 ➙ 3072 (+10) +0.3%
dcd_ch32_usbhs.c 1922 0 0 481 2402 ➙ 2404 (+2) +0.1%
dcd_dwc2.c 4229 ➙ 4230 (+1) 19 0 265 4513 ➙ 4514 (+1) +0.0%
dcd_eptri.c 2273 ➙ 2281 (+8) 0 0 259 2532 ➙ 2540 (+8) +0.3%
dcd_lpc17_40.c 1843 ➙ 1842 (-1) 0 0 792 2239 ➙ 2238 (-1) -0.0%
dcd_mm32f327x_otg.c 1478 ➙ 1474 (-4) 0 0 1290 2768 ➙ 2764 (-4) -0.1%
dcd_musb.c 2666 ➙ 2667 (+1) 0 0 179 2845 ➙ 2846 (+1) +0.0%
dcd_samg.c 1322 ➙ 1326 (+4) 0 0 72 1394 ➙ 1398 (+4) +0.3%
ncm_device.c 1766 ➙ 1791 (+25) 28 821 ➙ 833 (+12) 4393 6172 ➙ 6197 (+25) +0.4%
usbtmc_device.c 2261 ➙ 2283 (+22) 24 68 ➙ 69 (+1) 311 2604 ➙ 2625 (+21) +0.8%
video_device.c 4446 ➙ 4477 (+31) 5 1235 ➙ 1245 (+10) 480 4919 ➙ 4949 (+30) +0.6%
TOTAL 25904 ➙ 26001 (+97) 76 2124 ➙ 2147 (+23) 9886 35450 ➙ 35547 (+97) +0.3%
No changes
file .text .rodata .data .bss size % diff
audio_device.c 2885 0 1252 1621 4501 +0.0%
cdc_device.c 1235 16 1092 722 1955 +0.0%
cdc_host.c 6479 487 15 946 7639 +0.0%
dcd_ci_fs.c 1955 0 0 1290 3245 +0.0%
dcd_da146xx.c 3067 0 0 144 3211 +0.0%
dcd_ft9xx.c 3284 0 0 172 3456 +0.0%
dcd_msp430x5xx.c 1801 0 0 176 1977 +0.0%
dcd_nrf5x.c 2974 0 0 292 3266 +0.0%
dcd_nuc120.c 1096 0 0 78 1174 +0.0%
dcd_nuc121.c 1170 0 0 101 1271 +0.0%
dcd_nuc505.c 0 0 1533 157 1690 +0.0%
dcd_rp2040.c 1004 0 764 653 2420 +0.0%
dcd_rusb2.c 3346 0 0 156 3502 +0.0%
dcd_samd.c 1071 0 0 266 1337 +0.0%
dcd_stm32_fsdev.c 2568 0 0 291 2859 +0.0%
dfu_device.c 776 28 712 134 910 +0.0%
dfu_rt_device.c 157 0 134 0 157 +0.0%
dwc2_common.c 603 22 0 0 615 +0.0%
ecm_rndis_device.c 1067 0 1 2759 3826 +0.0%
ehci.c 2763 0 0 6274 7783 +0.0%
fsdev_common.c 182 0 0 0 182 +0.0%
hcd_ci_fs.c 2466 0 0 469 2936 +0.0%
hcd_dwc2.c 5071 25 1 545 5642 +0.0%
hcd_musb.c 3071 0 0 157 3228 +0.0%
hcd_pio_usb.c 262 0 240 0 502 +0.0%
hcd_rp2040.c 1996 17 4 321 2338 +0.0%
hcd_rusb2.c 2951 0 0 245 3196 +0.0%
hcd_samd.c 2220 0 0 324 2544 +0.0%
hcd_stm32_fsdev.c 3248 0 1 420 3670 +0.0%
hid_device.c 1121 44 997 119 1240 +0.0%
hid_host.c 1247 0 0 1288 2534 +0.0%
hub.c 1385 8 8 30 1419 +0.0%
midi2_host.c 1802 0 0 5921 7723 +0.0%
midi_device.c 1148 0 1007 614 1759 +0.0%
midi_host.c 1339 7 7 3538 4880 +0.0%
msc_device.c 2522 108 2293 802 3325 +0.0%
msc_host.c 1625 0 0 395 2020 +0.0%
printer_device.c 827 0 706 555 1380 +0.0%
rp2040_usb.c 386 35 632 11 1065 +0.0%
rusb2_common.c 160 0 16 0 176 +0.0%
tusb.c 449 0 389 3 451 +0.0%
tusb_fifo.c 855 0 486 0 850 +0.0%
typec_stm32.c 1230 8 2 19 1255 +0.0%
usbc.c 500 2 20 166 688 +0.0%
usbh.c 4979 57 81 1173 6256 +0.0%
vendor_device.c 1132 0 538 1561 2685 +0.0%
TOTAL 83475 864 12931 34908 120738 +0.0%

@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

Top 9 targets by memory change (%) (out of 2491 targets) View Project Dashboard →

target .text .rodata .data .bss total % diff
ch32v203c_r0_1v0/device_info 19,144 → 20,408 (+1,264) 2,436 → 4,356 (+1,920) 24,228 → 27,412 (+3,184) +13.1%
ch32v203c_r0_1v0/midi_rx 20,540 → 21,832 (+1,292) 3,340 → 5,260 (+1,920) 26,544 → 29,756 (+3,212) +12.1%
ch32v203c_r0_1v0/midi2_host 21,680 → 22,912 (+1,232) 6,864 → 8,784 (+1,920) 31,196 → 34,348 (+3,152) +10.1%
ch32v203c_r0_1v0/host_info_to_device_cdc 31,296 → 32,628 (+1,332) 3,212 → 5,132 (+1,920) 37,248 → 40,500 (+3,252) +8.7%
ch32v203c_r0_1v0/host_hid_to_device_cdc 31,360 → 32,600 (+1,240) 4,632 → 6,552 (+1,920) 38,728 → 41,888 (+3,160) +8.2%
ch32v203c_r0_1v0/cdc_msc_hid 32,344 → 33,560 (+1,216) 5,316 → 7,236 (+1,920) 40,340 → 43,476 (+3,136) +7.8%
ch32v203c_r0_1v0/bare_api 20,032 → 21,232 (+1,200) 3,060 → 3,828 (+768) 25,740 → 27,708 (+1,968) +7.6%
ch32v203c_r0_1v0/hid_controller 18,388 → 19,592 (+1,204) 1,716 → 2,196 (+480) 22,688 → 24,372 (+1,684) +7.4%
ch32v203c_r0_1v0/msc_file_explorer 40,164 → 41,380 (+1,216) 11,000 → 12,920 (+1,920) 53,816 → 56,952 (+3,136) +5.8%

@github-actions

Copy link
Copy Markdown

Hardware-in-the-loop (HIL) Test Report

No HIL run for this push (no affected boards, or hardware testing did not run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants