velxio/docs/emulator.md

279 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 813), PORTC (A0A5), PORTD (pins 07) on Uno/Nano; all ports on Mega2560; PB0PB5 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 A0A5 — 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 |
|-------------|----------|-----|
| 07 | PORTD | 07 |
| 813 | PORTB | 05 |
| A0A5 | PORTC | 05 |
### 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 PB2PB5. 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 2629 (A0A3) + 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, GPIO3239 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, 03300 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 | 021 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 | 027 — 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) |