ESP32
ESP32 is Espressif's popular dual-core WiFi and Bluetooth microcontroller, emulated in the browser on a from-scratch Tensilica Xtensa LX6 interpreter — the site's first Xtensa machine. Xtensa is a RISC-like ISA with a windowed register file (a0-a15 rotate over a larger physical AR file via WINDOWBASE/WINDOWSTART, driven by CALL4/8/12 + ENTRY + RETW), variable-length 24-bit and 16-bit (density) instructions, a shift-amount register with zero-overhead LOOP registers, and PS/EPC1/EXCCAUSE for exceptions. The core is wired to an ESP32 SoC model at the real hardware addresses: internal SRAM, XIP flash, the GPIO block with the on-board LED on GPIO2, and UART0. It runs REAL bare-metal Xtensa firmware, including an interactive UART REPL you can type into. Because the whole chip is ordinary JavaScript it plugs into the site's shared debugger: a from-scratch Xtensa disassembler, live a0-a15 + PC + the special registers, side-effect-free memory, single-instruction step, execution breakpoints and write-watchpoints. WiFi and BLE have no radio here and are HLE stubs, only core0 is run, and the devkit is silent.
Runs on: Web browser
ESP32 Online Emulator
Play ESP32 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Mini-REPL (UART0) | ESP32 | ESP32 | open | Open ⛶ | |
| Blink (on-board LED) | ESP32 | ESP32 | open | Open ⛶ | |
| Hello UART (serial) | ESP32 | ESP32 | open | Open ⛶ |
Chips
Notes
Embedding
The CPU and SoC are written from scratch for this site (public domain / CC0) — nothing here wraps a third-party core, and this is the site's first Xtensa machine. xtensa-esp32.js is a plain Xtensa LX6 interpreter (windowed register file, 24/16-bit encodings, the SAR shifts, L32R, the 32-bit multiply, the special registers and a basic exception entry) plus an ESP32 SoC model at the real hardware addresses (from the ESP32 TRM and the esp-idf soc.h). It is all inspectable JavaScript state, which is exactly what lets the shared debugger drive it.
var sys = window.ESP32.createSystem(); // Xtensa LX6 core + ESP32 SoC
sys.loadImage(bytes, 0x40080000); // flat app image into IRAM
sys.setEntry(0x40080000); // enter at the image start
sys.onUartTx = function(b){ /* UART0 TX -> console */ };
sys.feedRx(byte); // keyboard -> UART0 RX FIFO
sys.step(); // execute exactly one instruction
We own the run loop so the debugger can drive it. Each frame runs a time-boxed budget of sys.step() (one Xtensa instruction each); breakpoints stop it by PC, write-watchpoints by bus address. Everything is plain JavaScript state:
| Member | Kind | What it does |
|---|---|---|
sys.step() | method | Fetch → decode width (16-bit density vs 24-bit) → execute one instruction, advancing pc and wrapping the zero-overhead LOOP. The single-step primitive. |
sys.ar(i) / sys.setAr(i,v) | method | The current window's a0-a15, resolved through WINDOWBASE over the 64-register physical AR file. sys.getPc()/setPc() and sys.sr (sar/ps/windowbase/windowstart/lbeg/lend/lcount/epc1/exccause) complete the programmer's model — all readable and writable. |
sys.mem.sramI / .sramD / .flash / .rtc | field | The real memories as typed arrays — read side-effect-free for the hex + disassembly views. |
sys.read8/16/32, sys.write8/16/32 | method | The bus, decoding the ESP32 memory map. The write path notes watched addresses. |
sys.ledOn() | method | True when GPIO2 is driven high (output enabled) — polled each frame to draw the on-board LED. |
sys.feedRx(b) | method | Serial RX in — the keyboard drives the mini-REPL through UART0. |
Debugger integration
The plug-in (esp32-debug.js) describes the ESP32 to the shared debugger and nothing more — the core is not patched:
- New decoder. Because this is the first Xtensa machine on the site, a from-scratch disassembler was written at
/debugger/src/cpus/xtensa.jsand registered asxtensa, shown as "Xtensa LX6 (ESP32)". It decodes the variable-length ISA (24-bit when the low nibble op0 is 0-7, 16-bit density forms when 8-13) — the ALU/shift/load-store/branch set, L32R, the windowed and CALL0 calls (CALL0/4/8/12, CALLX, ENTRY, RET, RETW), RSR/WSR/XSR and the narrow forms — and matches the interpreter's encodings exactly. - Registers are read live each refresh: the current window's a0-a15 (resolved through WINDOWBASE), the pc, and the special registers SAR, PS, WINDOWBASE, WINDOWSTART, the LBEG/LEND/LCOUNT loop registers and EPC1/EXCCAUSE. Each has a
set()that writes straight back through the core. Single-stepping a windowed CALL4/ENTRY/RETW visibly rotates WINDOWBASE. - Memory is exposed as four chips, all read side-effect-free directly off the typed arrays, each with its real base so the disassembly lines up with PC: IRAM at
0x40080000(disassembled), DRAM at0x3FFB0000, XIP flash at0x400D0000(disassembled), and RTC memory at0x50000000. The UART/GPIO peripheral window is deliberately not a chip, because reading the UART FIFO pops a byte — memory reads stay side-effect-free. - Single step is one
sys.step(). Breakpoints are host-side checks of the PC (in the running code chip's offset space, so a click in the disasm gutter halts) run instruction-by-instruction when any are set. Write-watchpoints are a set the SoC's write path checks, pausing the loop the moment a watched bus address is written.
Architecture
The Espressif ESP32 (2016) is a low-cost WiFi + Bluetooth microcontroller built on a dual-core 32-bit Tensilica Xtensa LX6:
- Xtensa LX6 — two 32-bit cores at up to 240 MHz. Xtensa is a configurable RISC-like ISA with a windowed register file (the visible a0-a15 rotate over a larger physical AR file via WINDOWBASE/WINDOWSTART, driven by CALL4/8/12 + ENTRY + RETW), variable-length 24-bit and 16-bit (density) instructions, a shift-amount register (SAR) with zero-overhead LOOP registers (LBEG/LEND/LCOUNT), and PS / EPC1 / EXCCAUSE for exceptions. This emulator implements the LX6 core the ESP32 firmware uses — the windowed and CALL0 ABIs, the ALU / SAR shifts / loads-stores / branches, L32R, the 32-bit multiply, RSR/WSR/XSR and a basic trap — from scratch.
- Memory — internal SRAM reachable from the instruction bus at
0x40080000(IRAM) and the data bus at0x3FFB0000(DRAM); code also executes in place (XIP) from external SPI flash mapped at0x400D0000(and rodata at0x3F400000); a small RTC memory sits at0x50000000. All are at their real hardware addresses. - On-board LED — devkits commonly expose a user LED on GPIO2, driven through the GPIO block at
0x3FF44000(GPIO_ENABLE / GPIO_OUT and their W1TS/W1TC set/clear registers). - Serial — UART0 at
0x3FF40000is the console the ROM bootloader and apps log to; its TX FIFO drives the serial panel and its RX FIFO (read through UART_STATUS' RXFIFO_CNT) is fed by the keyboard.
The bundled firmware is real Xtensa LX6 machine code authored for this site (CC0), assembled to flat ESP32 images and run on the from-scratch interpreter. The default is an interactive mini-REPL that evaluates integer expressions over UART0 — because it is real machine code on the real memory map, every instruction single-steps in the debugger and led on visibly toggles the on-board LED.
Honest limits. This is a deliberate honest partial. The from-scratch core runs real Xtensa LX6 and drives the real GPIO / UART, but the full ESP-IDF / FreeRTOS + WiFi/BLE stack is out of scope: only core0 is run (core1 is not modelled), the WiFi and BLE radios have no RF — their register windows are RAM-backed HLE stubs so probing firmware does not hang, but no packets move — and the watchdog is fed so nothing resets. The default content is therefore bare-metal, not the full MicroPython/ESP-IDF image; timing is instruction-approximate, not cycle-exact; and the bare devkit has no on-board audio, so this emulator is naturally silent.