Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill qemu-system-test-runner
View source

qemu-system-test-runner

Overview

QEMU system emulation (per qemu.org system docs (opens in new window)) 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 (opens in new window), 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 (opens in new window)). --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:

QEMU machines and CPUs for embedded testing

View source (opens in new window)

QEMU machines and CPUs for embedded testing

Per target-arm.html (opens in new window), the ARM machines relevant to embedded testing include mps2-an385, mps2-an386, mps2-an500, mps2-an505, mps2-an511, mps2-an521, mps3-an524, mps3-an536, mps3-an547, the Stellaris lm3s6965evb / lm3s811evb, Raspberry Pi raspi0 / raspi1ap / raspi2b / raspi3ap / raspi3b / raspi4b, and Xilinx xilinx-zynq-a9 / xlnx-zcu102. The virt board is designed for use in virtual machines and does not correspond to any real hardware.

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

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
Cortex-M7qemu-system-arm -M mps2-an500cortex-m7
Cortex-M33 (TrustZone-M)qemu-system-arm -M mps2-an505 / mps2-an521 / mps3-an524cortex-m33
Cortex-A15 / A57qemu-system-arm -M virt (32-bit) / qemu-system-aarch64 -M virt (64-bit)cortex-a15 / cortex-a57 / max
Stellaris LM3S6965 (older M3 reference)qemu-system-arm -M lm3s6965evbimplicit
Raspberry Pi 3B (Cortex-A53)qemu-system-aarch64 -M raspi3bimplicit (cortex-a53)

-machine help lists all machines; -cpu help lists CPU models for the target.

Per cpu-features.html (opens in new window), the special max CPU type is "available for comprehensive feature testing". Named CPU models generally do not work with KVM, but for emulation-only test runs (not KVM-accelerated) the named models work fine.

QEMU CI integration (GitHub Actions)

View source (opens in new window)

QEMU CI integration (GitHub Actions)

The SKILL spine keeps the smoke run; the full pipeline installs the toolchain, cross-builds, and runs the binary under two CPU profiles:

