← board

ESP32 real-hardware flash + boot validation (S2/S3, C3)

Scope

The ESP QEMU + GDB path is verified ([[feature-esp32-idf-xtensa]] done). What remains can only be done with a board on USB:

Acceptance

A pxx ESP image boots on a physical board and its UART output matches the oracle; a basic peripheral/ISR fires on hardware. Requires the user's board + USB access.

Everything except the board is now in place (2026-08-02)

The three things that made this "un-automatable" are done; what is left is literally plugging a board in.

1. One command flashes and checks

tools/esp_flash.sh [--chip esp32s2|esp32s3|esp32c3] [--port /dev/ttyUSB0] <prog.pas>

tools/esp_flash.sh is the silicon twin of tools/esp_run.sh — same projects, same compiler flags, same output filter — so a program verified under qemu is re-checked on hardware with one word changed. It compiles the program for the chip, links it into the matching IDF project, writes flash with esptool, reads the serial console for N seconds, and (by default) diffs what the board said against the same program run on x86-64. It finds the port itself when exactly one is present and refuses to guess between several.

2. A program worth running first

test/test_esp_hw_validation.pas — everything it prints is pure computation, so board output must equal the x86-64 run byte for byte. It covers 64-bit arithmetic, a by-value record result, a 9-word argument list (the two xtensa ABI gaps closed this session), and managed strings; it toggles GPIO2 between lines so an LED or a scope shows it is really executing.

Verified under qemu against the oracle on esp32s3 (Xtensa/windowed) and esp32c3 (riscv32). The S2 has no qemu machine, so its first run IS the hardware run.

3. The S2 exists as a target at all

examples/esp32/hello-s2 is new. The S2 was never built for before — every project here was S3 or C3 — and it is half the user's hardware. It builds and links a pxx app_main with idf.py set-target esp32s2; esp_flash.sh --chip esp32s2 uses it as the harness.

Procedure for the board session

. ~/esp/esp-idf/export.sh
make compiler/pascal26

# per board, one line each:
tools/esp_flash.sh --chip esp32s3 test/test_esp_hw_validation.pas
tools/esp_flash.sh --chip esp32s2 test/test_esp_hw_validation.pas
tools/esp_flash.sh --chip esp32c3 test/test_esp_hw_validation.pas

Expected: the seven lines below, then esp_flash: OK — board output matches the x86-64 oracle, and an LED on GPIO2 that changed state a few times.

pxx esp hw validation
pow3^20 3486784401
int64min+1 -9223372036854775807
divmod -9223344366821 -675344
vec 7000011 4199 120
string ABCDEFGH 8
ok

Things that are expected to bite, so they do not read as failures:

The peripheral half is unblocked too (2026-08-02, later)

[[bug-esp-timer-callback-never-dispatched]] is FIXED — it was a 64-bit argument to a C function being passed with only its low word, so esp_timer's period arrived with a stale pointer in its high half. Both chips now run the periodic callback correctly, xtensa included. So the board session gets a second step:

ESP_PXXFLAGS="--no-signals -Fu$PWD/lib/rtl -Fu$PWD/lib/rtl/platform/esp" \
  tools/esp_flash.sh --chip esp32s3 --no-verify --seconds 15 \
  examples/esp32/timer-c3/main/main.pas

Expected:

PXX timer: started
PXX timer: tick=1 ... tick=5
PXX timer: done ticks=5 status=0

(--no-verify because the demo has no meaningful x86-64 run: it is all SDK calls.) That satisfies the acceptance's "a basic peripheral/ISR fires" — the callback is dispatched by the SDK's timer interrupt, which is the real thing on silicon and only emulated in qemu. make test-esp-idf guards the qemu side.

Still worth watching on hardware: qemu's systimer is not the S2/S3 silicon's, so a timer that works in emulation and not on the board would be new information.