From 1958d6ff9d948dd38d447059dc0bbf2a0164eceb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:22:49 -0700 Subject: [PATCH] test: run ESP32 programs in QEMU --- .github/workflows/linux.yml | 12 ++++++++++++ main_test.go | 30 +++++++++++++++++++++++++++++ src/runtime/runtime_esp32.go | 6 ------ src/runtime/runtime_esp32_abort.go | 11 +++++++++++ src/runtime/runtime_esp32_qemu.go | 28 +++++++++++++++++++++++++++ src/runtime/runtime_esp32xx.go | 4 ---- src/runtime/runtime_esp32xx_exit.go | 7 +++++++ targets/esp32-qemu.json | 5 +++++ targets/esp32.json | 1 - targets/esp32s3.json | 1 - 10 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 src/runtime/runtime_esp32_abort.go create mode 100644 src/runtime/runtime_esp32_qemu.go create mode 100644 src/runtime/runtime_esp32xx_exit.go create mode 100644 targets/esp32-qemu.json diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 0fccb7cea..10e080763 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -196,6 +196,10 @@ jobs: cat /proc/cpuinfo sudo apt-get update sudo apt-get install --no-install-recommends \ + libgcrypt20 \ + libpixman-1-0 \ + libsdl2-2.0-0 \ + libslirp0 \ qemu-system-arm \ qemu-system-riscv32 \ qemu-user \ @@ -216,6 +220,14 @@ jobs: version: "29.0.1" - name: Setup `wasm-tools` uses: bytecodealliance/actions/wasm-tools/setup@v1 + - name: Install Espressif QEMU + run: | + release=esp-develop-9.2.2-20260417 + archive=qemu-xtensa-softmmu-${release//-/_}-x86_64-linux-gnu.tar.xz + curl -fL --retry 3 -o "$RUNNER_TEMP/$archive" \ + "https://github.com/espressif/qemu/releases/download/$release/$archive" + tar -xJf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/qemu/bin" >> "$GITHUB_PATH" - name: Restore LLVM source cache uses: actions/cache/restore@v5 id: cache-llvm-source diff --git a/main_test.go b/main_test.go index 2dfd1683f..99a28ac63 100644 --- a/main_test.go +++ b/main_test.go @@ -261,6 +261,21 @@ func TestTimerStopResetRace(t *testing.T) { runTest("timer_stop_reset_race.go", optionsFromTarget("", sema), t, nil, nil) } +func TestESP32QEMU(t *testing.T) { + t.Parallel() + + options := optionsFromTarget("esp32-qemu", sema) + emuCheck(t, options) + machines, err := exec.Command("qemu-system-xtensa", "-machine", "help").Output() + if err != nil { + t.Fatal("failed to list qemu-system-xtensa machines:", err) + } + if !regexp.MustCompile(`(?m)^esp32\s`).Match(machines) { + t.Skip("qemu-system-xtensa does not support the ESP32 machine") + } + runTest("print.go", options, t, nil, nil) +} + func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { emuCheck(t, options) @@ -539,6 +554,9 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c if config.EmulatorName() == "simavr" { actual = cleanSimAVRTestOutput(actual) } + if config.EmulatorName() == "qemu-system-xtensa" { + actual = cleanESP32QEMUOutput(actual) + } if name == "testing.go" { // Strip actual time. re := regexp.MustCompile(`\([0-9]\.[0-9][0-9]s\)`) @@ -564,6 +582,18 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c } } +func cleanESP32QEMUOutput(output []byte) []byte { + entryLine := bytes.Index(output, []byte("\nentry ")) + if entryLine < 0 { + return output + } + entryLineEnd := bytes.IndexByte(output[entryLine+1:], '\n') + if entryLineEnd < 0 { + return output + } + return output[entryLine+1+entryLineEnd+1:] +} + func cleanSimAVRTestOutput(output []byte) []byte { output = bytes.ReplaceAll(output, []byte{0x1b, '[', '3', '2', 'm'}, nil) output = bytes.ReplaceAll(output, []byte{0x1b, '[', '0', 'm'}, nil) diff --git a/src/runtime/runtime_esp32.go b/src/runtime/runtime_esp32.go index 74e5c6af0..c47913ba2 100644 --- a/src/runtime/runtime_esp32.go +++ b/src/runtime/runtime_esp32.go @@ -78,12 +78,6 @@ var _ebss [0]byte //go:extern _vector_table var _vector_table [0]uintptr -func abort() { - for { - device.Asm("waiti 0") - } -} - // interruptInit installs the Xtensa vector table by writing its address // to the VECBASE special register and ensures all CPU interrupts are // initially disabled. diff --git a/src/runtime/runtime_esp32_abort.go b/src/runtime/runtime_esp32_abort.go new file mode 100644 index 000000000..464a66cbb --- /dev/null +++ b/src/runtime/runtime_esp32_abort.go @@ -0,0 +1,11 @@ +//go:build esp32 && !qemu + +package runtime + +import "device" + +func abort() { + for { + device.Asm("waiti 0") + } +} diff --git a/src/runtime/runtime_esp32_qemu.go b/src/runtime/runtime_esp32_qemu.go new file mode 100644 index 000000000..5651db745 --- /dev/null +++ b/src/runtime/runtime_esp32_qemu.go @@ -0,0 +1,28 @@ +//go:build esp32 && qemu + +package runtime + +import "device" + +func exit(code int) { + qemuExit(code) +} + +func abort() { + qemuExit(1) +} + +func qemuExit(code int) { + // QEMU semihosting expects a2 = SYS_exit (1) and a3 = the exit code. + // AsmFull cannot declare these clobbers, so no Go code may run afterward. + device.AsmFull( + "mov a3, {code}\n"+ + "movi a2, 1\n"+ + "simcall", + map[string]interface{}{"code": code}, + ) + // This loop ensures the clobbered registers are never used again. + for { + device.Asm("waiti 0") + } +} diff --git a/src/runtime/runtime_esp32xx.go b/src/runtime/runtime_esp32xx.go index d94deb0ac..bea3a6a3d 100644 --- a/src/runtime/runtime_esp32xx.go +++ b/src/runtime/runtime_esp32xx.go @@ -55,10 +55,6 @@ func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) * 25 } -func exit(code int) { - abort() -} - func putchar(c byte) { machine.Serial.WriteByte(c) } diff --git a/src/runtime/runtime_esp32xx_exit.go b/src/runtime/runtime_esp32xx_exit.go new file mode 100644 index 000000000..25a2010ca --- /dev/null +++ b/src/runtime/runtime_esp32xx_exit.go @@ -0,0 +1,7 @@ +//go:build (esp32 && !qemu) || esp32c3 || esp32c6 + +package runtime + +func exit(code int) { + abort() +} diff --git a/targets/esp32-qemu.json b/targets/esp32-qemu.json new file mode 100644 index 000000000..990802700 --- /dev/null +++ b/targets/esp32-qemu.json @@ -0,0 +1,5 @@ +{ + "inherits": ["esp32-generic"], + "build-tags": ["qemu"], + "emulator": "qemu-system-xtensa -machine esp32 -nographic -semihosting -drive file={img},if=mtd,format=raw" +} diff --git a/targets/esp32.json b/targets/esp32.json index df74be052..ab3023a9e 100644 --- a/targets/esp32.json +++ b/targets/esp32.json @@ -25,6 +25,5 @@ ], "binary-format": "esp32", "flash-method": "esp32flash", - "emulator": "qemu-system-xtensa -machine esp32 -nographic -drive file={img},if=mtd,format=raw", "gdb": ["xtensa-esp32-elf-gdb"] } diff --git a/targets/esp32s3.json b/targets/esp32s3.json index e9cd29081..28c7ff569 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -25,6 +25,5 @@ ], "binary-format": "esp32s3", "flash-method": "esp32jtag", - "emulator": "qemu-system-xtensa -machine esp32 -nographic -drive file={img},if=mtd,format=raw", "gdb": ["xtensa-esp32-elf-gdb"] }