jobs:
  qemu-arm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install toolchain
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-arm-none-eabi qemu-system-arm
      - name: Cross-build test binary
        run: |
          arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O0 -g \
              --specs=rdimon.specs \
              -I src -I ext/Unity/src \
              src/*.c ext/Unity/src/unity.c \
              test/test_*.c test/*_Runner.c \
              -o build/test.elf -lrdimon
      - name: Run under QEMU (mps2-an386 / cortex-m4)
        run: |
          qemu-system-arm -M mps2-an386 -cpu cortex-m4 \
              -nographic \
              -semihosting-config enable=on,target=native \
              -kernel build/test.elf | tee build/qemu.log
          # exit code is the Unity failure count (semihosting _exit)
      - name: Also run under Cortex-M0 for ABI sanity
        run: |
          arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O0 -g \
              --specs=rdimon.specs \
              -I src -I ext/Unity/src \
              src/*.c ext/Unity/src/unity.c \
              test/test_*.c test/*_Runner.c \
              -o build/test-m0.elf -lrdimon
          qemu-system-arm -M mps2-an385 -cpu cortex-m0 \
              -nographic -semihosting-config enable=on,target=native \
              -kernel build/test-m0.elf

The "run under multiple CPUs" pattern is the cheap way to catch CPU-feature regressions - float vs no-float, ARMv6-M vs ARMv7-M.

QEMU invocation flags for embedded test runs

View source (opens in new window)

QEMU invocation flags for embedded test runs

Per invocation.html (opens in new window):

FlagEffect
-M [type=]name[,prop=value,...]Select emulated machine; -machine help lists all
-cpu modelSelect CPU model; -cpu help lists models for the target
-smp [cpus=]n[,cores=...,...]SMP topology - for multi-core test targets
-m [size=]megs[,slots=n,maxmem=size]Guest RAM; M / G suffixes
-kernel fileKernel image loaded directly into guest memory - for embedded tests, this is the test ELF
-bios fileCustom BIOS / ROM image
-append "<string>"Kernel command-line arguments (Linux targets)
-nographicNo GUI; serial to console
-serial stdioRedirect serial port to host stdin / stdout
-monitor stdio / -monitor tcp:host:portQEMU human monitor
-qmp tcp:host:port[,server,nowait]QMP machine protocol over TCP - JSON-RPC

Related skills

ceedling-build-runner

Author and run the Ceedling build system for C unit testing - the canonical build orchestration on top of Unity (assertions) + CMock (mocks) + CException (exceptions). Covers ceedling new project scaffolding, the project.yml schema (:project / :paths / :files / :defines / :flags / :tools / :test_runner / :cmock / :unity / :cexception / :gcov / :plugins), the task surface (ceedling test:all, ceedling test:{name}, ceedling test:pattern, ceedling test:path, ceedling release, ceedling clean / clobber, ceedling gcov:all, ceedling module:create, ceedling environment, ceedling dumpconfig), JUnit XML output via the report_tests_pretty_stdout / report_tests_junit_xml plugins, gcov plugin integration, host vs cross-build flow, and CI wiring. Use when a C project wants the standard ThrowTheSwitch trio bundled by one build command. For the Unity assertion API see unity-test-framework-c; for CMock semantics see cmock-reference.

cmock-reference

Pure-reference catalog of CMock and Ceedling mocking semantics for C. Defines what CMock generates from a C header (the full Expect / ExpectAndReturn / ExpectAnyArgs / ExpectWithArray / Ignore / IgnoreAndReturn / IgnoreArg_{param} / ReturnThruPtr_{param} / AddCallback / Stub / ExpectAndThrow naming family), the cmock.yml :plugins list (ignore, ignore_stateless, ignore_arg, expect_any_args, array, callback, cexception, return_thru_ptr) and what each enables, mock-suffix and mock-prefix conventions, how Unity teardown validates expectations, the resetTest mid-test verification, strict vs ignore argument-matching modes, and the trade-offs between mock / stub / spy / fake. Use as the CMock semantics reference when authoring Ceedling tests with mocks or when reading an unfamiliar mock-driven test suite.

embedded-coverage-strategy-reference

Pure-reference catalog of code-coverage strategy for embedded C/C++: the criteria hierarchy (statement / branch / decision / condition / MC/DC), the gcov toolchain (.gcno/.gcda, --coverage), the LLVM source-based toolchain (llvm-profdata / llvm-cov), host-build vs QEMU-build instrumentation, MISRA-C:2012 and DO-178C structural-coverage expectations by safety level (DAL A maps to MC/DC), and report-format choices. Use when choosing what structural-coverage level to require and wiring gcov / llvm-cov into the build; physical .gcda retrieval from hardware is in hardware-in-loop-reference, QEMU machine flags in qemu-system-test-runner, and to author the embedded tests themselves use googletest-embedded-arm or unity-test-framework-c.

googletest-embedded-arm

Author and run GoogleTest 1.17+ for embedded C++ on ARM targets - TEST() / TEST_F() / TEST_P() / TYPED_TEST(), EXPECT_* vs ASSERT_* assertions, fixtures with SetUp() / TearDown(), value-parameterised tests, GoogleMock when paired, cross-compile with arm-none-eabi-g++, run on host or under QEMU via the qemu-system-test-runner skill, --gtest_filter / --gtest_output=xml:results.xml / --gtest_shuffle / --gtest_repeat command-line flags, and XML / JSON output parsing for CI. Use when the unit-under-test is C++ (modern C++17+) and the team wants the de-facto C++ test framework instead of the C-only Unity. For C use unity-test-framework-c; for pure mocks use cmock-reference.

hardware-in-loop-reference

Pure-reference catalog of hardware-in-the-loop (HIL) testing for embedded systems. Defines the HIL pattern (ECU-under-test + real-time plant simulator + I/O cards emulating sensors / actuators / buses), the V-cycle progression (MIL → SIL → PIL → HIL), the canonical vendor stack (NI VeriStand + PXI / CompactRIO, dSPACE SCALEXIO / MicroAutoBox, Vector CANoe + VT System, Speedgoat real-time targets), bus emulation per protocol (CAN / CAN FD / LIN / FlexRay / Automotive Ethernet / SOME-IP), fault-injection patterns (short-to-ground, open-circuit, signal corruption), DO-178C / ISO 26262 / IEC 61508 alignment, and the test-evidence chain HIL produces. Use as the HIL terminology + vendor + standard reference when scoping an embedded test rig or interpreting an automotive / aerospace / industrial HIL test report.

unity-test-framework-c

Author and run ThrowTheSwitch Unity (the C unit-testing library) for bare-metal and RTOS C code. Distinct from the Unity game-engine Test Framework at docs.unity3d.com: this is the ThrowTheSwitch C testing library at throwtheswitch.org/unity, a single C file plus headers that runs on 8-bit MCUs through 64-bit hosts. Anchored on the Unity assertion API and configuration macros regardless of execution environment: the TEST_ASSERT_EQUAL_* / _FLOAT / _DOUBLE / _STRING / _MEMORY / _BITS assertion families, setUp/tearDown/RUN_TEST/UNITY_BEGIN/UNITY_END semantics and the exit-code contract, the generate_test_runner.rb generator, build-time config defines (UNITY_INCLUDE_DOUBLE, UNITY_OUTPUT_CHAR, UNITY_EXCLUDE_SETJMP), and CI integration via Ceedling JUnit XML; applies to host builds, cross-builds, and QEMU-run targets alike. For QEMU machine flags, semihosting, and exit-code capture, see qemu-system-test-runner. Use when the unit-under-test is pure C and the target ranges from 8-bit AVR to Cortex-M0 to Linux ARM.