Gregory J. Durnan, PhD, CEng, FIMarEST

Getting Started: CH32V003F4P6 with WCH-LinkE
San Diego, CA, USA · gregory.durnan@gmail.com · durnan.org

A Sixteen-Kilobyte RISC-V Chip, No Vendor IDE Required

CH32V003F4P6-R0-1v1 board, CH32V003C eval board, WCH-LinkE probe and spare dice

The CH32V003 is WCH's entry-level RISC-V microcontroller: a QingKe V2A core running the RV32EC instruction set, 16 KB of flash, 2 KB of SRAM, and a price that undercuts even the classic 8051 boards this site already covers. It has no built-in UART bootloader the way the STC89C52RC does — instead, programming and debugging both go through a small USB probe called WCH-LinkE, talking to the chip over a single debug wire.

Everything WCH ships for this part assumes MounRiver Studio, their own Eclipse-based IDE. This page sets up something else: the same minimalist, no-proprietary-IDE workflow already in use for the STC89C52RC + SDCC + Geany page — a standalone GNU RISC-V cross-compiler, a Makefile or batch file, a second independent Geany profile, and WCH's own small flashing GUI as the one remaining manual step.

What's in the Kit, and the Case of the Missing DIP Socket

If you've used AVR or 8051 programmers before, it's natural to expect a ZIF socket or DIP header the programmer clips directly into. The CH32V003 boards have no such socket, and none is needed. WCH's own evaluation-board manual says it plainly: the debug interface is for “downloading, simulation debugging, single-wire communication, only need SWDIO to connect PD1.”

The QingKe V2A core uses a proprietary single-wire debug interface, not the two-wire ARM SWD protocol you may recognize from STM32 parts. One signal pin — PD1 — carries all programming and debug traffic. Add power and ground and the entire connection between WCH-LinkE and the board is three ordinary jumper wires into header pins the board already exposes — no socket, no adapter, no special connector to buy.

WCH-LinkE pinBoard pin (header P1)Purpose
3V3VCCPower. Don't also power the board over USB at the same time unless you know both rails agree.
GNDGNDCommon ground.
SWDIO / RMSPD1Single-wire debug / programming (SWIO).
SWCLK / TCKnot connectedUnused — CH32V003 is single-wire only.
Worth knowing up front: the crystal pads (R4, R5, Y1) are unpopulated on the F4P6-R0-1v1 board by default. The example below defaults to a 48 MHz HSE-PLL clock; with no crystal fitted, WCH's own clock-setup code safely falls back to the 24 MHz internal oscillator instead of hanging. You'll just see a different clock value reported over the debug UART, not a dead board.

A Note on “GCC” and Why You Need a Second Copy of It

It's tempting to think that because GCC's own manual lists RISC-V options (-march, -mabi, and so on) right alongside every other architecture it supports, any gcc.exe you already have can just be pointed at those flags. It can't — a given GCC binary is built for exactly one target. The Windows GNU toolchain most people use for things like gfortran is built to target x86_64-w64-mingw32 and produces native Windows executables; the RISC-V code generator simply isn't present in it.

What's needed is a second GCC build — same compiler family, entirely different --target — distributed as its own uniquely-prefixed binaries: riscv-none-elf-gcc.exe, riscv-none-elf-objcopy.exe, and so on. Because every filename is prefixed, it can't collide with a plain gcc.exe or gfortran.exe even sharing the same PATH. This setup goes one step further and skips PATH entirely — the Makefile and batch file below take the RISC-V toolchain's location as an explicit variable, so nothing about your existing GNU Tools install is touched.

The recommended distribution is the free, standalone xPack GNU RISC-V Embedded GCC (riscv-none-elf-gcc) — unzip it, note the path to its bin folder, done.

Flashing with WCH-LinkUtility

WCH-LinkUtility configured for CH32V003 in RISC-V mode

Until a command-line flasher is wired into the build, WCH-LinkUtility's small GUI does the flashing:

  1. Launch WCH-LinkUtility.exe and set Core: RISC-V, Series: CH32V003, Addr: 0x8000000.
  2. Leave Erase All, Program, Verify, and Reset and Run all checked — the standard flash-and-go combination.
  3. Browse Target File to the .hex produced by the build below.
  4. Wire WCH-LinkE per the table above, plug it into USB, and confirm the status line reads Connected RISC-V mode WCH-Link Cnt:1.
  5. Run the combined operation and watch Result Collect for Succeed:1 | Total:1.

The Toolchain

Three pieces, mirroring the STC workflow:

Toolchain Flags, and Why Each One Is There

CH32V003 being RV32EC isn't quite enough on its own to hand GCC:

This exact Makefile and flag set were verified end-to-end, not just written from documentation: compiled and linked cleanly with xPack riscv-none-elf-gcc 14.2.0, producing a working image that fits the target's 16 KB/2 KB budget with headroom to spare.

+-------------------------+
| Write main.c (GPIO,     |
| pulled from WCH's own   |
| GPIO_Toggle example)    |
+-------------------------+
           |
           v
+-------------------------+
| riscv-none-elf-gcc      |
| -march=rv32ec_zicsr_    |
|   zifencei -mabi=ilp32e |
| --specs=nano.specs      |
| -> .elf -> .hex          |
+-------------------------+
           |
           v
+-------------------------+
| WCH-LinkUtility:        |
| Erase All / Program /   |
| Verify / Reset and Run  |
+-------------------------+
           |
           v
+-------------------------+
| PD0 toggles every       |
| 250 ms                  |
+-------------------------+

The Example: GPIO Blink

void GPIO_Toggle_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz;
    GPIO_Init(GPIOD, &GPIO_InitStructure);
}

int main(void)
{
    u8 i = 0;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);
    GPIO_Toggle_INIT();

    while(1)
    {
        Delay_Ms(250);
        GPIO_WriteBit(GPIOD, GPIO_Pin_0, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
    }
}

Pulled directly from CH32V003EVT.ZIP\EVT\EXAM\GPIO\GPIO_Toggle. A full scan of all 423 example source files in WCH's package found only a handful of stray non-English punctuation marks in two unrelated SPI examples (since corrected) — nothing in this one. WCH's own example source was already in English; the actual work here was building an original, standalone command-line build system around it.

Geany Build Commands

As with the STC workflow, Geany calls build commands directly without a shell in front of them, so the batch file needs the same cmd /c prefix:

Optional: Seeing printf Output (UART Debug)

The example's printf calls ride on USART1 (PD5 TX, 115200 8N1) and need a USB-to-serial adapter to actually view — the blink itself works with or without one. The recommended part is a genuine FTDI FT232RL, not a CH340-based module, carrying the same reasoning forward from the CH340 VSP driver on the STC89C52RC boards this site already covers:

This is a standing convention across this site's guides, not a one-off choice for this chip.

Get the Full Write-Up

The complete guide covers the xPack GCC install, the full annotated Makefile and build_ch32v003.bat, exact Geany Build Command settings, the WCH-LinkUtility walkthrough with screenshots, and a troubleshooting section built from the actual build errors this exact toolchain combination produces the first time through (missing zicsr/zifencei, flash overflow from full newlib, and more).

Where This Goes Next

This page deliberately stops at “GUI flashing tool, command-line build.” The natural next step — mirroring how the STC89C52RC page replaced STC-ISP with a from-scratch C flasher — is a command-line flashing utility for WCH-LinkE, so that make flash becomes as real a target as make all. WCH-LinkUtility's own protocol isn't publicly documented, but community projects (most notably minichlink, part of the open-source ch32v003fun project) have already reverse-engineered and open-sourced exactly this, and are the logical next stop once this last loop is worth closing.