Velxio uses real CPU emulation on every supported board — never a simplified state machine. This document walks through each backend and how they share the same UI and bus abstractions.
There are five distinct CPU backends:
| Backend | CPU family | Boards | Where it runs |
|---|---|---|---|
| avr8js | AVR8 (ATmega328P, ATmega2560, ATtiny85) | Uno, Nano, Mega 2560, ATtiny85 | Browser (TypeScript) |
| rp2040js | ARM Cortex-M0+ | Pi Pico, Pi Pico W | Browser (TypeScript) |
| QEMU lcgamboa (Xtensa) | Xtensa LX6 / LX7 | ESP32 DevKit V1, DevKit C V4, ESP32-CAM, Wemos Lolin32 Lite, ESP32-S3, XIAO ESP32-S3, Arduino Nano ESP32 | Backend (libqemu-xtensa.so/dll) |
| QEMU lcgamboa (RISC-V) | RISC-V RV32IMC | ESP32-C3 DevKit, XIAO ESP32-C3, ESP32-C3 SuperMini | Backend (libqemu-riscv32.so/dll) |
| QEMU upstream (ARM) | ARM Cortex-A7/A53/A72/A76 | Raspberry Pi Zero / 1 / 2 / 3B / 4B / 5 | Backend (qemu-system-arm / qemu-system-aarch64) |
The browser backends (avr8js, rp2040js) execute in a Web Worker — no roundtrip to the server during simulation. The QEMU backends run as Python-managed subprocesses and stream events over WebSocket to the frontend.
User Code (Monaco Editor — multi-file)
v
useEditorStore (Zustand)
v
FastAPI backend --> arduino-cli / ESP-IDF / rp2040 toolchain --> .hex / .bin / .uf2
v
Per-board Simulator (browser) OR QEMU bridge (backend WebSocket)
v
CPU execution loop (~60 FPS via requestAnimationFrame, OR event-driven over WS)
v
GPIO / UART / I2C / SPI / RMT / PWM bus events
v
PinManager --> ngspice analog netlist (if enabled)
v
PartSimulationRegistry --> wokwi-element + Velxio Web Components update
The same useSimulatorStore + PinManager layer sits in front of every backend. Components don't know whether they're talking to avr8js, rp2040js, or QEMU — they just subscribe to pin 13 went HIGH events.
Backed by avr8js running in the browser.
Each animation frame executes approximately 267,000 CPU cycles (16 MHz / 60 FPS):
avrInstruction(cpu); // decode + execute one AVR instruction
cpu.tick(); // advance peripheral timers and countersA speed multiplier (0.1x – 10x) scales the cycles-per-frame budget.
| Peripheral | Notes |
|---|---|
| GPIO | PORTB (pins 8–13), PORTC (A0–A5), PORTD (pins 0–7) on Uno/Nano; all ports on Mega2560; PB0–PB5 on ATtiny85 |
| Timer0 / Timer1 / Timer2 | millis(), delay(), PWM via analogWrite() (registers polled each frame: OCR0A/B, OCR1AL/BL, OCR2A/B) |
| USART | Full transmit + receive, auto baud-rate detection from UBRR |
| ADC | 10-bit, 5 V ref on A0–A5 — voltage is injected from the live SPICE node, not from setAnalog() calls |
| SPI | Hardware SPI peripheral (ILI9341, SD card, NeoPixel single-color) |
| I2C (TWI) | Hardware I2C with virtual device bus (DS1307, TMP102, EEPROM, BMP280) |
| Arduino Pin | AVR Port | Bit |
|---|---|---|
| 0–7 | PORTD | 0–7 |
| 8–13 | PORTB | 0–5 |
| A0–A5 | PORTC | 0–5 |
8 MHz internal / 16 MHz external (PLL), all 6 I/O pins, USI peripheral (used as Wire library backend), Timer0/Timer1, 10-bit ADC on PB2–PB5. Compiled with AttinyCore (ATTinyCore:avr:attinyx5:chip=85,clock=16pll).
Backed by rp2040js running in the browser.
| Feature | Details |
|---|---|
| Clock | 133 MHz, dual-core (we run core0 only for now) |
| GPIO | All 30 pins — input, output, event listeners, pin-state injection |
| UART | UART0 + UART1, Serial Monitor wires up to UART0 by default |
| ADC | 12-bit on GPIO 26–29 (A0–A3) + internal temperature sensor (ch4) |
| I2C | I2C0 + I2C1, master mode, virtual device bus |
| SPI | SPI0 + SPI1, configurable loopback or custom MISO injection |
| PWM | Any GPIO, full duty-cycle readout |
| Timing | WFI fast-forward — delay() advances simulation time instead of busy-waiting |
| Oscilloscope hook | GPIO transition timestamps at ~8 ns resolution (feeds the on-canvas oscilloscope) |
The Pico W ships a simulated CYW43439 WiFi chip. See Pico W WiFi Emulation for the SPI handshake, RFC1483 framing, and SLIRP NAT bridge.
Compiled with the earlephilhower arduino-pico core. Serial redirect to UART0 is patched into sketch.ino only (other files left alone).
See RP2040 Emulation for the full peripheral model.
Backed by the lcgamboa QEMU fork running as a libqemu-xtensa.{dll,so,dylib} shared library, embedded by the FastAPI backend. The frontend talks to it over a WebSocket bridge (/ws/sim/{board_id}).
| Feature | Notes |
|---|---|
| Clock | LX6 @ 240 MHz (ESP32), LX7 @ 240 MHz (S3), dual-core models |
| GPIO | All 40 pins on classic ESP32; direction tracking, GPIO32–39 fix (input-only quirk handled), pin-state callbacks |
| UART | UART0, UART1, UART2 — multi-UART serial, auto baud-rate detection |
| ADC | 12-bit on every ADC-capable pin, 0–3300 mV injection from canvas potentiometers and from the live SPICE node |
| I2C | Synchronous bus + virtual device response |
| SPI | Full-duplex with configurable MISO byte injection |
| RMT / NeoPixel | Hardware RMT decoder, WS2812 24-bit GRB frame decoding |
| LEDC / PWM | 16 channels, duty-cycle readout, LEDC->GPIO mapping, LED brightness |
| WiFi | SLIRP NAT — connect with WiFi.begin("PICSimLabWifi", "") |
| BLE | Advertising + basic GAP via QEMU's BLE shim |
Toolchain pinned to arduino-esp32 2.0.17 (IDF 4.4.x) — only version compatible with the lcgamboa WiFi shim. ESP-IDF projects (idf.py) supported via the espidf_compiler wrapper.
See ESP32 Emulation for setup and architectural details, ESP32 WiFi/Bluetooth for the radio stack.
Same QEMU backend pattern as Xtensa, different library (libqemu-riscv32.{dll,so,dylib}) and machine type (esp32c3-picsimlab).
| Feature | Notes |
|---|---|
| ISA | RV32IMC @ 160 MHz |
| GPIO | 0–21 via W1TS/W1TC MMIO registers |
| UART | UART0 to the Serial Monitor |
| ADC | 12-bit, ADC1 channels |
| WiFi / BLE | Same SLIRP path as Xtensa ESP32 |
A TypeScript ISA layer also lives at frontend/src/simulation/RiscVCore.ts + Esp32C3Simulator.ts. It exists for Vitest unit-test infrastructure — it does not implement the 150+ ROM functions ESP-IDF needs and is not the production emulation path. All shipped ESP32-C3 simulation goes through QEMU.
See RISC-V Emulation for details.
The Pi family runs in upstream QEMU (no patches): qemu-system-arm for Pi Zero/1/2 and qemu-system-aarch64 for Pi 3B / 4B / 5.
| Feature | Notes |
|---|---|
| CPU | Cortex-A7 (Zero/1/2), A53 (3B), A72 (4), A76 (5) |
| Machine | virt for armhf, raspi3b for Pi 3B (full BCM2837), virt again for 4/5 |
| OS | Raspberry Pi OS (Trixie) — real Linux userland, runs Python 3 scripts directly |
| GPIO | 0–27 — output, input, event detection, PWM (binary state). Driven by an in-image RPi.GPIO shim that streams events over ttyAMA1 |
| Serial | ttyAMA0 for the user-facing Serial Monitor, ttyAMA1 for the GPIO protocol |
| Storage | qcow2 overlay on top of the base SD image — base never mutates, session state is isolated |
| File system | UI-side Virtual File System (useVfsStore) — edit Python on the canvas, upload to the Pi at boot |
| Multi-board | UART bridge to AVR / RP2040 / ESP32 instances on the same canvas |
Boot kernels (kernel8.img), device trees (bcm2710-rpi-3-b.dtb), and base OS images live in img/ and are bundled into the Docker image. To rebuild from scratch, see BUILD-QEMU.md and BOOT_IMAGES.md.
See Raspberry Pi 3 Emulation for the full bridge protocol.
| Language | Boards | Toolchain |
|---|---|---|
| Arduino C++ | Every board | arduino-cli (AVR / RP2040 / ATtiny / ESP32) |
| ESP-IDF C | All ESP32 variants | idf.py (via espidf_compiler.py) |
| MicroPython | Pico, Pico W, all ESP32 / ESP32-S3 / ESP32-C3 | Pre-built MicroPython firmware booted under QEMU; user .py files mounted via VFS |
| Python 3 | All Raspberry Pi boards | Native — runs on the booted Pi OS rootfs with the RPi.GPIO shim pre-installed |
The languageMode field on each BoardInstance toggles between arduino and micropython. See MicroPython Implementation for how .py files reach the running firmware on Pico and ESP32.
Multiple boards can sit on the same canvas, each with its own file group, its own running state, and its own Serial Monitor:
- A potentiometer wired to A0 of board A and A1 of board B is the same SPICE node — both ADCs read the same voltage.
- UART TX of board A wired to UART RX of board B forwards bytes through the backend message bus.
- Each board's pin state changes feed the same
PinManager, so a NeoPixel ring wired to two boards reflects whichever wrote last.
Board instance IDs are deterministic — the first board of a kind uses the bare boardKind string (e.g. arduino-uno) as its ID; subsequent ones get a suffix. Example payloads in frontend/src/data/examples.ts (boards: [...] field) demonstrate the multi-board format.
- AVR / RP2040 — Intel HEX produced by
arduino-cli. Parser infrontend/src/utils/hexParser.tsreads:-prefixed lines, extracts addresses, returns aUint8Array. AVRSimulator widens it toUint16Array(16-bit words, little-endian). - ESP32 family — ESP-IDF
.binfiles (bootloader + partition table + app). The backend writes them straight into the QEMU image at boot. - Pi family — no firmware artifact; the user's Python files are uploaded to the running OS via the VFS at boot.
When the Electrical Sim toggle is on, the simulator builds a SPICE netlist every frame:
NetlistBuilder(frontend/src/simulation/spice/NetlistBuilder.ts) runs Union-Find onwires[]to coalesce connected pins into electrical nodes.- Each component's
componentToSpice.tsmapper emits SPICE cards (R, C, L, D, Q, M, V, I, op-amp subcircuits, …). - Each MCU digital output pin becomes a Thevenin source — voltage from the port driver, output impedance from the port driver model.
useElectricalStorecalls into the lazy-loaded ngspice WASM (SpiceEngine.lazy.ts) and asks for a.optransient point.- Node voltages flow back: each MCU ADC reads its node's voltage on the next
analogRead(), instruments (voltmeter, ammeter, oscilloscope) read their probe nodes directly.
See Electrical Simulation User Guide for the user-facing workflow and the Circuit Emulation series for engine internals.
Components are Web Components — either upstream wokwi-elements (Lit) or Velxio-native (vanilla HTMLElement + Shadow DOM).
- The component is rendered on
SimulatorCanvasvia<DynamicComponent>, which callsdocument.createElement(metadata.tagName). DynamicComponentextracts thepinInfogetter from the rendered DOM (100ms polling, 2s timeout).- If the component is in
PartSimulationRegistry, its handlers are attached:- Output components —
onPinStateChange(pinName, state, element)updates the element's properties when the bus says the pin changed. - Input components —
attachEvents(element, simulator, pinHelper)registers DOM event listeners that inject pin state into the CPU.
- Output components —
- Arduino pin assignment is resolved from connected wires (or from the property dialog when not wired).
Wires use orthogonal routing (no diagonal paths). Each wire stores:
{
id: string
start: { componentId, pinName, x, y }
end: { componentId, pinName, x, y }
color: string // e.g. 'red' for VCC
signalType: 'digital' | 'analog' | 'power-vcc' | 'power-gnd' | 'pwm' | 'i2c' | 'spi' | 'usart'
}Wire positions update automatically when components move (updateWirePositions(componentId) after drag, with retries at 100/300/500 ms for board-internal pins that mount asynchronously).
| File | Purpose |
|---|---|
frontend/src/simulation/AVRSimulator.ts |
AVR8 CPU emulator wrapper |
frontend/src/simulation/RP2040Simulator.ts |
RP2040 wrapper, WFI fast-forward |
frontend/src/simulation/Esp32Bridge.ts |
WebSocket bridge to the Xtensa/RISC-V QEMU worker |
frontend/src/simulation/RaspberryPi3Bridge.ts |
UART + GPIO bridge to the QEMU Pi instance |
frontend/src/simulation/RiscVCore.ts |
RV32IMC TypeScript ISA (test-only) |
frontend/src/simulation/PinManager.ts |
Pin-to-component mapping, listener dispatch |
frontend/src/simulation/PartSimulationRegistry.ts |
Per-component output/input handlers |
frontend/src/simulation/spice/ |
ngspice WASM, NetlistBuilder, MNA solver, component mappers |
frontend/src/utils/hexParser.ts |
Intel HEX parser |
frontend/src/utils/pinPositionCalculator.ts |
Pin coordinate conversion (element -> canvas) |
| File | Purpose |
|---|---|
backend/app/services/arduino_cli.py |
arduino-cli subprocess wrapper |
backend/app/services/espidf_compiler.py |
ESP-IDF subprocess wrapper |
backend/app/services/qemu_manager.py |
QEMU process lifecycle (Pi family) |
backend/app/services/esp32_worker.py |
libqemu-xtensa / libqemu-riscv32 process supervisor |
backend/app/services/gpio_shim.py |
RPi.GPIO -> WebSocket bridge for Pi family |
backend/app/api/routes/compile.py |
/api/compile/ — Arduino compile (sync + async) |
backend/app/api/routes/compile_chip.py |
/api/compile/chip/ — Custom-chip WASM compile |
backend/app/api/routes/simulation.py |
/ws/sim/{board_id} — QEMU WebSocket bridge |
backend/app/api/routes/iot_gateway.py |
HTTP proxy for ESP32 web servers (real browser <-> simulated ESP32) |