DEV Community

Cover image for How I Built a Multi-Architecture Embedded Simulator That Runs in the Browser
David Montero Crespo
David Montero Crespo

Posted on

How I Built a Multi-Architecture Embedded Simulator That Runs in the Browser

Building Velxio: a browser-based emulator for 19 microcontroller boards using avr8js and QEMU

When I started building Velxio, I just wanted to blink an LED on a virtual Arduino Uno. A year later, it emulates 19 boards across 5 CPU architectures — and the most interesting part was figuring out how to run an ESP32 and a Raspberry Pi 3 in a web browser.

Here's how it works.

The Challenge

Embedded development has a hardware problem: you need physical boards to test your code. Simulators exist, but most are either cloud-only (Wokwi), desktop-only (Proteus), or limited to a single architecture.

I wanted to build something that:

  • Runs real compiled binaries (not interpreted pseudocode)
  • Supports multiple CPU architectures
  • Works entirely in the browser
  • Is open source and self-hostable

Architecture Overview

The key insight was that not every architecture needs the same emulation strategy. Some CPUs are simple enough to emulate in JavaScript. Others need QEMU on the backend.

┌─────────────────────────────────────────────┐
│                  Browser                     │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │  avr8js   │  │ rp2040js │  │ RISC-V TS │  │
│  │  (AVR8)   │  │ (RP2040) │  │ (ESP32-C3)│  │
│  └──────────┘  └──────────┘  └───────────┘  │
│                                              │
│  Monaco Editor  │  Component Canvas          │
│  Serial Monitor │  Wire System               │
└──────────────┬──────────────────────────────┘
               │ API calls
┌──────────────▼──────────────────────────────┐
│                  Backend                     │
│                                              │
│  ┌────────────┐  ┌──────────────────────┐   │
│  │ arduino-cli │  │ QEMU                 │   │
│  │ (compiler)  │  │ ├─ Xtensa (ESP32)    │   │
│  └────────────┘  │ └─ raspi3b (Pi 3)     │   │
│                  └──────────────────────┘   │
│  FastAPI + Python                            │
└──────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Client-Side: AVR8 Emulation

The simplest architecture. avr8js is a TypeScript AVR emulator that runs Arduino code cycle-accurately at 16 MHz.

The execution loop runs at ~60 FPS. Each frame, we execute enough cycles to keep up with real time:

const CYCLES_PER_FRAME = 16_000_000 / 60; // ~267,000 cycles

function runFrame() {
  for (let i = 0; i < CYCLES_PER_FRAME; i++) {
    avrInstruction(cpu);  // Execute one AVR instruction
    cpu.tick();            // Update timers and peripherals
  }
  requestAnimationFrame(runFrame);
}
Enter fullscreen mode Exit fullscreen mode

GPIO changes are detected via port listeners:

portB.addListener((value, oldValue) => {
  // Pin 13 (built-in LED) is bit 5 of PORTB
  const led = (value >> 5) & 1;
  updateLED(led);
});
Enter fullscreen mode Exit fullscreen mode

This is entirely client-side. No server needed for AVR boards.

Client-Side: Custom RISC-V Core

This was the most fun part. The ESP32-C3 uses a RISC-V RV32IMC core. Rather than running QEMU in the browser (too heavy), I wrote a RISC-V instruction decoder and executor in TypeScript.

It handles the base integer instructions (RV32I), multiplication/division (M extension), and compressed instructions (C extension). The ESP32-C3 runs at 160 MHz emulated, and the CH32V003 at 48 MHz.

The advantage: zero server dependency for these boards. The disadvantage: no FPU or vector extensions (yet).

Server-Side: QEMU for ESP32 and Pi 3

The Xtensa ISA (used by ESP32, ESP32-S3) is complex enough that browser emulation isn't practical. Instead, Velxio runs QEMU on the backend.

For ESP32, I use the lcgamboa QEMU fork which adds:

  • ESP32 machine definitions
  • GPIO, ADC, and timer peripheral emulation
  • SPI flash emulation
  • ROM function stubs

For Raspberry Pi 3, it's standard QEMU raspi3b machine — but with a twist. It boots a real Pi OS image with a virtual filesystem, so you can actually run Python scripts on a real Linux kernel.

Sensor Simulation: Beyond GPIO Toggles

The thing I'm most proud of is the sensor simulation. A DHT22 doesn't just output HIGH/LOW — it uses a specific 40-bit one-wire protocol with precise timing.

The emulated sensor responds to the CPU's start signal with the correct timing sequence:

  1. CPU pulls data pin LOW for 1ms (start signal)
  2. Sensor pulls LOW for 80µs, then HIGH for 80µs
  3. Sensor sends 40 bits: 16 humidity + 16 temperature + 8 checksum
  4. Each bit is encoded as 50µs LOW + 26-28µs HIGH (0) or 70µs HIGH (1)

Similarly, the HC-SR04 ultrasonic sensor responds to a 10µs trigger pulse with an echo pulse whose duration encodes the distance.

The Component System

The visual components come from wokwi-elements — Web Components that render as SVG. React renders them on a draggable canvas with:

  • Orthogonal wire routing with 8 signal types
  • Pin overlay system for connections
  • Component rotation and property editing
  • 48+ components (LEDs, displays, sensors, motors, etc.)

What I Learned

  1. Not every arch needs the same approach. In-browser emulation is great for simple CPUs. QEMU is better for complex ones. Don't force everything into one strategy.

  2. Protocol-level sensor simulation matters. GPIO toggles are useless for testing real code. If your DHT library expects specific timing, your simulator needs to provide it.

  3. Local-first is a feature. A lot of people want to test embedded code without sending it to someone else's server.

Try It

19 boards. 5 architectures. 48+ components. Fully local. Open source.

If you work with embedded systems and have opinions about which boards or peripherals to prioritize next, I'd love to hear them.

Top comments (0)