Skip to content

ch32: portable async USBFS host driver — hub, multi-device, hotplug (v20x + v307)#3682

Open
RobertDaleSmith wants to merge 1 commit into
hathach:masterfrom
joypad-ai:ch32v307-usbfs-host
Open

ch32: portable async USBFS host driver — hub, multi-device, hotplug (v20x + v307)#3682
RobertDaleSmith wants to merge 1 commit into
hathach:masterfrom
joypad-ai:ch32v307-usbfs-host

Conversation

@RobertDaleSmith

@RobertDaleSmith RobertDaleSmith commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworks the CH32 USBFS host controller driver into a portable, fully
interrupt-driven asynchronous scheduler
that supports USB hubs, multiple
simultaneous devices, and hotplug
, covering both CH32V20x and CH32V307
from one driver.

The existing driver (added in #2793 by @verylowfreq) drives a single device with
a blocking per-transfer model on v20x, and v307 USBFS host wasn't wired up at
all. This replaces the transfer core with an ep[]-scheduler model and routes
control transfers through WCH's reference transaction layer, keeping the same
CFG_TUH_WCH_USBIP_USBFS entry point.

Important

This supersedes @verylowfreq's v20x driver with a hub-capable one for the whole
family. v307 is hardware-verified; v20x is build-verified only (I don't have
a v20x board). The silicon and registers are identical across the family, but
v20x should get a hardware pass before merge — happy to coordinate.

Why

The CH32 USBFS peripheral has a single host transfer engine (one DEV_ADDR
latch, one HOST_EP_PID token, one shared toggle pair, one RX/TX DMA pair).
One-transfer-at-a-time works for a single boot device but can't fan out to a hub
plus several interrupt endpoints — which real adapters need.

What changed

  • hcd_ch32_usbfs.c — async ep[] scheduler. Modeled on the max3421 HCD:
    ISR-driven round-robin over a flat ep[] array keyed by (daddr, ep_num, dir), one transaction outstanding on the engine, interrupt EPs paced to
    bInterval.
  • Fully interrupt-driven attach/disconnect. hcd_init() enables SOF
    generation so the 1 ms SOF interrupt runs even before a device attaches; that
    tick polls DEV_ATTACH (a device present at power-up makes no DETECT edge) and
    the no-response disconnect counter. Hot-plug also takes the DETECT edge as a
    fast path. No application poll hook is required — the stock host examples
    work unmodified.
  • wch_usbfs_ll.{c,h} (new) — WCH reference transaction layer. Control
    transfers run through WCH's proven synchronous primitives
    (USBFSH_CtrlTransfer) + root-port reset/enable — this is what reliably
    enumerates bus-powered hubs and a broad range of controllers. Derived from
    WCH's HOST_KM reference, © WCH; thin config header is MIT.
  • MCU-portable via ch32_usbfs_reg.h. The selector also picks
    ch32v20x_usb.h/ch32v30x_usb.h by CFG_TUSB_MCU; the driver and LL include
    only the selector (no hardcoded MCU header). v20x's SDK has no
    RCC_USBFSCLKConfig, so the LL defers USB clock-source setup to board_init()
    there (as v307 already does).
  • Host enable + BSP wiring. tusb_mcu.h auto-enables
    CFG_TUH_WCH_USBIP_USBFS for v307 (mirroring the v20x block). The host sources
    are wired into both the make (family.mk) and cmake (family.cmake)
    builds. v307 BSP routes the USBFS IRQ to tuh_int_handler (the vector is
    USBFS_IRQHandler, not OTG_FS_IRQHandler) and bumps the linker stack; v20x's
    USBHD_IRQHandler -> tuh_int_handler wiring is unchanged.
  • tusb_verify.h. No-op the assert ebreak on CH32 RISC-V cores: a failed
    TU_ASSERT/TU_VERIFY was soft-resetting the chip (the WCH-LinkE leaves the
    debug module enabled) instead of returning the error, turning recoverable
    enumeration hiccups into reboot loops. Helps device-mode CH32 users too.

Testing

Hardware — CH32V307VCT6 (USBFS host PA11/PA12 → USBHS device out):

  • HID keyboards, mice, gamepads enumerate and report.
  • Bus-powered hub with multiple controllers attached at once.
  • Hotplug — attach/detach of devices and of a downstream-of-hub device, plus a
    device already present at power-up.
  • Sustained input passthrough host-in → device-out.

Build — riscv-none-elf-gcc 13.2.0, make + cmake, every example, all boards:

  • v20x: ch32v203c_r0_1v0, ch32v203g_r0_1v0, nanoch32v203
  • v30x: ch32v307v_r1_1v0, nanoch32v305

v20x runtime: not validated — needs a hardware pass (cc @verylowfreq).

@RobertDaleSmith RobertDaleSmith force-pushed the ch32v307-usbfs-host branch 4 times, most recently from 1f528bc to c05ab16 Compare June 7, 2026 14:46
@RobertDaleSmith RobertDaleSmith changed the title ch32: portable async USBFS host (hub, multi-device, hotplug) for v20x + v307 ch32: portable async USBFS host driver — hub, multi-device, hotplug (v20x + v307) Jun 7, 2026
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

Top 10 targets by memory change (%) (out of 2403 targets) View Project Dashboard →

target .text .rodata .data .bss total % diff
ch32v307v_r1_1v0/hid_generic_inout 10,520 → 11,636 (+1,116) 11,016 → 12,132 (+1,116) +10.1%
ch32v307v_r1_1v0/dfu_runtime 9,380 → 10,504 (+1,124) 13,068 → 14,188 (+1,120) +8.6%
ch32v307v_r1_1v0/printer_to_cdc 13,308 → 14,424 (+1,116) 13,796 → 14,912 (+1,116) +8.1%
ch32v307v_r1_1v0/webusb_serial 13,528 → 14,632 (+1,104) 14,072 → 15,176 (+1,104) +7.8%
ch32v307v_r1_1v0/usbtmc 13,976 → 15,072 (+1,096) 14,592 → 15,688 (+1,096) +7.5%
ch32v307v_r1_1v0/hid_boot_interface 11,364 → 12,480 (+1,116) 15,132 → 16,244 (+1,112) +7.3%
ch32v307v_r1_1v0/hid_multiple_interface 11,340 → 12,452 (+1,112) 15,108 → 16,216 (+1,108) +7.3%
ch32v307v_r1_1v0/hid_composite 11,712 → 12,828 (+1,116) 15,504 → 16,616 (+1,112) +7.2%
ch32v203c_r0_1v0/hid_controller 18,380 → 19,588 (+1,208) 1,716 → 2,016 (+300) 22,680 → 24,192 (+1,512) +6.7%
ch32v307v_r1_1v0/audio_test 13,008 → 14,140 (+1,132) 17,384 → 18,512 (+1,128) +6.5%

@RobertDaleSmith RobertDaleSmith marked this pull request as ready for review June 8, 2026 15:11
@verylowfreq

Copy link
Copy Markdown
Contributor

Thank you for this work!

I tested on CH32V203 and found an enumeration issue with some Low Speed
Devices connected via a hub. I traced it to the speed selection logic and
made a fix.

PR: joypad-ai#1

Please take a look when you get a chance.

@RobertDaleSmith

Copy link
Copy Markdown
Contributor Author

@verylowfreq thank you for testing so quickly. i merged your changes in and verified it all still works on my end with a v307. 🙏

@HiFiPhile HiFiPhile left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for your hard work ! But I've to say in current state it's not mergeable due to:

  • wch_usbfs_ll is not MIT licensed to be included in src (although I believe WCH doesn't care)
  • wch_usbfs_ll is a black box and nightmare to maintain, previously we had very limited support from WCH, if anything goes wrong we have to spend a lot of time.
  • Many functions are unused like USBFS_RCC_Init and USBFS_Host_Init which make things confusing.
  • USBFSH_CtrlTransfer implement control transfer Synchronously with a maximum blocking time of DEF_CTRL_TRANS_TIMEOVER_CNT which against TinyUSB's asynchronous approach especially when no RTOS is used.

@RobertDaleSmith

Copy link
Copy Markdown
Contributor Author

@HiFiPhile thank you so much for taking the time to review this and for the feedback. I totally understandable the concerns. I will circle back to this and see what I can come up with. 🙏

@RobertDaleSmith RobertDaleSmith force-pushed the ch32v307-usbfs-host branch 2 times, most recently from 67d4804 to 434051d Compare June 23, 2026 04:59
…v20x + v307)

Add a TinyUSB HCD for the CH32 USBFS host controller (CH32V20x and
CH32V307). Control, interrupt and bulk transfers all run through one
ISR-driven async ep[] scheduler modeled on the MAX3421 HCD, with a single
transaction outstanding on the controller's single host engine. A
per-(daddr, ep) endpoint model supports USB hubs, multiple downstream
devices and hotplug.

Control transfers are fully asynchronous: hcd_setup_send queues the SETUP,
the DATA and STATUS stages run via hcd_edpt_xfer, and each stage completes
from the transfer-done IRQ. Nothing blocks the caller, so it works without
an RTOS. Root-port reset/enable and the transaction layer are native
register sequences derived only from the CH32 register definitions in
ch32_usbfs_reg.h — no vendor host code is included.

Also wires the USBFS host into the ch32v20x and ch32v30x BSPs (IRQ handler,
family glue, linker) and makes tusb_verify's ebreak a no-op on CH32 RISC-V
cores so a TU_VERIFY failure does not trap the core.

Low-speed-via-hub speed-select and register-update fixes contributed by
verylowfreq.
@github-actions

Copy link
Copy Markdown

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

hfp.json

Board cdc_dual_ports cdc_msc dfu cdc_msc_throughput audio_test_freertos dfu_runtime cdc_msc_freertos hid_boot_interface msc_dual_lun hid_generic_inout printer_to_cdc midi_test mtp
stm32l412nucleo ✅ CDC 633k/399k MSC 816k/793k
stm32f746disco ✅ CDC 13.6M/10.4M MSC 15.1M/30.4M
stm32f746disco-DMA ✅ CDC 13M/9.9M MSC 15M/30.7M
lpcxpresso43s67 ✅ CDC 12.8M/12.2M MSC 30.5M/33.1M

Legend: ✅ pass · ❌ fail · ⚪ skipped · blank not run

tinyusb.json

Board cdc_dual_ports cdc_msc dfu cdc_msc_throughput audio_test_freertos dfu_runtime cdc_msc_freertos hid_boot_interface msc_dual_lun hid_generic_inout printer_to_cdc midi_test mtp host_info_to_device_cdc cdc_msc_hid msc_file_explorer msc_file_explorer_freertos device_info hid_composite_freertos
ek_tm4c123gxl ✅ CDC 629k/713k MSC 812k/931k
espressif_p4_function_ev rd 409KB/s
espressif_p4_function_ev-DMA rd 409KB/s
espressif_s3_devkitm rd 409KB/s
espressif_s3_devkitm-DMA rd 409KB/s
feather_nrf52840_express ✅ CDC 451k/286k MSC 571k/503k
max32666fthr ✅ CDC 7M/14.3M MSC 6.7M/20.3M
metro_m4_express ✅ CDC 484k/487k MSC 600k/577k
mimxrt1015_evk ✅ CDC 10.8M/6.3M MSC 16M/13.6M
mimxrt1064_evk ✅ CDC 10.9M/6.3M MSC 16.1M/13.6M rd 1365KB/s
lpcxpresso11u37 ✅ CDC 317k/297k MSC 574k/537k
ra4m1_ek ✅ CDC 556k/453k MSC 577k/517k
raspberry_pi_pico ✅ CDC 560k/441k MSC 698k/931k
raspberry_pi_pico_w rd 1106KB/s rd 1022KB/s
raspberry_pi_pico2 rd 1110KB/s rd 1022KB/s
adafruit_fruit_jam ✅ CDC 650k/506k MSC 911k/1M rd 62KB/s rd 62KB/s
stm32f072disco ✅ CDC 313k/307k MSC 542k/512k
stm32f407disco ✅ CDC 608k/553k MSC 853k/990k
stm32f723disco ✅ CDC 954k/760k MSC 964k/1M rd 18078KB/s rd 4096KB/s
stm32f723disco-DMA ✅ CDC 1M/597k MSC 1.1M/1M rd 19418KB/s rd 4096KB/s
stm32h743nucleo ✅ CDC 549k/511k MSC 512k/511k
stm32h743nucleo-DMA ✅ CDC 538k/528k MSC 566k/550k
stm32g0b1nucleo ✅ CDC 484k/474k MSC 597k/556k
stm32l476disco ✅ CDC 530k/514k MSC 537k/549k
stm32u083nucleo ✅ CDC 585k/489k MSC 755k/525k
nanoch32v203-fsdev
nanoch32v203-usbfs
ch32v103r_r1_1v0
ch582m_evt

Legend: ✅ pass · ❌ fail · ⚪ skipped · blank 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.

3 participants