rv003usb: a USB controller written in firmware for the CH32V003
CH32V003 RISC-V Pure Software USB Controller
At a glance
- What is it?
- rv003usb implements a USB low-speed device in software on the CH32V003 RISC-V microcontroller, using a pin-change interrupt instead of a USB peripheral. It suits makers who want HID devices on very cheap hardware, and it comes with sharp limits around pins, timing and unfinished serial demos.
- Who is it for?
- Adopt rv003usb if your device can live with low-speed HID and a five bit pin limit, and if you are comfortable owning interrupt timing in your own application code. Do not adopt it for CDC, RNDIS or anything that needs full-speed throughput, since those examples are unfinished and Linux blocks low-speed Ethernet.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 39 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Ten cents of silicon without a USB peripheral
The CH32V003 is a cheap RISC-V microcontroller, and rv003usb exists to solve a very specific problem with it: getting that chip onto a USB bus at all. The project's own description calls it a Pure Software USB Controller, and the README opens by asking whether you have ever wanted a 10 cent processor that a computer can talk to over USB. The answer it gives is that firmware can do it, with no external transceiver and no dedicated USB block on the chip.
The audience is narrow and specific. If you are building a gamepad, a small keyboard and mouse combination, or a custom device your own PC software needs to talk to, this stack gets you a working HID device on hardware that costs almost nothing. The README also points out a version of the tool that writes programs to run on the PC so they can exchange fully formed messages with the device, and it links a separate Android test project for that purpose.
That framing matters, because the whole design is aimed at low speed and human interface devices rather than bulk storage or networking. Everything the stack does well and everything it does poorly follows from that choice.
A pin-change interrupt doing the work of a controller
The mechanism is unusual enough to be worth reading closely. Rather than using a USB peripheral with dedicated hardware, the core is a pin-change interrupt routine written in assembly, plus a C layer that handles the rest of the protocol. The README links straight to the interrupt handler and to the transmission routine inside rv003usb/rv003usb.S, and describes around 250 lines of C in rv003usb/rv003usb.c to cover the remainder.
The bus presence is signalled by a 1.5k pull-up on D-, so the host knows a low-speed device is present. When a host pulls the line, the pin-change interrupt fires and the code answers setup packets. Control setup events, HID feature requests, and interrupt endpoints for both sending and receiving are all handled in that C file.
What makes this adaptable is that the core assembly is standardized and the C file is where per-project behaviour lives. Pin changes, added features, and different device personalities are supposed to be changes in the C layer rather than surgery on the interrupt path, which is a sensible way to keep the timing critical part frozen.
The bootloader that fits in 1,920 bytes
Size is the other headline claim, and it is a concrete one. The README states that the bootloader version of the tool can create an HID device, enumerate, and allow execution of code on the device in 1,920 bytes of code. That figure applies to the bootloader build. A basic HID setup, such as the Joystick demo, lands at roughly 2kB.
The bootloader directory in the repository is a CH32V003 self-bootloader that is able to flash itself with minichlink, and it is marked as working in the demo table. There is also a separate bootloader_v006 directory at the top level, which suggests the versioning scheme is not fixed around a single image.
For a bootloader this small, pin configuration gets extra attention. The README warns that if you plan to use the USB bootloader you should look at the bootloader directory for additional considerations on the pin configuration, which is a reasonable reminder that a device enumerated over USB is also a device that must come up reliably from a cold power cycle with no host help.
The demo table marks its own unfinished work
The README does something that few project pages manage well: it grades itself. Four examples are marked as complete, including demo_gamepad for a two axis gamepad with eight buttons, demo_composite_hid for mouse and keyboard in one device, demo_hidapi for talking to the device directly, and the bootloader.
Below that, a second table lists work the author calls largely incomplete and proof of concept. The cdc_exp example enumerates as a USB serial port and sends and receives data, but it is flagged as incomplete and very simple. usb_cdc_uart turns the chip into a USB to UART bridge at 115200 baud, and the note says it has very similar issues to cdc_exp and was tested on Linux. test_ethernet is an RNDIS device, and the README marks it very slow. The MIDI example only demonstrates MIDI-OUT.
One note reads directly: CDC in Windows likely can work, but the author could not figure out how to do it, and Linux explicitly blacklists all low-speed USB Ethernet that the author could find. That is an unusually candid statement about where the project stops.
Five bit logic decides where D+ and D- can live
The pin configuration lives in usb_config.h, and it carries a constraint that is easy to miss. The README includes a caution block explaining that only GPIOs 0 to 4 can be used for USB D+ and D-, because the c.andi instruction can only hold a five bit value. That means PC5 through PC7 and PD5 through PD7 are excluded outright. The defaults shown in the documentation look like this:
#define USB_PORT D // [A,C,D] GPIO Port to use with D+, D- and DPU
#define USB_PIN_DP 3 // [0-4] GPIO Number for USB D+ Pin
#define USB_PIN_DM 4 // [0-4] GPIO Number for USB D- Pin
#define USB_PIN_DPU 5 // [0-7] GPIO for feeding the 1.5k Pull-Up on USB D- Pin; Comment out if not used / tied to 3V3!All configured pins must sit on the same GPIO port, which means the port letter in the configuration is not a free choice. The DPU pin that drives the pull-up is optional and has three arrangements: let rv003usb control it, tie the pull-up to 3V3, or tie it to a GPIO of your choice on any port and signal presence yourself. The last option still permits re-enumeration by pulsing D- low in user code. Changing any of this requires running make clean before make.
Four components, a 1.5k pull-up and a 3.6V ceiling
The hardware story is short enough to read in a minute, and the schematic in the doc directory is referenced from the README. U1 is the CH32V003 and J1 is a USB type C connector. R1, a 1.5k 5 percent resistor, is required under all configurations, though it may connect D- to DPU or to VCC if the intent is for the part to always be on the bus. U2 with C1 and C2 run the microcontroller at 3.3V, and the README is explicit that for USB the part should not be run beyond 3.6V.
If you want the host to supply 5V power over a type C connector, R2 and R3, both 5.1k, are needed. Optionally, 33 or 47 ohm resistors can be placed in series with D+ and D- to reduce noise and protect the chip, and the README recommends them if you plan to pulse D- low to trigger a re-enumeration.
Small packages are supported. The README points to a thread in issue 25 showing use of the SOIC-8 variant, which matters because an eight pin package leaves little room for spare GPIO alongside the USB pins.
Where a hardware USB controller is the better trade
The honest comparison is not against another open source project but against a different class of hardware. A microcontroller that ships with a USB peripheral handles enumeration, descriptors and endpoints in silicon, which removes the timing constraint, the pin limit and the code size budget that shape this project. For anything above low speed HID, that is a fundamentally cheaper trade in engineering time.
This project inverts that trade. The whole point is to use a part that costs about ten cents and has no USB block, so the constraints are accepted in exchange for the price. The cost is paid in the areas the README is careful about: GPIO 0 to 4 only, one port for all USB pins, a 3.6V ceiling, and code that must respect interrupt rules.
The stack is also built on ch32fun, which the README describes as a minimal development SDK of sorts for the CH32s, allowing maximum flexibility without lots of code surrounding a HAL. That layering choice is consistent with the whole design. You are expected to read the timing rules and keep your own code short around them.
Interrupt timing is the price of bit-banging USB
The README spends a whole section on interrupt care, and that section is where the real engineering constraints live. Interrupts and critical sections are allowed, but critical sections should be kept to approximately 40 or fewer cycles where possible. If an interrupt will run longer than about 40 cycles, it must have preemption enabled, and the README points at the ws2812b SPI DMA driver in ch32fun as a worked example.
Two rules are absolute. The external pin-change interrupt must be the highest priority, and it must never be preempted. A short delay before it fires is acceptable, but interrupting the USB reception code mid transfer is prohibited.
Those rules mean your application code is not free to do whatever it likes. Any long operation in your firmware either has to be preemptible or has to live entirely outside the critical window, and a design that assumes a comfortable margin between an LED refresh and the USB interrupt will not survive contact with real traffic. The README documents this clearly, which is more than most projects of this size do.
Editorial conclusion
Adopt rv003usb if your device can live with low-speed HID and a five bit pin limit, and if you are comfortable owning interrupt timing in your own application code. Do not adopt it for CDC, RNDIS or anything that needs full-speed throughput, since those examples are unfinished and Linux blocks low-speed Ethernet. Before committing hardware, verify the pin map against the c.andi constraint on GPIO 0 to 4, confirm your board stays under 3.6V, and check the bootloader directory for the pull-up arrangement you intend to use.
Frequently asked questions
What does rv003usb add to a CH32V003?
It implements the USB device stack in firmware rather than through a hardware peripheral. The core is a pin-change interrupt routine in assembly plus around 250 lines of C that handle control setup events, HID feature requests and interrupt endpoints.
Can rv003usb be used with a CH32V003 in an SOIC-8 package?
Yes, the README points to a thread in issue 25 that demonstrates SOIC-8 support. The USB pins still have to fit the limits described in the documentation, with D+ and D- restricted to GPIO 0 to 4 of a single port.
What is the smallest rv003usb example?
The bootloader version can create an HID device, enumerate and allow execution of code on the device in 1,920 bytes of code. Basic HID setups such as the Joystick demo are approximately 2kB.
Which rv003usb demos are unfinished?
The cdc_exp and usb_cdc_uart examples are marked incomplete and very simple, test_ethernet is an RNDIS device described as very slow, and the MIDI example only demonstrates MIDI-OUT. The README also notes that CDC in Windows likely can work but the author could not find a way to do it.
Do I have to clean the build after changing the USB pins?
Yes. The README states that you must run make clean before make whenever you change the configuration in usb_config.h, and the same requirement is repeated for custom hardware pin assignments.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cnlohr-rv003usb)Was this useful?