ESP32-S3
ESP32-S3 is Espressif's popular dual-core WiFi and Bluetooth microcontroller, emulated in the browser on a from-scratch Tensilica Xtensa LX7 interpreter, part of the Xtensa family. 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-S3 SoC model at the real hardware addresses: internal SRAM, XIP flash, the GPIO block with the on-board LED on GPIO48, 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-S3 Online Emulator
Play ESP32-S3 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| RGB LED colour cycle | ESP32-S3 | ESP32-S3 | open | Open ⛶ | |
| Mini-REPL (UART0) | ESP32-S3 | ESP32-S3 | open | Open ⛶ | |
| RGB-LED blink | ESP32-S3 | ESP32-S3 | open | Open ⛶ | |
| Hello UART (serial) | ESP32-S3 | ESP32-S3 | open | Open ⛶ | |
| MicroPython v1.24.1 | ESP32-S3 | ESP32-S3 | grey | 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. xtensa-esp32s3.js is adapted from the site's ESP32 (Xtensa LX6) core: the LX7 the ESP32-S3 uses shares the same windowed register file, the 24/16-bit density encodings, L32R, the SAR shifts, the 32-bit multiply and the special registers, so the CPU interpreter is reused unchanged and what differs is the SoC — the ESP32-S3 memory map and peripheral layout (including the bank-1 GPIO registers, because the S3 has GPIO0-48). It is all inspectable JavaScript state, which is exactly what lets the shared debugger drive it.
var sys = window.ESP32S3.createSystem(); // Xtensa LX7 core + ESP32-S3 SoC
sys.loadImage(bytes, 0x40370000); // flat app image into IRAM
sys.setEntry(0x40370000); // enter at the image start
sys.onUartTx = function(b){ /* UART0 TX -> console */ };
sys.feedRx(byte); // keyboard -> UART0 RX FIFO
sys.getPixel(); // decoded WS2812 RGB LED (GPIO48)
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-S3 memory map. The write path notes watched addresses and decodes the WS2812 edge on GPIO48. |
sys.getPixel() | method | The on-board RGB LED — the 24-bit GRB colour decoded from the bit-banged WS2812 waveform on GPIO48, polled each frame to draw the LED. |
sys.feedRx(b) | method | Serial RX in — the keyboard drives the mini-REPL through UART0. |
Debugger integration
The plug-in (esp32-s3-debug.js) describes the ESP32-S3 to the shared debugger and nothing more — the core is not patched:
- Reused Xtensa decoder. The ESP32-S3's LX7 uses the same instruction encodings as the LX6, so the shared from-scratch disassembler at
/debugger/src/cpus/xtensa.js(registered asxtensa) is reused unmodified; the CPU is shown as "Xtensa LX7 (ESP32-S3)". 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 the Hello firmware's 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
0x40370000(disassembled), DRAM at0x3FC88000, XIP flash at0x42000000(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-S3 (2020) is a low-cost WiFi + Bluetooth LE microcontroller built on a dual-core 32-bit Tensilica Xtensa LX7:
- Xtensa LX7 — two 32-bit cores at up to 240 MHz. Like the ESP32's LX6, the LX7 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. Because the LX7 shares these with the LX6 for the code this firmware runs, the site's from-scratch Xtensa core is reused and only the SoC differs.
- Memory — internal SRAM reachable from the instruction bus at
0x40370000(IRAM) and the data bus at0x3FC88000(DRAM); code also executes in place (XIP) from external SPI flash mapped at0x42000000(and rodata at0x3C000000); a small RTC memory sits at0x50000000. All are at their real hardware addresses — note how they differ from the classic ESP32. - On-board RGB LED — an ESP32-S3-DevKitC carries a single addressable WS2812 RGB LED on GPIO48. GPIO48 lives in the GPIO block's bank-1 registers (GPIO_OUT1 / GPIO_ENABLE1 at
0x60004010/0x6000402Cand their W1TS/W1TC set/clear pairs). The firmware bit-bangs the one-wire 800 kHz waveform and the SoC decodes the HIGH-pulse widths back into a 24-bit GRB colour. - Serial — UART0 at
0x60000000is 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 LX7 machine code authored for this site (CC0), assembled to flat ESP32-S3 images and run on the from-scratch interpreter. The default is an RGB-LED colour cycle that bit-bangs the on-board WS2812; the picker also has 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 lights the on-board RGB LED.
The MicroPython config runs the stock image on a separate, complete machine (esp32s3-idf-soc.js): core 0 starts at the reset vector, the ROM loads the second-stage bootloader from SPI flash, the bootloader loads the application, FreeRTOS brings up both cores and the REPL task reaches a live >>>. Each core executes one instruction per clock. The REPL has a littlefs filesystem on the emulated flash, and neopixel on GPIO48 lights the on-board RGB LED through the RMT peripheral.
Limits. The bare-metal configs run on core0 only. In every config the WiFi and BLE radios have no RF (in MicroPython, activating WLAN or BLE does not return until Reboot) and watchdogs never reset the chip; timing is instruction-approximate, not cycle-exact; and the bare devkit has no on-board audio, so this emulator is naturally silent. In MicroPython there is no PSRAM, so the boot log shows the same two PSRAM lines a module without it prints; the hardware I2C driver does not return from a transfer (Ctrl-C recovers, and SoftI2C works); ADC, touch and PWM duty read 0, and esp32.mcu_temperature() gives a meaningless value. Files last until the page is reloaded: they survive Reboot, and each page load starts from the stock image.