<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: David Montero Crespo</title>
    <description>The latest articles on Forem by David Montero Crespo (@davidmonterocrespo24).</description>
    <link>https://forem.com/davidmonterocrespo24</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1339759%2Fc88abcd4-0c60-4743-8792-33a49bc2f825.jpeg</url>
      <title>Forem: David Montero Crespo</title>
      <link>https://forem.com/davidmonterocrespo24</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/davidmonterocrespo24"/>
    <language>en</language>
    <item>
      <title>How I Built a Multi-Architecture Embedded Simulator That Runs in the Browser</title>
      <dc:creator>David Montero Crespo</dc:creator>
      <pubDate>Sat, 28 Mar 2026 03:11:15 +0000</pubDate>
      <link>https://forem.com/davidmonterocrespo24/how-i-built-a-multi-architecture-embedded-simulator-that-runs-in-the-browser-3me2</link>
      <guid>https://forem.com/davidmonterocrespo24/how-i-built-a-multi-architecture-embedded-simulator-that-runs-in-the-browser-3me2</guid>
      <description>&lt;h2&gt;
  
  
  Building Velxio: a browser-based emulator for 19 microcontroller boards using avr8js and QEMU
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I wanted to build something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs real compiled binaries (not interpreted pseudocode)&lt;/li&gt;
&lt;li&gt;Supports multiple CPU architectures&lt;/li&gt;
&lt;li&gt;Works entirely in the browser&lt;/li&gt;
&lt;li&gt;Is open source and self-hostable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│                  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                            │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client-Side: AVR8 Emulation
&lt;/h2&gt;

&lt;p&gt;The simplest architecture. &lt;a href="https://github.com/wokwi/avr8js" rel="noopener noreferrer"&gt;avr8js&lt;/a&gt; is a TypeScript AVR emulator that runs Arduino code cycle-accurately at 16 MHz.&lt;/p&gt;

&lt;p&gt;The execution loop runs at ~60 FPS. Each frame, we execute enough cycles to keep up with real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CYCLES_PER_FRAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ~267,000 cycles&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;CYCLES_PER_FRAME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;avrInstruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Execute one AVR instruction&lt;/span&gt;
    &lt;span class="nx"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// Update timers and peripherals&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runFrame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPIO changes are detected via port listeners:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;portB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Pin 13 (built-in LED) is bit 5 of PORTB&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;updateLED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;led&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is entirely client-side. No server needed for AVR boards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-Side: Custom RISC-V Core
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The advantage: zero server dependency for these boards. The disadvantage: no FPU or vector extensions (yet).&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-Side: QEMU for ESP32 and Pi 3
&lt;/h2&gt;

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

&lt;p&gt;For ESP32, I use the &lt;a href="https://github.com/lcgamboa/qemu" rel="noopener noreferrer"&gt;lcgamboa QEMU fork&lt;/a&gt; which adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESP32 machine definitions&lt;/li&gt;
&lt;li&gt;GPIO, ADC, and timer peripheral emulation&lt;/li&gt;
&lt;li&gt;SPI flash emulation&lt;/li&gt;
&lt;li&gt;ROM function stubs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Raspberry Pi 3, it's standard QEMU &lt;code&gt;raspi3b&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sensor Simulation: Beyond GPIO Toggles
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The emulated sensor responds to the CPU's start signal with the correct timing sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CPU pulls data pin LOW for 1ms (start signal)&lt;/li&gt;
&lt;li&gt;Sensor pulls LOW for 80µs, then HIGH for 80µs&lt;/li&gt;
&lt;li&gt;Sensor sends 40 bits: 16 humidity + 16 temperature + 8 checksum&lt;/li&gt;
&lt;li&gt;Each bit is encoded as 50µs LOW + 26-28µs HIGH (0) or 70µs HIGH (1)&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  The Component System
&lt;/h2&gt;

&lt;p&gt;The visual components come from &lt;a href="https://github.com/wokwi/wokwi-elements" rel="noopener noreferrer"&gt;wokwi-elements&lt;/a&gt; — Web Components that render as SVG. React renders them on a draggable canvas with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orthogonal wire routing with 8 signal types&lt;/li&gt;
&lt;li&gt;Pin overlay system for connections&lt;/li&gt;
&lt;li&gt;Component rotation and property editing&lt;/li&gt;
&lt;li&gt;48+ components (LEDs, displays, sensors, motors, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not every arch needs the same approach.&lt;/strong&gt; In-browser emulation is great for simple CPUs. QEMU is better for complex ones. Don't force everything into one strategy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protocol-level sensor simulation matters.&lt;/strong&gt; GPIO toggles are useless for testing real code. If your DHT library expects specific timing, your simulator needs to provide it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local-first is a feature.&lt;/strong&gt; A lot of people want to test embedded code without sending it to someone else's server.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://velxio.dev" rel="noopener noreferrer"&gt;velxio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/davidmonterocrespo24/velxio" rel="noopener noreferrer"&gt;github.com/davidmonterocrespo24/velxio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: AGPLv3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host&lt;/strong&gt;: &lt;code&gt;docker pull ghcr.io/davidmonterocrespo24/velxio&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;19 boards. 5 architectures. 48+ components. Fully local. Open source.&lt;/p&gt;

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

</description>
      <category>iot</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>arduino</category>
    </item>
  </channel>
</rss>
