flexe

free little xtensa emulator. boots real esp-idf firmware, models esp32 hardware, and tests production roms end to end.

$ cmake -S . -B build && cmake --build build -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
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.

full lx6 isa

alu, shifts, branches, zero-overhead loops, mac16, fpu, guest-managed window overflow/underflow vectors, exceptions and interrupts (levels 1–7, ccompare timers, waiti).

dual core, like the real thing

pro cpu + app cpu, synchronized cycle counts, freertos preemption on both. the benchmark counts instructions executed on both cores — no funny accounting.

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.

get it

git clone https://github.com/levkropp/flexe.git
cd flexe
cmake -S . -B build
cmake --build build -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.