✧ 10-CENT RISC-V FIELD GUIDE ✧
48 MHZ · 16KB FLASH · 2KB RAM · ONE WIRE TO RULE IT
The CH32V003 is WCH's famously cheap 32-bit RISC-V microcontroller. Ten-ish cents for the bare chip, a couple bucks for a dev board. It is the "just enough computer" — perfect for haptics, LEDs, sensors, and dedicated single-job electronics where an ESP32 is overkill.
No USB, no WiFi, no Bluetooth on-chip. That is the point. It does one job, costs nothing, and boots instantly.
Everything used in the haptic build on this page. Prices are typical AliExpress/eBay singles — buying multi-packs cuts them roughly in half.
| PART | ROLE | NOTES | EST. USD |
|---|---|---|---|
| CH32V003F4P6 dev board (TENSTAR style) | The brain | USB-C powered minimum system board. All V pads = one rail (~5 V on USB-C). No user LED on this revision. | $1.50 |
| WCH-LinkE programmer | Flash + debug + UART | Must be the E version — plain WCH-Link cannot speak the '003's 1-wire SDI. One-time buy, reused forever. | $5.00 |
| MAX4466 electret mic module | Ears | Analog out, adjustable gain pot on the back. Powered from the board's V rail so its output always stays within ADC range. | $1.50 |
| Mini H-bridge module (L9110S / DRV8833 class) | Muscle driver | Two direction inputs (IN1/IN2). Drives the vibration motor both ways for tap and spin patterns. | $1.00 |
| Coin vibration motor (10 mm ERM) | The haptics | ~70–100 mA draw. Never wire it straight to a GPIO — always through the H-bridge. | $0.80 |
| 3 mm LED + 220 Ω–1 kΩ resistor | Volume meter | Hardware-PWM'd on PC3. The resistor is not optional. | $0.10 |
| CR2032 coin cell + holder | Untethered power (optional) | 3 V, ~220 mAh. Fine for MCU + LED. Motor spikes will sag it — see POWER section for the honest math. | $0.50 |
| TOTAL (with programmer) | $0.00 | ||
| TOTAL (programmer already owned) | $0.00 | ||
One-time setup. After this, every CH32V003 project is just pio run -t upload.
pio pkg install -g -p https://github.com/Community-PIO-CH32V/platform-ch32v.git
Minimal platformio.ini for any '003 project:
[env:genericCH32V003F4P6] platform = ch32v board = genericCH32V003F4P6 framework = ch32v003fun upload_protocol = wch-link
Framework pick: ch32v003fun (register-level, tiny, fast) fits this chip best. Arduino cores exist but eat the 16 KB flash alarmingly fast.
The CH32V003 uses a 1-wire debug interface (SDI). No SWCLK. On the TENSTAR board the programming header is the three center pads on the underside:
| WCH-LINKE | BOARD PAD | WHAT IT IS |
|---|---|---|
| SWDIO / TMS | SWD | Internally the MCU's PD1/SWIO pin |
| 3V3 | V | Power — ONLY when USB-C is unplugged |
| GND | G | Common ground — always connected |
The LinkE's RX/TX pins are a bonus USB-serial adapter (wire LinkE RX → PD5 for printf), not programming pins.
Then from the project folder:
pio run # build pio run -t upload # flash via LinkE
Every pin, what it multiplexes to, and whether it has a trap. The two trap pins are highlighted.
| PIN | ANALOG | TIMER PWM | SERIAL | NOTES |
|---|---|---|---|---|
| PA1 | A1 | T1CH2 | — | Also crystal OSCI |
| PA2 | A0 | T1CH2N | — | Also crystal OSCO |
| PC0 | — | T2CH3 | — | Free |
| PC1 | — | T2CH4 | I²C SDA | Free |
| PC2 | — | T2CH2 | I²C SCL | Free |
| PC3 | — | T1CH3 | — | Volume LED in this build |
| PC4 | A2 | T1CH4 | — | MAX4466 mic in this build |
| PC5 | — | T1ETR | SPI SCK | 5V-tolerant (FT) |
| PC6 | — | — | SPI MOSI | 5V-tolerant (FT) |
| PC7 | — | T1CH2_ | SPI MISO | Free |
| PD0 | — | T1CH1N | — | Free |
| PD1 | — | T1CH3N | — | ⚠ SWIO — the programming pin. Leave it alone. |
| PD2 | A3 | T1CH1 | — | H-bridge IN1 (forward) in this build |
| PD3 | A4 | T2CH2 | — | H-bridge IN2 (reverse) in this build |
| PD4 | A7 | T2CH1ETR | — | Free |
| PD5 | A5 | — | UART TX | printf output → LinkE RX |
| PD6 | A6 | — | UART RX | Free / serial in |
| PD7 | — | T2CH4 | — | ⚠ NRST — drive it low and the chip RESETS. Found out the hard way. |
Nothing mystical. They are just two GPIO banks (Port C, Port D) with their own registers. What differs per pin is (1) which peripherals it routes to, (2) 5 V tolerance — only PC5/PC6 are FT, and (3) special roles, which both live on Port D: PD1 = SWIO, PD7 = NRST. Pick pins by peripheral, not by letter.
Every one of these was hit while building this project. Read them and skip the pain.
Wired a blink LED to PD7. It turned on… and never off. Why: PD7 doubles as NRST. Driving it low to turn the LED off yanks reset, the chip reboots, sets the pin high again, forever. Looks exactly like a hang with the LED lit. Use PC3 (or any non-trap pin) instead.
Confirmed with a meter: every pad marked V on this board — including the center programming-header V — is one shared rail, and it sits at ~5 V whenever USB-C is plugged in. If the LinkE's 3V3 output were connected at the same time, 5 V would be backfed into the programmer. Rule: one power source at a time. USB-C powered → connect only SWD + GND to the LinkE.
The CH32V003's ADC full-scale reference is VDD itself. Run the whole board at 5 V and the ADC range is 0–5 V; power the MAX4466 from the same rail and its output physically cannot exceed VDD. The danger case is only a mixed-rail setup (5 V mic into a 3.3 V-powered chip). Single rail = automatically safe.
The WCH-LinkE has two personalities. USB PID 8010 = RISC-V mode (what you want). 8012 = ARM/DAP mode (uploads silently fail). Hold ModeS while plugging in to switch back.
The '003 introduced a 1-wire SDI interface. The older WCH-Link (no E) does not support it. When ordering, the E matters.
The haptics FFT is a 64-point radix-2 in Q15 fixed-point: int16 buffers, a 17-entry quarter-wave sine table, zero heap, zero floats. Whole firmware: ~3 KB flash, ~280 bytes RAM. Write like it's 1985 and the chip rewards you.
Mic in → tiny FFT → three frequency bands → different motor pattern per band, plus an LED that breathes with volume. All hardware PWM.
| BAND | RANGE | MOTOR PATTERN |
|---|---|---|
| LOW | ~125–375 Hz | tap tap (two short forward pulses) |
| MID | ~500–1125 Hz | continuous spin, duty tracks energy |
| HIGH | ~1250–3000 Hz | tap taaaap tap taaaap (reversing) |
| WIRE | FROM | TO |
|---|---|---|
| Mic signal | MAX4466 OUT | PC4 |
| Mic power | MAX4466 VCC/GND | Board V / G (same rail!) |
| Motor fwd | PD2 (TIM1_CH1) | H-bridge IN1 |
| Motor rev | PD3 (TIM2_CH2) | H-bridge IN2 |
| Motor | H-bridge OUT A/B | Vibration motor |
| Volume LED | PC3 → resistor | LED+ … LED− → GND |
Firmware lives in src/main.c next to this page. Key tuning knobs at the top of the file: ENERGY_GATE (noise floor), BAND_MARGIN (band separation), PWM_DUTY_FLOOR/MAX (motor kick and ceiling), LED_VOLUME_GATE/MAX (LED range), TAP_*_MS (rhythm), BIN_* (band edges).
Firmware guarantees only one H-bridge input is PWM'd at a time and coasts between direction flips — no shoot-through.
| SOURCE | V RAIL | VERDICT |
|---|---|---|
| USB-C | ~5 V | Best for the full haptic rig. Disconnect LinkE 3V3 first. |
| WCH-LinkE 3V3 | ~3.3 V | Fine for flashing + mic + LED dev. Marginal for the motor. |
| CR2032 coin cell | 3 V nominal | MCU + LED: yes. Motor: short bursts only — see below. |
A CR2032 stores ~220 mAh but has high internal resistance (~10–30 Ω). The CH32V003 idles in single-digit mA — weeks of runtime. But an ERM vibration motor pulls 70–100 mA spikes, which sag a CR2032 below the MCU's brown-out and cause resets. If you want untethered haptics:
Whatever the source: motor power goes through the H-bridge, grounds are shared, and a motor never ever touches a GPIO directly.