SearchA-ZC › CH32V003

CH32V003

2022 Open source · CC0 On-board only Online

CH32V003 is the famous ~10-cent RISC-V microcontroller — a WCH CH32V003 (a 48 MHz QingKe RV32EC core: RISC-V RV32E plus the compressed extension, with 16 KB flash and 2 KB SRAM) — emulated from scratch in the browser on a from-scratch RISC-V core, the site's first RISC-V MCU. It runs REAL compiled firmware: a flat flash image that boots from address 0x00000000 exactly as on silicon (the QingKe core executes the vector table's opening j handle_reset), then drives the real CH32V003 registers. The two on-board peripherals shown are the user LED on PD0 and a USART1 serial console — with nothing invented: no breadboard, no fake audio (a bare CH32V003 has no speaker, so it is honestly silent). Because the whole chip is ordinary JavaScript, it plugs into this site's shared debugger: the real RISC-V disassembler, live x0-x15 and the machine CSRs, side-effect-free FLASH / SRAM / peripheral memory at the real addresses, single-instruction step, execution breakpoints and write-watchpoints.

CH32V003 at WCH ↗

Runs on: Web browser

CH32V003 Online Emulator

Play CH32V003 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Blink PD0 (user LED)CH32V003CH32V003openOpen ⛶
SysTick Blink (STK)CH32V003CH32V003openOpen ⛶
USART HelloCH32V003CH32V003openOpen ⛶
USART CounterCH32V003CH32V003openOpen ⛶

Chips

Notes

Embedding

This is a two-layer stack: a real RISC-V CPU core underneath, and a from-scratch CH32V003 SoC + dev board on top. It loads real RV32EC flash images and runs them on the actual on-chip peripherals — RCC clock gating, the GPIO ports, USART1 and the SysTick timer — at their real addresses. The firmware is genuine compiled machine code, not a shim.

  • The CPU. rv32.js is a from-scratch RISC-V interpreter that executes RV32EC: the RV32I integer base restricted to the 16 registers x0..x15 (that is what the "E" embedded profile means), plus the "C" 16-bit compressed encodings the compiler emits throughout, plus the Zicsr CSR ops and machine-mode traps (ecall/ebreak/mret). The real chip has no hardware multiply, so -march=rv32ec turns multiplies into shift/add — the core never needs the M extension.
  • Real reset. The QingKe core boots executing from flash address 0x00000000, where the vector table's first entry is j handle_reset — so the very first instruction fetched is the jump to the reset code, exactly like the silicon.
  • The board, in JavaScript. ch32v003-board.js maps FLASH/SRAM/peripherals at their real CH32V003 addresses, answers the RCC clock-ready bits the firmware spins on, folds GPIO BSHR/BCR into OUTDR, captures every byte written to USART1->DATAR into the serial console, and runs the SysTick counter.
PieceKindWhat it does
CH32V003.create(canvas)factoryBuild the RV32EC core + the CH32V003 memory map + peripherals.
load(bytes) / reset()methodCopy the flat image into FLASH; reset sets PC = 0x00000000 (QingKe boot) and SP to the top of SRAM.
runFrame()methodStep a batch of instructions and advance the SysTick counter.
ledOn() / getSerial()methodThe on-board user LED (PD0) state and the accumulated USART1 output.
present()methodDraw the on-board LED indicator and the serial console to the canvas.

Debugger integration

ch32v003-debug.js reads window.EMU_BOOT and hands the shared debugger a genuine RISC-V RV32EC machine — the same core the firmware runs on, exposed instruction-accurately.

  • Registers. registers() reads x0-x15 (with their ABI names zero/ra/sp/gp/tp/t0../a0../a5), pc, and the machine CSRs mstatus/mtvec/mepc/mcause live each refresh; the general registers and pc each have a set() that writes straight back into the core (x0 stays hard-wired to zero).
  • Disassembly. FLASH is decoded with the shared riscv decoder (the RISC-V disassembler, which already handles the RV32I base plus the compressed encodings), so the code window shows real RISC-V mnemonics at the real reset address 0x00000000 — the opening j handle_reset, the reset-handler .data copy, and the polling loops.
  • Single-step. Step calls transport.stepInsn, which advances the core exactly one RISC-V instruction (16- or 32-bit); pc and the registers update after each step. You can watch a compressed c.lw/c.sw execute right next to a full 32-bit lui/auipc.
  • Breakpoints & watchpoints. Execution breakpoints are a pc set the run-loop checks before each instruction; write watchpoints wrap the board's memory-write path and pause when a watched address is written (e.g. GPIOD->OUTDR at 0x4001140C or USART1->DATAR at 0x40013804). Both are real.
  • Memory map. Three chips read the real address space side-effect-free: FLASH (program, disassembled, 0x00000000), SRAM (0x20000000), and the peripheral block (0x40000000 — RCC, GPIOA/C/D, USART1). Reading the dynamic registers (USART1 STATR, GPIO INDR) is side-effect-free.

Architecture

The WCH CH32V003 is the famous ~10-cent RISC-V microcontroller: a QingKe RV32EC core (RISC-V RV32E + the compressed extension, 48 MHz) with 16 KB flash and 2 KB SRAM, GPIO, a USART, timers and an ADC in a tiny package. This build models the chip from scratch on a from-scratch RISC-V core — the site's first RISC-V MCU.

  • QingKe RV32EC CPU — runs the real compiled RISC-V image. RV32E is the RV32I base cut down to 16 registers (x0..x15); "C" adds the 16-bit compressed encodings. FLASH is at 0x00000000, SRAM at 0x20000000, peripherals at 0x40000000. On reset the core executes from flash address 0, whose first vector-table entry is j handle_reset. There is no hardware multiply (no M extension), so the toolchain emits shift/add for multiplies.
  • RCC (Reset & Clock Control, 0x40021000) — the firmware gates each peripheral's clock through APB2PCENR before touching it, and spins on the clock-ready bits (HSIRDY, and HSERDY/PLLRDY when requested); the board answers those bits so real clock-setup code proceeds.
  • GPIO ports A/C/D — the CFGLR pin-config, OUTDR output data, the INDR input data and the atomic BSHR/BCR set/reset registers. The on-board user LED is on PD0 (this build's convention: lit when GPIOD->OUTDR bit 0 is driven high).
  • USART1 (0x40013800) — the serial port. STATR keeps TXE/TC asserted so polling transmit code runs, and every byte written to DATAR is captured onto the on-screen serial console (TX on PD5).
  • SysTick / STK (0xE000F000) — the core's 32-bit up-counter with a compare register (STK_CMP), enable bit (STK_CTLR.STE) and the compare flag (STK_SR), advanced by executed instructions; the SysTick Blink program times the LED off it. The core also models machine-mode trap entry (mtvec vectoring, mret) functionally for the timer/USART IRQs, though the bundled firmware polls.
  • Content. Four original CC0 bare-metal C programs compiled with clang for rv32ec: Blink PD0, SysTick Blink, USART Hello and USART Counter. No proprietary firmware is used.
  • Honest limits. A bare CH32V003 has no on-board audio (no speaker/DAC), so this emulator is genuinely silent with no sound path. It also has no user-input peripheral wired to the firmware (the LED is an output and the serial console is TX-only), so there is no input pad. The ADC, SPI/I2C, the other timers, option bytes and the interrupt-driven paths are not modelled — the bundled firmware polls RCC, GPIO, USART1 and SysTick.