| Pin | Note | Direction | OUT bit | IO_MUX | Level | Connected to |
|---|
| Address | Machine code | Basic | Line | Source |
|---|
Quick start
- Write RISC-V (RV32IM) assembly in the editor, or pick one of the Examples.
- Add parts with the toolbar above the board. Wire a part by clicking one of its pins and then a GPIO pin on the board (or choose the GPIO in the part's settings panel). Ground / 3V3 are connected automatically while Power supply is set to Automatic in ⚙ Settings (above the breadboard, on the right). Wire style in the same dialog switches between curved wires and horizontal / vertical wires; with the latter, click while wiring to place points. If a point is diagonal to the previous one, the wire bends at a right angle, first along the direction you moved the mouse (move back near the previous point to choose again). Backspace removes the last point. To edit a wire afterwards, click it: drag a round handle to move a corner, drag a bar handle to move a segment (this adds corners where the segment meets a pin), and double-click a corner to remove it. Set Power supply to Manual to wire each part's supply pin to a GND or 3V3 pin yourself: a part without power doesn't work, and the rail you choose sets its wiring mode (for example, an LED's fixed leg on 3V3 makes it active-low).
- Press ▶ Run (F5). Use Step (F10), breakpoints (click a line number) and
ebreakto debug. Choose a slow speed such as 10 Hz (trace) to watch each instruction execute.
Memory map
| Address | Region |
|---|---|
| 0x40380000 | .text — your code (internal SRAM, instruction bus) |
| 0x3FCA0000 | .data / .bss / .rodata — your variables |
| 0x3FCDFFF0 | initial stack pointer sp (stack grows down); gp = start of .data |
| 0x60000000 | UART0 |
| 0x60004000 | GPIO |
| 0x60009000 | IO_MUX (MCU_SEL, FUN_IE, FUN_WPU, FUN_WPD are modelled) |
| 0x60023000 | SYSTIMER (16 MHz counter) |
| 0x60043000 | USB Serial/JTAG (EP1 writes go to the serial monitor) |
Execution starts at _start, else main, else the first instruction. Other peripheral addresses (0x60000000–0x600D0FFF) read back what was written, so watchdog and clock setup code does no harm.
GPIO registers (base 0x60004000)
| Offset | Name | Description |
|---|---|---|
| 0x04 | GPIO_OUT_REG | Output level of GPIO0–21 (bit n = GPIOn) |
| 0x08 | GPIO_OUT_W1TS_REG | Write 1s to set output bits |
| 0x0C | GPIO_OUT_W1TC_REG | Write 1s to clear output bits |
| 0x20 | GPIO_ENABLE_REG | Output enable (1 = output, 0 = input) |
| 0x24 | GPIO_ENABLE_W1TS_REG | Write 1s to make pins outputs |
| 0x28 | GPIO_ENABLE_W1TC_REG | Write 1s to make pins inputs |
| 0x3C | GPIO_IN_REG | Current level of every pin (read-only) |
IO_MUX (base 0x60009000)
IO_MUX_GPIOn_REG = 0x60009004 + 4·n. As on the real chip:
| Bit | Name | Effect |
|---|---|---|
| 14:12 | MCU_SEL | IO MUX function (0, 1 or 2). Function 1 is always GPIOn; only pins in a GPIO function are driven by GPIO_OUT / GPIO_ENABLE. |
| 9 | FUN_IE | Input enable. When 0, the pin always reads 0 in GPIO_IN_REG. |
| 8 | FUN_WPU | Internal pull-up |
| 7 | FUN_WPD | Internal pull-down |
A button only connects its pin to GND (or 3V3) while pressed. When released the pin is floating unless a pull resistor is enabled, so set FUN_IE | FUN_WPU for a button to GND (or FUN_IE | FUN_WPD for a button to 3V3). Reading a floating pin or a pin with FUN_IE = 0 prints a warning in the serial monitor. Reset values are the ones a real ESP32-C3 shows when the ROM has booted a program from flash: GPIO2–8 and 10 have IE = 1 and no pull; GPIO9, 12–17, 20 and 21 have IE = 1 and a pull-up; GPIO0/1, 11, 18, 19 have IE = 0. GPIO19 (USB D+) is also pulled up by the USB PHY.
After reset MCU_SEL = 0 (GPIO12/13: 1, set by the ROM's DIO flash setup), and function 0 is not GPIO on every pin:
| Pins | Function 0 | To use as GPIO output |
|---|---|---|
| GPIO0–3, 8–11, 18, 19 | GPIOn | works directly |
| GPIO4–7 | JTAG (MTMS, MTDI, MTCK, MTDO) | set MCU_SEL = 1 |
| GPIO12–17 | SPI flash (SPIHD … SPIQ) | |
| GPIO20, 21 | U0RXD, U0TXD (GPIO21 is driven high by the idle UART) |
Writing GPIO_OUT for an output pin that is not in a GPIO function prints a warning. Inputs (GPIO_IN_REG) work in any function as long as FUN_IE = 1. Other fields (FUN_DRV, …) read back what was written.
SYSTIMER (base 0x60023000)
| Offset | Name | Description |
|---|---|---|
| 0x04 | UNIT0_OP_REG | Write bit 30 (UPDATE) to latch the counter; bit 29 (VALUE_VALID) reads 1 |
| 0x40 | UNIT0_VALUE_HI_REG | Latched counter bits [51:32] |
| 0x44 | UNIT0_VALUE_LO_REG | Latched counter bits [31:0] — 16 000 ticks per ms |
UART0 (base 0x60000000)
| Offset | Name | Description |
|---|---|---|
| 0x00 | UART_FIFO_REG | Write: transmit a byte · Read: receive a byte |
| 0x1C | UART_STATUS_REG | bits [9:0] RXFIFO_CNT, bits [25:16] TXFIFO_CNT (always 0) |
USB-Serial-JTAG (base 0x60043000)
The chip's own USB serial port (the USB-C connector of the SuperMini and the XIAO ESP32C3). The serial monitor's input reaches both this port and UART0.
| Offset | Name | Description |
|---|---|---|
| 0x00 | EP1_REG | Write: queue a byte to send · Read: next received byte |
| 0x04 | EP1_CONF_REG | bit 0 WR_DONE (write 1: send the queued bytes now; needed on real hardware), bit 1 IN_EP_DATA_FREE (room to queue), bit 2 OUT_EP_DATA_AVAIL (a byte was received) |
ecall services
Conveniences like RARS/Venus. Put the service number in a7, the argument in a0. Real hardware has no OS to answer an ecall; the Hardware build adds a small runtime that implements these services over USB-Serial-JTAG and UART0.
| a7 | Service | a7 | Service |
|---|---|---|---|
| 1 | print int a0 | 32 | sleep a0 milliseconds |
| 4 | print string at a0 | 30 | a0/a1 = milliseconds since start |
| 11 | print char a0 | 12 | read char → a0 (−1 if none) |
| 34 | print hex a0 | 35 / 36 | print binary / unsigned |
| 10 | exit | 93 | exit with code a0 |
Assembler
Instructions: all of RV32I and the M extension, plus Zicsr (csrrw/s/c[i]), ecall, ebreak, fence, wfi, mret.
Pseudo-instructions: nop li la mv not neg seqz snez sltz sgtz beqz bnez blez bgez bltz bgtz bgt ble bgtu bleu j jr jal label ret call tail csrr csrw csrs csrc rdcycle rdtime rdinstret, lw rd, label and sw rs, label, rt.
Directives: .text .data .bss .rodata .section .equ/.set (or NAME = value) .word .half .byte .ascii .asciz/.string .space/.zero .align .balign .globl.
Expressions: + - * / % << >> & | ^ ~, parentheses, 'c' characters, 0x/0b numbers, %hi(sym)/%lo(sym).
Labels: name: and numeric local labels (1: referenced as 1b / 1f). Comments start with #, // or ;, or use /* … */.
Boards
Pick the board above the breadboard. Wires to GPIOs the new board doesn't have are removed.
| Board | GPIOs on the headers | On the board |
|---|---|---|
| ESP32-C3-DevKitM-1 | 0–10, 18–21 | BOOT button (GPIO9, active low), RST, USB-UART bridge on UART0. The RGB LED on GPIO8 is not modelled. |
| ESP32-C3 SuperMini | 0–10, 20, 21 (USB-C up: left IO5–IO10, IO20, IO21; right 5V, GND, 3V3, IO4–IO0) | Blue LED on GPIO8 (lit when the pin is LOW), BOOT (GPIO9), RST, USB-C = the chip's USB-Serial-JTAG (GPIO18/19) |
| Seeed Studio XIAO ESP32C3 | 2–10, 20, 21 (USB-C up: left D0–D6 = IO2–IO7, IO21; right 5V, GND, 3V3, D10–D7 = IO10, IO9, IO8, IO20). Pin tooltips show the D names. | BOOT (GPIO9), RST, USB-C = the chip's USB-Serial-JTAG (GPIO18/19). No user LED; UART0 is on D7 (RX) / D6 (TX). |
| ESP32-C3 Lab Board | Same as the SuperMini (the female headers J2 / J1 beside the module repeat its pins) | A SuperMini on a carrier board with: 8 LEDs LED1–LED8 on GPIO0–7 (pin → LED → resistor → GND, lit when HIGH); a common-cathode 7-segment digit with a–g, dp on GPIO0–7; push buttons PB1–PB4 on GPIO9, 10, 20, 21 (pin → GND while pressed; keys 1–4). Click a jumper to install / remove it: LED and 7S connect the LEDs' and the display's ground, PB the buttons' 10k pull-ups. GPIO4–7 start in the JTAG function and GPIO20/21 as U0RXD/U0TXD (U0TXD drives GPIO21 HIGH), so set MCU_SEL = 1 in their IO_MUX registers first. |
LED matrix (MAX7219)
An 8×8 LED matrix module with a MAX7219 driver, or 4 of them daisy-chained (32×8; choose Modules in the part's settings). Wire DIN, CS (LOAD) and CLK to three GPIO outputs and send 16-bit words by bit-banging, MSB first: the MAX7219 shifts DIN in on each rising edge of CLK and stores the word when CS goes from LOW to HIGH. Bits 11–8 are the register address, bits 7–0 the data. The simulator follows every pin change as it happens, so clock edges are never missed at any speed.
| Register | Address | Data |
|---|---|---|
| No-op | 0x0 | Used to skip a chip in a chain |
| Digit 0–7 | 0x1–0x8 | Row 0–7 (top to bottom); bit 7 is the leftmost LED |
| Decode mode | 0x9 | Bit n = 1: digit n shows a Code B character (0–9, -, E, H, L, P, blank) from data bits 3–0, bit 7 = DP |
| Intensity | 0xA | 0–15: brightness (duty cycle (2n+1)/32) |
| Scan limit | 0xB | 0–7: show digits 0..n only |
| Shutdown | 0xC | 0 = display off, 1 = normal operation |
| Display test | 0xF | 1 = all LEDs on (overrides everything else) |
At power-up (and whenever the simulation is reset) the chip is in shutdown with every register 0, so a program must at least set the scan limit and leave shutdown. In a chain, send one word per chip between CS LOW and CS HIGH: the first word ends up in the chip farthest from the board (the rightmost module); use no-ops for chips you don't want to change. The module is powered from 5V (or 3V3) and GND when Power supply is Manual. See the LED matrix (MAX7219) example.
Running on a real board
Connect the board over USB and use the ⚡ Hardware menu (Chrome or Edge on a desktop, page opened from https:// or localhost; other browsers can only download the .bin):
- Run on board (RAM) resets the board into its ROM bootloader, loads the program into SRAM and starts it. Nothing is written to flash.
- Flash to board writes a firmware image at flash offset 0x0 and restarts the board, so the program runs at every power-up (it replaces whatever was in flash).
- Download firmware (.bin) saves the image for
esptool.py --chip esp32c3 write_flash 0x0 firmware.bin.
Serial port: the top of the menu lists the ports this page may use and marks the one in use (also shown next to the menu). Browsers don't tell web pages a port's COM / tty name, so ports are named after their USB chip (e.g. ESP32 USB-Serial-JTAG (303a:1001) for the SuperMini and the XIAO ESP32C3, CP210x USB-UART for the DevKitM-1). Choose another port… opens the browser's own list, which does show the COM names; Forget this port removes the permission.
The program keeps the simulator's addresses (code at 0x40380000, data at 0x3FCA0000). A runtime added in front of it turns off the watchdogs, sets sp (0x3FCDA000) and gp, answers the ecall services, and prints mcause / mepc / mtval if the program crashes. While it runs, the serial monitor shows the board's output (including the ROM boot messages) and sends what you type to the board. If the board doesn't answer, hold BOOT, press RST, release BOOT and try again.
Differences on the real chip: the CPU runs at 20 MHz (the simulator: 160 MHz), so time delays with SYSTIMER rather than instruction loops. The ESP32-C3 has no cycle / time / instret CSRs; the runtime emulates them with the chip's cycle counter (CSR 0x7E2, upper halves read 0). On the SuperMini and the XIAO ESP32C3, UART0 is on GPIO20/21, not on the USB-C port.
Keyboard
F5 run/pause · F10 step · Ctrl+Enter restart · Ctrl+S save in browser · Ctrl+B show/hide the code editor · Ctrl+J show/hide the bottom panels · Del delete selected part/wire · Esc cancel wiring. Buttons can be given a keyboard key in their settings.