Flash it from your browser → (WebHID; Chrome, Edge, Opera. No install.)
A small USB HID bootloader for the CH32V003. The device enumerates as
a vendor HID device (VID 0x1209, PID 0xBEEF)
and is flashed from a browser over WebHID, with no native host tooling
required. Its USB product name is CH32V.
The CH32V003 has no hardware USB peripheral. The bootloader bit-bangs a low-speed HID stack, which is small enough to live in the 1920-byte boot area. Flash data uses HID feature reports over control endpoint 0; the required HID Interrupt IN endpoint is present but carries no data. The board has a fixed 1.5 kΩ pull-up from D- to 3.3 V. On entry and before starting the user app, the bootloader drives D- low for a short USB disconnect.
Prerequisites: CMake ≥ 3.20, Ninja, and the xPack RISC-V GCC
toolchain (put its bin/ on your PATH).
cmake -S . -B build -G Ninja
cmake --build buildThe build emits:
bin/ch32v003_hid_bootloader.elfbin/ch32v003_hid_bootloader.binIt fails if the binary exceeds the CH32V003 boot-area limit of 1920 bytes (the linker script enforces this).
Open webhid-flasher.html in a Chromium-based browser
(Chrome, Edge, or Opera). The page accepts .bin,
.elf, and .uf2 files, converts them
client-side, and sends addressed 8-byte chunks over HID feature reports.
The device enumerates as 1209:BEEF — that's the filter the
WebHID page uses to find it.
To flash:
First time only: plug in the board before clicking Flash so your browser can show the one-time permission prompt. After that, the three steps above are all you need — a reset is enough to start each new flash.
For detailed Windows-side diagnostics, use the dependency-free Python tool. It logs device identity, HID report sizes and IDs, every packet, address, payload, result, Windows error, transfer time, and wait:
python .\hid-flasher.py --probe
python .\hid-flasher.py firmware.bin --dry-run
python .\hid-flasher.py firmware.binThe tool accepts .bin, .elf, and
.uf2. Use --base for a BIN with a non-default
address, --step for one packet at a time, or
--limit-chunks N --no-reset for a controlled partial-write
test.
On power-up the bootloader waits for about five seconds. If no first
write report arrives during that window it jumps straight to the user
application; once flashing starts it stays active and writes the payload
into flash. When all pages are written, the flasher sends a reset
command and the chip reboots into the newly-written user application. If
the first user-flash word is erased (0xFFFFFFFF), the
bootloader stays active instead of starting empty flash.
0x08000000–0x08004000, 16 KB) before
sending.The bootloader fits in a 1920-byte BOOT area on the
CH32V003 (remapped to 0x00000000 on reset). The linker
script enforces this: overflow fails the link. The current binary is
1804 / 1920 B (94 %).
The code is size-focused, but its identifiers remain descriptive.
-nostdlib acts as a guardrail: a stray
int * int would fail the link instead of silently pulling
__mulsi3 (~+36 B) into the image. The main size reductions
are a shared descriptor lookup table, a 16-byte report buffer with word
copies, aligned command-header reads, and the inlined
process_flash_report handler. The build also force-inlines
wait_for_flash, writes the needed peripheral registers
directly, and keeps boot_firmware as one stable handoff
point under LTO.
Library audit: zero libgcc/libc symbols. The rv003usb ISR is at its
feature floor — every optional macro is off and
--gc-sections removes nothing.
The low-speed USB stack in rv003usb/ is rv003usb by Charles Lohr
(cnlohr).
MIT. Copyright (c) 2026, xrip.