CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/qemu-system-test-runner

Author and run QEMU system emulation as an embedded-test target - qemu-system-arm / qemu-system-aarch64 / qemu-system-riscv32 launching cross-compiled ELF binaries on virtual MCUs and SoCs. Covers machine selection (-M virt / mps2-an385 / mps2-an386 / mps2-an500 / mps2-an511 / mps3-an524 / lm3s6965evb / raspi3b / xilinx-zynq-a9), CPU selection (-cpu cortex-m0 / cortex-m3 / cortex-m4 / cortex-m33 / cortex-a15 / cortex-a57 / max), -kernel ELF load, -nographic + -serial stdio, ARM semihosting via -semihosting-config enable=on,target=native (so cross-compiled GoogleTest / Unity binaries print to host stdio and exit with the test return code), GDB stub via -S -gdb tcp::1234, QMP monitor via -qmp tcp:host:port for automated test orchestration, and CI wiring. Use when host-only test runs are insufficient and the team wants arch-correct (endianness / alignment / interrupt-vector) behaviour on a virtual MCU without committing to physical hardware-in-loop.

65

Quality

82%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
qemu-system-test-runner
description:
Author and run QEMU system emulation as an embedded-test target - qemu-system-arm / qemu-system-aarch64 / qemu-system-riscv32 launching cross-compiled ELF binaries on virtual MCUs and SoCs. Covers machine selection (-M virt / mps2-an385 / mps2-an386 / mps2-an500 / mps2-an511 / mps3-an524 / lm3s6965evb / raspi3b / xilinx-zynq-a9), CPU selection (-cpu cortex-m0 / cortex-m3 / cortex-m4 / cortex-m33 / cortex-a15 / cortex-a57 / max), -kernel ELF load, -nographic + -serial stdio, ARM semihosting via -semihosting-config enable=on,target=native (so cross-compiled GoogleTest / Unity binaries print to host stdio and exit with the test return code), GDB stub via -S -gdb tcp::1234, QMP monitor via -qmp tcp:host:port for automated test orchestration, and CI wiring. Use when host-only test runs are insufficient and the team wants arch-correct (endianness / alignment / interrupt-vector) behaviour on a virtual MCU without committing to physical hardware-in-loop.
metadata:
{"keywords":"qemu, embedded, arm, cortex-m, cortex-a, semihosting, system-emulation, riscv"}

qemu-system-test-runner

Overview

QEMU system emulation (per qemu.org system docs) runs cross-compiled ELF test binaries on virtual MCUs and SoCs. The embedded-relevant binaries are qemu-system-arm, qemu-system-aarch64, qemu-system-riscv32, and qemu-system-riscv64.

In an embedded test pipeline QEMU sits between the host build (fast, but misses arch-specific behaviour) and the physical hardware-in-loop rig (slow, expensive). A cross-compiled GoogleTest or Unity binary runs under QEMU with semihosting; the test's main() returns the failure count; QEMU exits with that code; CI gates on it.

Composes with:

  • googletest-embedded-arm - cross-compiled C++ test binary.
  • unity-test-framework-c - cross-compiled C test binary.
  • ceedling-build-runner - Ceedling project configured for ARM cross-compile.
  • hardware-in-loop-reference - the next rung up the V-cycle (PIL / HIL).

When to use

  • Host tests pass but you suspect arch-specific bugs (endianness, alignment, interrupt-vector ordering, weak-symbol resolution).
  • Physical HIL is expensive / scheduled - QEMU is the V-cycle PIL stage between SIL and HIL.
  • The team wants reproducible "on-target" tests in CI without flashing a real board.
  • Cross-compile target is one of QEMU's supported boards (Cortex-M3/M4/M7/M33 via mps2-* boards; Cortex-A* via virt / raspi / zynq; RISC-V via virt).

QEMU does not emulate analog I/O, sensor wiring, or real-time timing precisely - for that, escalate to a real HIL rig per hardware-in-loop-reference.

Authoring

Machine + CPU selection

Match the machine to the CPU profile: mps2-* boards for M-profile cores, virt for A-profile and RISC-V. The common Cortex-M mapping:

Test targetQEMU commandTypical -cpu
Cortex-M0 / M0+qemu-system-arm -M mps2-an385cortex-m0
Cortex-M3qemu-system-arm -M mps2-an385cortex-m3 (board default)
Cortex-M4qemu-system-arm -M mps2-an386cortex-m4

The full machine list, the A-profile / Raspberry Pi / Xilinx targets, and the max CPU note are in references/boards.md.

Booting the test ELF

For an embedded test, load the ELF with -kernel and send serial to the console with -nographic. The full invocation flag table (-M, -cpu, -smp, -m, -bios, -serial, -monitor, -qmp), per invocation.html, is in references/flags.md.

ARM semihosting

The critical flag for cross-compiled test binaries:

-semihosting-config [enable=on|off][,target=native|gdb|auto][,chardev=name][,arg=string]

Per the same invocation page, semihosting "allows direct host system calls and I/O operations". When the test ELF was linked with --specs=rdimon.specs, printf and exit() from the binary go through ARM semihosting calls; with -semihosting-config enable=on,target=native QEMU services those calls against the host.

Practically: the test binary's main() returns the failure count; the C runtime calls _exit(rc); QEMU exits with rc. CI gates on $?.

Building (the test binary side)

The test side is covered in googletest-embedded-arm and unity-test-framework-c; the canonical Cortex-M4 build:

arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O0 -g \
    --specs=rdimon.specs \
    test/test_ringbuffer.c test/test_ringbuffer_Runner.c \
    src/ringbuffer.c ext/Unity/src/unity.c \
    -o build/test.elf -lrdimon

