diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 8db99cc17d..ae1b87ec40 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/builder/esp.go b/builder/esp.go index 739035c089..9b59047a24 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -111,9 +111,6 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // Compute the size of the RAM portion of the image (everything the ROM // bootloader loads, up to and including the appended SHA256 hash). ramImageSize := 0 - if makeImage { - ramImageSize += 4096 - } ramImageSize += 24 // image header (8) + trailer fields (16) for _, seg := range segments { ramImageSize += 8 + len(seg.data) // segment header + data (4-aligned) @@ -284,6 +281,9 @@ func makeESPFirmwareImage(infile, outfile, format string) error { dromSize := 0 if len(dromSegs) > 0 { targetImageOffset := int(dromFlashAddr - flashBase) + if makeImage { + targetImageOffset = int(dromFlashAddr) + } if outf.Len() > targetImageOffset { return fmt.Errorf("ESP32: RAM segments too large (%d bytes), overlap DROM at flash 0x%x", outf.Len(), dromFlashAddr) } @@ -304,6 +304,9 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } iromFlashAddr := dromFlashAddr + uint32(dromPages)*pageSize targetImageOffset := int(iromFlashAddr - flashBase) + if makeImage { + targetImageOffset = int(iromFlashAddr) + } if outf.Len() > targetImageOffset { return fmt.Errorf("ESP32: DROM too large, overlaps IROM at flash 0x%x", iromFlashAddr) } diff --git a/main_test.go b/main_test.go index 2dfd1683f6..99a28ac631 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 74e5c6af0c..c47913ba2a 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 0000000000..464a66cbba --- /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 0000000000..5651db745c --- /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 d94deb0ac9..bea3a6a3d0 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 0000000000..25a2010ca5 --- /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 0000000000..9908027006 --- /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 df74be052d..ab3023a9e2 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 e9cd290818..28c7ff569f 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"] }