Skip to content

Hurra v3 — CH32H417 dual-core RISC-V

Bare-metal firmware for the WCH CH32H417 dual-core RISC-V MCU. It enumerates a real HID device on its USB High-Speed host port, replays that device to the host PC on its USB Full-Speed device port, forwards every real report through unchanged, and lets you inject your own mouse and keyboard input on top of the live HID stream over a serial command link.

This is a port of Hurra v2 (which targeted the NXP i.MX RT1062 / Teensy MicroMod). The command protocols and host tooling are unchanged; the hardware abstraction layer (USB stack, UART, clocks, startup, inter-core channel) was rewritten for the CH32H417.

The source is live on GitHub: github.com/VoltCyclone/Hurra-v3.

Live

Hurra v3 is up and running. Highlights:

  • High-Speed host capture → Full-Speed device replay — captures the real mouse/keyboard at up to ~8 kHz on USBHS and clones it to the PC on USBFS.
  • Dual-core split — the V5F core runs the latency-critical USB relay; the V3F core owns boot, clocks, and the command link.
  • Both command protocols — Hurra binary (default) and Ferrum ASCII, ported verbatim from v2.
  • Always-on humanization on injected motion.

Build the merged dual-core image from source — build from source. No prebuilt binary is published yet.

How it works

   USB HID device ──→ CH32H417 USBHS host ──┐
                                            │  (V5F: proxy + merge + humanize)
   Host PC USB ←── CH32H417 USBFS device ───┘
                          ↑ ICC (shared SRAM @0x20178000 + IPC doorbell)
   Host PC USB ──→ WCH-Link VCP ──→ CH32H417 USART1 (V3F: parse + command)

The real device is captured on the USBHS controller (480 Mbps High-Speed host) and cloned to the PC on the USBFS controller (12 Mbps Full-Speed device); the two controllers run simultaneously. The fast V5F core polls the host endpoints, merges in any injected input, and forwards the combined report to the PC. Injected input arrives from the V3F core: the PC sends commands over the WCH-Link VCP into USART1, where V3F parses them (Hurra or Ferrum), applies humanization control, and pushes injection records to V5F across the inter-core channel. Injection rides real reports when the mouse is moving (merge) or is emitted as standalone synthetic reports when the mouse is idle.

Hardware

  • WCH CH32H417QEU6 (QFN128) USB 3.0 development board.
  • Dual-core RISC-V, both QingKe cores:
    • V3F (~100 MHz) — the boot / master core. Sets up clocks, releases V5F, and runs the command side: USART command link, protocol parser, humanize-level control, LED status.
    • V5F (400 MHz) — the relay core. Runs the latency-critical USB hot path: capture on USBHS host → merge → forward on USBFS device.
  • USB topology — two on-chip controllers, running at the same time:
    • USBHS (480 Mbps High-Speed) = host port — captures the real mouse/keyboard at High-Speed (up to ~8 kHz mice).
    • USBFS (12 Mbps Full-Speed) = device port — the clone presented to the PC. Full-Speed caps the PC-facing HID at ~1 kHz.
  • Command-link USARTUSART1 (PA9 = TX, PA10 = RX, AF7), interrupt-driven, wired to the on-board WCH-LinkE virtual COM port (solder bridges SB3→PA10 / SB4→PA9), so one USB-C cable carries flash + debug + command link with no external dongle. The Hurra build boots it at 921600 baud (the WCH-LinkE VCP ceiling).
  • A 25 MHz HSE crystal feeds the USB PLLs (480 MHz for USBHS, 48 MHz for USBFS).

Dual-core architecture

The two cores divide the work along the latency boundary:

  • V3F (master / command core) — boots from flash, runs SystemInit() (clocks), releases V5F via NVIC_WakeUp_V5F, then runs the command loop: USART RX (DMA) → proto_feed (hurra/ferrum) → act_* → ICC inject-command ring. It also drives the LED status ladder from telemetry it consumes back over the ICC.
  • V5F (relay / hot path) — the centerpiece of the man-in-the-middle: USBHS host poll (real device) → merge (drain ICC injection + humanize) → USBFS device send (to PC). It emits standalone synthetic reports when the physical mouse is idle, and streams forwarded/dropped report telemetry back to V3F.

Inter-core channel (ICC)

The cores talk over a small shared-SRAM mailbox at a fixed address (0x20178000):

  • Two lock-free SPSC ring buffers of fixed 16-byte records. V3F→V5F carries injection commands (INJECT_MOUSE, INJECT_KEYBOARD, CLICK_RELEASE, KB_RELEASE, SET_BAUD, SET_HUMAN_LEVEL, PHYS_MASK); V5F→V3F carries telemetry (TELEM_COUNTS, TELEM_STATUS). volatile head/tail with RISC-V fences; no locks on the data path.
  • HSEM (hardware semaphore) handles the one-time startup rendezvous — V3F initializes the rings and sets a magic value before releasing V5F; the two cores hand-shake via HSEM ID 0.
  • An IPC doorbell (IPC channel 0) lets V3F wake V5F from wfi when it queues injection, so the hot path can sleep when there is no work.

Build & flash

The wire protocols are identical to Hurra v2 — the parsers (hurra.c, ferrum.c) were ported essentially verbatim. The build produces two per-core images (V3F master + V5F relay), converted to raw binaries and merged into a single flash image (build/Merge.bin, V3F at offset 0, V5F at 0x10000).

make all                 # build both cores + merge -> build/Merge.bin (Hurra, default)
make PROTOCOL=ferrum all # Ferrum ASCII protocol instead
make flash               # merge + program the board over the on-board WCH-LinkE
make test                # host-native unit tests (humanization, parsers)
make clean

Toolchain: riscv-none-elf-gcc (MounRiver / xPack) by default, or override the prefix with make TOOLCHAIN=riscv64-unknown-elf all for the Homebrew RISC-V toolchain.

Flashing goes over the on-board WCH-LinkE (same USB-C cable as the command link). make flash auto-detects a CLI flasher, preferring wlink and falling back to the WCH-OpenOCD fork. Mainline openocd does not work — it has no wlinke adapter driver. Pin assignments (LED, command USART) are board-specific — see src/board.h.

Command protocols

Both protocols behave exactly as on Hurra v2 — see Protocols for the wire formats and command tables.