SearchA-ZE › ESP32

ESP32

2016 Open source · CC0 On-board only Online

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.

ESP32 at Espressif ↗

Runs on: Web browser

ESP32 Online Emulator

Play ESP32 using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Mini-REPL (UART0)ESP32ESP32openOpen ⛶
Blink (on-board LED)ESP32ESP32openOpen ⛶
Hello UART (serial)ESP32ESP32openOpen ⛶

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:

MemberKindWhat it does
sys.step()methodFetch → 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)methodThe 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 / .rtcfieldThe real memories as typed arrays — read side-effect-free for the hex + disassembly views.
sys.read8/16/32, sys.write8/16/32methodThe bus, decoding the ESP32 memory map. The write path notes watched addresses.
sys.ledOn()methodTrue when GPIO2 is driven high (output enabled) — polled each frame to draw the on-board LED.
sys.feedRx(b)methodSerial 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.js and registered as xtensa, 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 at 0x3FFB0000, XIP flash at 0x400D0000 (disassembled), and RTC memory at 0x50000000. 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 at 0x3FFB0000 (DRAM); code also executes in place (XIP) from external SPI flash mapped at 0x400D0000 (and rodata at 0x3F400000); a small RTC memory sits at 0x50000000. 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).
  • SerialUART0 at 0x3FF40000 is 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.