Skip to content

Latest commit

 

History

History
278 lines (199 loc) · 14.6 KB

File metadata and controls

278 lines (199 loc) · 14.6 KB

Emulator Architecture

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.


High-Level Data Flow

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.


AVR8 Emulation (Arduino Uno / Nano / Mega / ATtiny85)

Backed by avr8js running in the browser.

Execution loop

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 counters

A speed multiplier (0.1x10x) scales the cycles-per-frame budget.

Supported peripherals

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)

Pin mapping (Uno / Nano)

Arduino Pin AVR Port Bit
0–7 PORTD 0–7
8–13 PORTB 0–5
A0–A5 PORTC 0–5

ATtiny85 specifics

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).


RP2040 Emulation (Raspberry Pi Pico / Pico W)

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)

Pico W extras

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.


Xtensa ESP32 / ESP32-S3 (QEMU)

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.


RISC-V ESP32-C3 (QEMU)

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.


ARM Raspberry Pi (QEMU)

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.


Languages

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.


Multi-board canvases

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.


HEX / BIN / UF2 loading

  • AVR / RP2040 — Intel HEX produced by arduino-cli. Parser in frontend/src/utils/hexParser.ts reads :-prefixed lines, extracts addresses, returns a Uint8Array. AVRSimulator widens it to Uint16Array (16-bit words, little-endian).
  • ESP32 family — ESP-IDF .bin files (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.

Co-simulation with ngspice

When the Electrical Sim toggle is on, the simulator builds a SPICE netlist every frame:

  1. NetlistBuilder (frontend/src/simulation/spice/NetlistBuilder.ts) runs Union-Find on wires[] to coalesce connected pins into electrical nodes.
  2. Each component's componentToSpice.ts mapper emits SPICE cards (R, C, L, D, Q, M, V, I, op-amp subcircuits, …).
  3. Each MCU digital output pin becomes a Thevenin source — voltage from the port driver, output impedance from the port driver model.
  4. useElectricalStore calls into the lazy-loaded ngspice WASM (SpiceEngine.lazy.ts) and asks for a .op transient point.
  5. 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.


Component System

Components are Web Components — either upstream wokwi-elements (Lit) or Velxio-native (vanilla HTMLElement + Shadow DOM).

Registration

  1. The component is rendered on SimulatorCanvas via <DynamicComponent>, which calls document.createElement(metadata.tagName).
  2. DynamicComponent extracts the pinInfo getter from the rendered DOM (100ms polling, 2s timeout).
  3. If the component is in PartSimulationRegistry, its handlers are attached:
    • Output componentsonPinStateChange(pinName, state, element) updates the element's properties when the bus says the pin changed.
    • Input componentsattachEvents(element, simulator, pinHelper) registers DOM event listeners that inject pin state into the CPU.
  4. Arduino pin assignment is resolved from connected wires (or from the property dialog when not wired).

Wire routing

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).


Key Source Files

Frontend

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)

Backend

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)