flexe

free little xtensa emulator. boots real esp-idf and arduino firmware on virtual esp32 and esp32-s3 hardware.

$ cmake -S . -B build && cmake --build build --target xtensa-emu -j
# c17, cmake, openssl, zlib, and pthreads.

$ ./build/xtensa-emu -q -s hello_world.elf -c 5000000 hello_world.bin
I (0) cpu_start: Starting scheduler on PRO CPU.
Hello world!

$ FLEXE_ROMS=./roms MAX_UNMAPPED=0 REPS=5 ./scripts/bench-firmware.sh
# release + lto; both engines must pass and produce the same uart digest:
meshtastic  25.03× real-time
nerdminer   12.84× real-time
openhasp     8.31× real-time
tasmota      8.29× real-time
wled         2.32× real-time
# same s3 image, same host, esp-emulator 0.48.0 vs flexe (2026-10-09):
cpuburn     0.74s jit vs 13.91s official (1.21b insns, identical checksum)
2.32× slowest current corpus image
(against a 240 mhz esp32)
5 production corpus images
(both engines, matching uart)
2 native jit backends
(arm64 + x86-64)
700+ tests, all passing
(13k+ assertions)

what's in the box

a jit that actually jit

traces hot basic blocks through conditional branches, chains them into long native runs (arm64 + x86-64), and constant-folds flash literal loads. cold code stays interpreted. no correctness cliff — unhandled instructions just run in the interpreter.

runs firmware people actually ship

unmodified bruce, esp32 marauder, meshtastic, nerdminer, openhasp, tasmota, and wled images pass scripted hardware and service scenarios on both engines. pinned output digests catch a jit that produces the wrong result rather than no result.

a little lx6 / lx7 machine

integer and floating-point execution, register windows, loops, exceptions, and interrupts. target descriptors select esp32/lx6 or esp32-s3/lx7 geometry and supported behavior.

dual core, like the real thing

two guest cores take turns on one deterministic timeline, sharing memory and interrupts. retired instructions count work on both cores; idle jumps count only toward elapsed time.

stub superpowers

rom functions (ets_printf, memcpy, sha/aes), freertos tasks/queues/semaphores, esp_timer callbacks, nvs, vfs/spiffs with host file backing, even lwip sockets bridged to the host network.

debugging arsenal

elf symbols, breakpoints, verbose + windowed traces, function-call trees, hierarchical traces for billion-cycle runs, checkpoints you can save and resume later.

tiny and boring (on purpose)

plain c17, a switch-based interpreter, and hand-rolled jit backends. the implementation stays inspectable even as production firmware expands the hardware model.

meet the machine

one session connects target descriptors, the firmware loader, execution engines, shared memory, device models, and host endpoints.

functional hardware, with explicit limits. each firmware pass belongs to a pinned image and scenario; host-backed services and modeled controllers have separate support boundaries.

get it

git clone https://github.com/levkropp/flexe.git
cd flexe
cmake -S . -B build
cmake --build build --target xtensa-emu -j
# then feed it an esp-idf .bin
./build/xtensa-emu -s app.elf -c 10000000 app.bin
c17 compiler cmake openssl zlib + pthreads an esp-idf firmware .bin to play with

production benchmarks measured on apple silicon with a release, lto, host-native build. jit is on by default. real-time factor is measured against a 240 mhz esp32. the esp-emulator 0.48.0 head-to-head (2026-10-09, same s3 images, same host) is documented with the other dated results.