-lrdimon links the ARM semihosting library (developer.arm.com GNU Toolchain). --specs=rdimon.specs pulls in startup code that wires printf / _write / _exit to the semihosting interface.

Running

Smoke run

qemu-system-arm -M mps2-an386 -cpu cortex-m4 \
    -nographic \
    -semihosting-config enable=on,target=native \
    -kernel build/test.elf

Output (Unity-style):

test/test_ringbuffer.c:34:test_ringbuffer_initially_empty:PASS
test/test_ringbuffer.c:42:test_ringbuffer_push_increments_size:PASS

-----------------------
2 Tests 0 Failures 0 Ignored
OK

Exit code: 0 (or the failure count for Unity / GoogleTest).

Inspecting boot / interrupt behaviour

Per the invocation page, -d "activates debug logging for specified subsystems":

qemu-system-arm -M mps2-an386 -cpu cortex-m4 \
    -nographic -semihosting-config enable=on,target=native \
    -d int,cpu_reset,unimp \
    -kernel build/test.elf 2> qemu-debug.log

-d help lists the available items. int traces interrupts; cpu_reset traces resets; unimp traces unimplemented features (board-quirk hunts).

GDB-attached debug

# Terminal 1
qemu-system-arm -M mps2-an386 -cpu cortex-m4 -nographic \
    -semihosting-config enable=on,target=native \
    -S -gdb tcp::1234 \
    -kernel build/test.elf

# Terminal 2
arm-none-eabi-gdb build/test.elf \
    -ex "target remote :1234" \
    -ex "b main" \
    -ex "continue"

-S "freezes the CPU at startup"; -gdb tcp::1234 "opens a GDB stub on the specified device" (per the invocation page).

QMP for automated orchestration

Per the same invocation reference, -qmp tcp:host:port[,server, nowait] exposes the QEMU Machine Protocol over TCP as JSON-RPC. For a CI orchestration that needs to inject faults mid-test:

qemu-system-arm -M mps2-an386 -cpu cortex-m4 -nographic \
    -semihosting-config enable=on,target=native \
    -qmp tcp:localhost:4444,server,nowait \
    -kernel build/test.elf

A test harness then connects to localhost:4444 and issues {"execute":"query-status"}, {"execute":"stop"}, {"execute":"cont"} - useful for staged fault injection that mirrors the HIL fault-injection patterns.

Parsing results

Exit code (primary signal)

Semihosting's _exit(rc) propagates rc through QEMU. A test binary linked with --specs=rdimon.specs and ending in:

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_x);
    return UNITY_END();   /* failure count */
}

…makes qemu-system-arm ... -kernel test.elf; echo $? return the failure count. CI gates on the exit code directly.

stdout parsing

For Unity / GoogleTest, the canonical line format reaches host stdout via semihosting → QEMU stdio. Grep / tee / pipe-to-JUnit the same way as a host run - see unity-test-framework-c and googletest-embedded-arm.

Timing the run

QEMU emulation is not real-time. For a test that asserts on wall-clock latency, the result is wrong - QEMU executes faster than physical hardware for compute-bound code and slower for I/O-heavy code. For real-time-sensitive tests, escalate to hardware-in-loop-reference.

CI integration

The canonical GitHub Actions pipeline (install toolchain, cross-build, run under two CPU profiles) is in references/ci.md. The "run under multiple CPUs" pattern is the cheap way to catch CPU-feature regressions - float vs no-float, ARMv6-M vs ARMv7-M.

Anti-patterns

Anti-patternWhy it failsFix
Forgetting -semihosting-config enable=on,target=nativeTest prints nothing; QEMU never exitsAlways set both -semihosting-config enable=on and target=native (or auto)
-kernel pointed at a stripped binaryQEMU loads but symbol info gone for debugKeep -g debug info; strip only the release binary
Asserting on wall-clock timing under QEMUQEMU is not real-timeMove timing assertions to HIL; QEMU asserts only logical correctness
Using -M virt for a Cortex-M testvirt is Cortex-A - wrong instruction setMatch machine to CPU profile (mps2-* for M-profile, virt for A-profile)
Not pinning the QEMU versionNewer QEMU may emulate differently; CI flakesPin qemu-system-arm version in the runner image
Skipping -cpu and relying on board defaultBoard defaults shift between QEMU versionsAlways specify -cpu explicitly
Reading -monitor stdio and -nographic together without -serial mon:stdioSerial and monitor share stdio; output garblesUse -monitor none -serial stdio for clean output
Running QEMU in CI without a sane timeoutA hung test ELF runs foreverWrap in timeout 60 qemu-system-arm ...

Limitations

  • Not real-time. Per the QEMU docs (system overview), emulation timing is not deterministic. Use only for logical correctness; HIL for timing.
  • Peripheral coverage varies by board. Per the ARM target page, mps2-* boards expose a specific set of peripherals (UART, timer, GPIO via PL061); custom MCU peripherals are not modelled. If the test exercises an STM32 USART, you must mock the peripheral or use a real board.
  • No analog I/O. No ADC, no DAC, no electrical-level fault injection. That's strictly HIL territory.
  • max CPU diverges from production. The max CPU is per the cpu-features page "available for comprehensive feature testing"; passing on max does not mean passing on the production part.
  • QMP is structured but not stable across major QEMU versions. Pin the QEMU version in CI if you script QMP.
  • Semihosting has security implications on real hardware. In production firmware never link --specs=rdimon.specs; the semihosting bkpt is a debug-only path.

References

Cited inline. Foundational documents:

SKILL.md

tile.json