CH32V003
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.
Runs on: Web browser
CH32V003 Online Emulator
Play CH32V003 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Blink PD0 (user LED) | CH32V003 | CH32V003 | open | Open ⛶ | |
| SysTick Blink (STK) | CH32V003 | CH32V003 | open | Open ⛶ | |
| USART Hello | CH32V003 | CH32V003 | open | Open ⛶ | |
| USART Counter | CH32V003 | CH32V003 | open | Open ⛶ |
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.jsis a from-scratch RISC-V interpreter that executes RV32EC: the RV32I integer base restricted to the 16 registersx0..x15(that is what the "E" embedded profile means), plus the "C" 16-bit compressed encodings the compiler emits throughout, plus theZicsrCSR ops and machine-mode traps (ecall/ebreak/mret). The real chip has no hardware multiply, so-march=rv32ecturns 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 isj 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.jsmaps FLASH/SRAM/peripherals at their real CH32V003 addresses, answers the RCC clock-ready bits the firmware spins on, folds GPIOBSHR/BCRintoOUTDR, captures every byte written toUSART1->DATARinto the serial console, and runs the SysTick counter.
| Piece | Kind | What it does |
|---|---|---|
CH32V003.create(canvas) | factory | Build the RV32EC core + the CH32V003 memory map + peripherals. |
load(bytes) / reset() | method | Copy the flat image into FLASH; reset sets PC = 0x00000000 (QingKe boot) and SP to the top of SRAM. |
runFrame() | method | Step a batch of instructions and advance the SysTick counter. |
ledOn() / getSerial() | method | The on-board user LED (PD0) state and the accumulated USART1 output. |
present() | method | Draw 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()readsx0-x15(with their ABI names zero/ra/sp/gp/tp/t0../a0../a5),pc, and the machine CSRsmstatus/mtvec/mepc/mcauselive each refresh; the general registers and pc each have aset()that writes straight back into the core (x0 stays hard-wired to zero). - Disassembly. FLASH is decoded with the shared
riscvdecoder (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 address0x00000000— the openingj handle_reset, the reset-handler.datacopy, 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 compressedc.lw/c.swexecute right next to a full 32-bitlui/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->OUTDRat0x4001140CorUSART1->DATARat0x40013804). 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 at0x00000000, SRAM at0x20000000, peripherals at0x40000000. On reset the core executes from flash address 0, whose first vector-table entry isj 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 throughAPB2PCENRbefore touching it, and spins on the clock-ready bits (HSIRDY, andHSERDY/PLLRDYwhen requested); the board answers those bits so real clock-setup code proceeds. - GPIO ports A/C/D — the
CFGLRpin-config,OUTDRoutput data, theINDRinput data and the atomicBSHR/BCRset/reset registers. The on-board user LED is on PD0 (this build's convention: lit whenGPIOD->OUTDRbit 0 is driven high). - USART1 (
0x40013800) — the serial port.STATRkeepsTXE/TCasserted so polling transmit code runs, and every byte written toDATARis 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 (mtvecvectoring,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.