Free little xtensa emulator: a lightweight ESP32/Xtensa LX6 emulator with experimental ESP32-S3/LX7 support, written in C. Flexe boots unmodified ESP-IDF and Arduino firmware, models the peripherals used by real boards, and includes ARM64 and x86-64 JIT backends.
Flexe is under active development. Bruce, Marauder, Meshtastic, NerdMiner, openHASP, Tasmota, and WLED pass scripted end-to-end scenarios, and the broader production corpus passes the generic interpreter/JIT gate described in Firmware compatibility. The current functional hardware milestone states what must be demonstrated before broader support claims.
- Xtensa LX6 integer, loop, MAC16, floating-point, exception, interrupt, and windowed-register execution
- ESP32 SRAM, ROM, flash, RTC memory, PSRAM, flash MMU, and dual-core model
- Functional and event-timed GPIO, UART, SPI, I2C, I2S, RMT, LEDC, PCNT, MCPWM, TWAI, Ethernet, SDMMC/SDIO, ADC/DAC, RTC, timer, watchdog, and crypto models, with each model's timing limits documented separately
- CYD ILI9341 display, XPT2046 touch, SD/FAT, and SPIFFS integration
- FreeRTOS, ESP timer, NVS, GPIO, Wi-Fi, Bluetooth, VFS, and ROM boundaries needed by production firmware
- Switch interpreter plus tracing JIT for Apple silicon and x86-64
- Differential JIT verification, compiled-firmware hardware gates, stock-ROM scenarios, and reproducible performance benchmarks
Flexe is not cycle accurate. It preserves firmware-visible time and device ordering, but it does not model cache timing or truly simultaneous execution of both cores.
Requirements: a C17 compiler, CMake, OpenSSL, zlib, and pthreads. S3 Ethernet host forwarding is optional and additionally needs libslirp 4.9+.
cmake -S . -B build
cmake --build build -jOn macOS with Homebrew OpenSSL:
cmake -S . -B build -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)"
cmake --build build -jThe main outputs are:
build/xtensa-emu— emulator CLIbuild/xtensa-tests— unit and differential test suitebuild/flexe-stock-rom-test— scripted production-firmware scenario runnerbuild/flexe-generic-rom-test— arbitrary production-ROM probe
Release builds use LTO and host-native tuning by default. Pass
-DNATIVE_ARCH=OFF for portable binaries.
# JIT is enabled by default
./build/xtensa-emu firmware.bin
# Load ELF symbols and stop after a fixed cycle budget
./build/xtensa-emu -s firmware.elf -c 10000000 firmware.bin
# Compare with the interpreter
./build/xtensa-emu --no-jit -s firmware.elf -c 10000000 firmware.bin
# Quiet firmware UART output, or trace instructions to stderr
./build/xtensa-emu -q firmware.bin
./build/xtensa-emu -T -c 1000000 firmware.bin 2>trace.logCommon options:
| Option | Meaning |
|---|---|
-J |
Enable the JIT (the default on ARM64 and x86-64) |
--no-jit |
Run only the interpreter |
--jit-stats |
Print compilation and coverage statistics |
--jit-verify |
Replay compiled blocks in the interpreter and compare state |
-s ELF |
Load symbols and firmware hooks from an ELF image |
-R ROM_ELF |
Load official ESP32 ROM code and data images |
--usb-console |
Use native USB Serial/JTAG instead of UART0 for console output |
--unhandled-report |
Rank unsupported MMIO by register, guest PC, core, and direction; uses the interpreter for accurate attribution |
-c N |
Stop after N aggregate emulated cycles |
-q |
Suppress emulator diagnostics |
-T |
Emit an instruction trace to stderr |
-b ADDR |
Set a breakpoint on both cores |
-m ADDR[:LEN] |
Dump guest memory on exit |
Bruce, Marauder, Meshtastic, NerdMiner, openHASP, Tasmota, and WLED pass scripted board-level scenarios in both engines. See Firmware compatibility for pinned versions, assertions, and remaining board-specific coverage.
./build/xtensa-tests
./scripts/test-fixtures.sh # all Arduino hardware gates
./scripts/test-fixtures.sh spi-master i2c-wire
./scripts/check-stock-roms.sh # curated external ROMs
FLEXE_ROMS=/path/to/roms ./scripts/check-firmware.shProduction ROMs are intentionally not committed. The stock runner accepts
BRUCE_BIN, MARAUDER_BIN, MESHTASTIC_BIN, NERDMINER_BIN,
OPENHASP_BIN, TASMOTA_BIN, and WLED_BIN; the generic runner accepts paths
or a FLEXE_ROMS directory. Set FLEXE_ROM_ELF for images that use data from
the official ESP32 mask ROM, including the pinned Meshtastic build.
See Testing for sanitizer builds, fixture configuration, JIT verification, and what each gate asserts.
Flexe measures two different things:
- real-time factor — simulated ESP32 time divided by host wall time;
1.0xkeeps pace with a 240 MHz ESP32 - retired MIPS — actual guest instructions executed per host second, excluding halted and fast-forwarded time
ARDUINO_CLI=/path/to/arduino-cli ./scripts/bench-compute.sh
MESHTASTIC_BIN=/path/to/meshtastic.bin ./scripts/bench-stock-roms.sh
./scripts/bench-firmware.sh /path/to/firmware.binIn the current Apple-silicon release benchmark, every image in the five-ROM generic corpus clears real time in both engines. The stricter repeated WLED acceptance benchmark sustains 1.482x interpreted and 3.522x under the JIT. See Performance for dated results and methodology.
The core is a switch interpreter with a tracing JIT. Cold or unsupported instructions remain interpreted; hot basic blocks are compiled, chained, and checked against firmware-visible timers and interrupts.
src/ CPU, JIT, memory, peripheral, and service models
tests/ unit and differential tests
tests/fixtures/ Arduino firmware used by hardware gates
tools/ host-side integration runners and diagnostics
scripts/ build, test, corpus, and benchmark entry points
docs/ design, compatibility, testing, and performance notes
Read ARCHITECTURE.md for the detailed design.
MIT