CH32V003
The CH32V003 is a 32-bit RISC-V microcontroller from WCH, built on the QingKe V2A core running the RV32EC instruction set at 48 MHz. It ships with 16 KB of flash and 2 KB of SRAM, a full peripheral set that includes UART, I2C, SPI, a 10-bit ADC with eight channels, two timers with PWM, a built-in analog comparator, and single-channel DMA. The operating voltage range spans 3.3V to 5V, and the chip is available in TSSOP20, SOP16, SOP8, and QFN20 packages.
At under $0.10 per unit in volume, the CH32V003 sits at the price point of an 8-bit PIC or AVR while delivering 32-bit performance on an open instruction set. It is the entry point for the Rovari platform and the chip you will spend the most time with in the 250-in-1 starter kit.
Rovari SDK: All examples on this page use the Rovari hardware abstraction layer
(#include "rovari.h").
The SDK wraps the WCH standard peripheral library with clean functions like
pin_mode(),
uart_init(), and
pwm_init().
When you need direct register access, the full hardware interface is still available underneath.
Every example below is provided in both C and C++ style. The C API uses standalone functions.
The C++ API wraps the same hardware in lightweight objects like
Gpio,
Pwm, and
Uart.
Both compile to the same binary size. Choose whichever reads better to you.
Specifications
| Feature | Detail |
|---|---|
| Core | 32-bit RISC-V (RV32EC) QingKe V2A |
| Clock | 48 MHz internal oscillator |
| Flash | 16 KB |
| SRAM | 2 KB |
| GPIO | Up to 18 pins (package dependent) |
| ADC | 10-bit, 8 channels |
| Timers | 1 advanced 16-bit, 1 general purpose 16-bit |
| Communication | 1x USART, 1x I2C, 1x SPI |
| Other | Analog comparator, DMA, watchdog |
| Voltage | 3.3V to 5V tolerant |
| Packages | TSSOP20, SOP16, SOP8, QFN20 |
| Temperature | -40°C to +85°C |
Setup
All examples on this page are built and flashed using Rovari Studio, which bundles the RISC-V GCC toolchain, WCH-Link drivers, and the Rovari SDK. Select CH32V003 as your target, write your code in the editor, and press Run. The IDE handles compilation, linking, and flashing in a single step.
Hardware
- CH32V003F4P6 development board (or bare chip on a breadboard)
- WCH-LinkE programmer
- USB cable
- Breadboard and jumper wires
Wiring the Programmer
The CH32V003 uses a single wire debug interface. Connect three wires from the WCH-LinkE to the chip:
| WCH-LinkE | CH32V003 |
|---|---|
| SWDIO | PD1 |
| GND | GND |
| 3V3 | VCC |
Single wire only. The CH32V003 debug interface uses one data line (SWDIO on PD1), not the two wire SWD protocol used by ARM chips. Make sure your WCH-LinkE is set to single wire mode.
Blink
The embedded equivalent of "hello world." This example configures a GPIO pin as an output and toggles it at a fixed interval, blinking an LED connected to PC0. It covers clock enabling, pin configuration, and blocking delays.
Hardware
- LED (any color)
- 1kΩ resistor
- Wired from PC0 through resistor to GND
Code
/* * 00_Blink (C style) * Platform: CH32V003 / Rovari SDK * Hardware: LED on PC0 via 1k resistor */ #include "rovari.h" void app_init() { pin_mode(PC0, Output); } void app_run() { pin_toggle(PC0); delay(500); }
Key Concepts
Expected result: The LED toggles every 500ms in the C version (1 Hz blink rate) and every 250ms in the C++ version (2 Hz). If the LED stays dark, check that your resistor connects PC0 to the LED anode and the cathode goes to GND.
Millis
The delay() function blocks the entire CPU while it waits. That works for a single LED, but the moment you need to blink a light and read a sensor at the same time, blocking delays fall apart. The millis() function returns the number of milliseconds since boot, letting you schedule actions without blocking.
Hardware
- LED on PC1 via 1kΩ resistor
Code
#include "rovari.h" uint32_t last_toggle = 0; void app_init() { pin_mode(PC1, Output); } void app_run() { if (millis() - last_toggle >= 500) { last_toggle = millis(); pin_toggle(PC1); } }
Key Concepts
Why this matters: Every real project needs to do multiple things at once. Millis lets you blink an LED every 500ms while reading a sensor every 100ms and checking a button every 50ms, all in the same loop with no blocking.
PWM
Pulse Width Modulation lets you simulate analog output on a digital pin by rapidly switching it on and off at a controlled duty cycle. This is how you control LED brightness, motor speed, and servo position. The CH32V003 generates hardware PWM through its timer peripheral, so the signal runs at a precise frequency with zero CPU overhead after setup.
Hardware
- LED on PD2 (TIM1 CH1) via 1kΩ resistor
Code
#include "rovari.h" void app_init() { pwm_init(PD2, 1000); } void app_run() { for (uint8_t i = 0; i < 255; i++) { pwm_write(PD2, i); delay(10); } for (int i = 255; i >= 0; i--) { pwm_write(PD2, (uint8_t)i); delay(10); } }
Key Concepts
ADC
The 10-bit ADC converts an analog voltage on a pin into a digital value from 0 to 1023. This example reads a potentiometer and prints the raw count and millivolt value over UART. The SDK provides both raw and calibrated millivolt reads in a single call.
Hardware
- Potentiometer on PA1 (ADC channel 1)
- WCH-Link for UART (TX=PD5, RX=PD6)
Code
#include "rovari.h" void app_init() { uart_init(SERIAL1, 115200); adc_init(PA1); uart_println(SERIAL1, "ADC Reading"); } void app_run() { uint16_t raw = analog_read(PA1); uint16_t mv = analog_read_mv(PA1); uart_printf(SERIAL1, "raw: %d mv: %d\r\n", raw, mv); delay(500); }
Key Concepts
UART
UART is the most common way to send text between a microcontroller and a computer. The CH32V003 has one hardware USART mapped to PD5 (TX) and PD6 (RX). This example initializes the port at 115200 baud, prints a greeting, and echoes back anything the user types.
Hardware
- WCH-Link for UART (TX=PD5, RX=PD6)
- 115200 baud, 8N1
Code
#include "rovari.h" void app_init() { uart_init(SERIAL1, 115200); uart_println(SERIAL1, "CH32V003 UART Ready"); uart_println(SERIAL1, "Type something and press Enter"); } void app_run() { char buf[32]; int n = uart_read_line(SERIAL1, buf, sizeof(buf)); if (n > 0) { uart_print(SERIAL1, "You said: "); uart_println(SERIAL1, buf); } }
Key Concepts
SPI
SPI is a synchronous serial bus commonly used for fast communication with sensors, displays, and memory chips. This example drives an MCP41010 digital potentiometer, sweeping its wiper from 0 to 255. The chip select line is managed manually with a regular GPIO pin.
Hardware
- MCP41010 digital potentiometer
- SCK=PC5, MOSI=PC6, MISO=PC7
- CS=PC3 (manual control)
Code
#include "rovari.h" #define MCP41010_CMD_WRITE 0x11 void app_init() { spi_init(SPI_1, 1000000, 0, 0); pin_mode(PC3, Output); digital_write(PC3, High); } void app_run() { for (uint8_t i = 0; i < 255; i++) { digital_write(PC3, Low); spi_write(SPI_1, MCP41010_CMD_WRITE); spi_write(SPI_1, i); digital_write(PC3, High); delay(10); } }
Key Concepts
I2C
I2C is a two wire bus that lets you connect multiple devices on the same pair of lines. This example communicates with a 24LC16B EEPROM, writing a byte to an address, reading it back, and printing the result over UART to verify the data survived the round trip.
Hardware
- 24LC16B EEPROM (address 0x50)
- SCL=PC2, SDA=PC1
- 4.7kΩ pull-ups on both lines
- WCH-Link for UART (TX=PD5, RX=PD6)
Code
#include "rovari.h" #define EEPROM_ADDR 0x50 void app_init() { uart_init(SERIAL1, 115200); i2c_init(I2C_1, 100000); uart_println(SERIAL1, "I2C EEPROM Test"); } void app_run() { static uint8_t val = 0; static uint8_t addr = 0; i2c_write_reg(I2C_1, EEPROM_ADDR, addr, val); delay(6); uint8_t rd = i2c_read_reg(I2C_1, EEPROM_ADDR, addr); uart_printf(SERIAL1, "W[%d]=%d R=%d %s\r\n", addr, val, rd, (rd == val) ? "OK" : "FAIL"); val += 7; addr = (addr + 1) & 0x0F; delay(1000); }
Key Concepts
OLED Display
An SSD1306 OLED gives your project a visual output beyond blinking LEDs. The Rovari SDK includes a built-in driver that supports both 128x32 and 128x64 panels over I2C. The API uses a line-based text interface, so you can print formatted strings to specific rows without managing a frame buffer yourself.
Hardware
- SSD1306 128x32 OLED module
- SCL=PC2, SDA=PC1
- 4.7kΩ pull-ups on both lines
Code
#include "rovari.h" #include "rovari_ssd1306.h" void app_init() { i2c_init(I2C_1, 100000); oled_init(I2C_1, OLED_128x32, SSD1306_I2C_ADDR); oled_set_orientation(OLED_ORIENT_180); oled_clear(); oled_printf_line(0, "Rovari SDK"); oled_printf_line(1, "CH32V003"); oled_printf_line(2, "RISC-V 48MHz"); } void app_run() { static uint32_t count = 0; oled_printf_line(3, "Count: %lu", count); count++; delay(100); }
Key Concepts
Downloads
All source code for the examples on this page is available on GitHub. Clone the repository to get every project pre-configured for Rovari Studio with build files, Guvari blocks, and companion Python scripts included.
External Resources
- Rovari Studio (IDE, build system, instruments)
- WCH CH32V003 product page
- OpenWCH GitHub repository
- Arduino to RISC-V migration guide