279 lines
15 KiB
Markdown
279 lines
15 KiB
Markdown
# 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
|
||
|
||
```text
|
||
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](https://github.com/wokwi/avr8js)** running in the browser.
|
||
|
||
### Execution loop
|
||
|
||
Each animation frame executes approximately 267,000 CPU cycles (16 MHz / 60 FPS):
|
||
|
||
```typescript
|
||
avrInstruction(cpu); // decode + execute one AVR instruction
|
||
cpu.tick(); // advance peripheral timers and counters
|
||
```
|
||
|
||
A speed multiplier (`0.1x` – `10x`) 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](https://github.com/wokwi/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](./PICO_W_WIFI_EMULATION.md) for the SPI handshake, RFC1483 framing, and SLIRP NAT bridge.
|
||
|
||
Compiled with the [earlephilhower arduino-pico](https://github.com/earlephilhower/arduino-pico) core. Serial redirect to UART0 is patched into `sketch.ino` only (other files left alone).
|
||
|
||
See [RP2040 Emulation](./RP2040_EMULATION.md) for the full peripheral model.
|
||
|
||
---
|
||
|
||
## Xtensa ESP32 / ESP32-S3 (QEMU)
|
||
|
||
Backed by the **[lcgamboa QEMU fork](https://github.com/lcgamboa/qemu)** 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](../backend/app/services/espidf_compiler.py) wrapper.
|
||
|
||
See [ESP32 Emulation](./ESP32_EMULATION.md) for setup and architectural details, [ESP32 WiFi/Bluetooth](./ESP32_WIFI_BLUETOOTH.md) 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](./RISCV_EMULATION.md) 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](./BUILD-QEMU.md) and [BOOT_IMAGES.md](./BOOT_IMAGES.md).
|
||
|
||
See [Raspberry Pi 3 Emulation](./RASPBERRYPI3_EMULATION.md) 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](../backend/app/services/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](./MICROPYTHON_IMPLEMENTATION.md) 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](./wiki/electrical-simulation-user-guide.md) for the user-facing workflow and the [Circuit Emulation series](./wiki/circuit-emulation-overview.md) for engine internals.
|
||
|
||
---
|
||
|
||
## Component System
|
||
|
||
Components are Web Components — either upstream [wokwi-elements](https://github.com/wokwi/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 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.
|
||
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:
|
||
|
||
```typescript
|
||
{
|
||
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) |
|