QEMU Firmware Testing: Emulate Embedded Targets Without Physical Hardware
Waiting for physical hardware to run firmware tests is a bottleneck that slows embedded development to a crawl. Hardware is expensive, limited in quantity, and can't run in CI. QEMU emulation changes that — you can run and test ARM Cortex-M firmware on any Linux machine, including cloud CI runners.
This guide covers practical QEMU-based firmware testing: how to emulate your target, run unit tests, and integrate everything into a CI pipeline.
Why Emulate for Testing?
Before hardware arrives, you need to validate logic. Even after hardware arrives, emulation is better for:
- Unit tests — run hundreds of tests in seconds without flashing
- CI/CD — every PR runs firmware tests automatically
- Debugging — QEMU's GDB integration is better than many hardware debuggers
- Edge cases — inject hardware faults that are hard to reproduce physically
- Regression testing — never find out a working feature broke in production
QEMU for ARM Cortex-M
QEMU supports a growing list of ARM Cortex-M targets. Common ones:
# List supported ARM machine types
qemu-system-arm -machine help | grep -E "mps2|stm32|lm3s|virt"
# Common targets:
# mps2-an385 — Cortex-M3 (ARM MPS2+ board)
# mps2-an500 — Cortex-M7
# stm32vldiscovery — STM32F100
# netduinoplus2 — STM32F405Running Firmware in QEMU
Basic Execution
# Build your firmware (example: ARM GCC for Cortex-M3)
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \
-T linker.ld \
-o firmware.elf \
src/*.c
# Run in QEMU (mps2-an385 emulates Cortex-M3)
qemu-system-arm \
-machine mps2-an385 \
-cpu cortex-m3 \
-kernel firmware.elf \
-nographic \
-semihosting-config enable=on,target=nativeSemihosting for I/O
Semihosting lets your firmware use the host's stdin/stdout during testing — no UART hardware needed:
// In your firmware: use semihosting for test output
#include <stdio.h> // Links to semihosting when -specs=rdimon.specs
int main(void) {
printf("Firmware booted\n");
// Run your tests
int result = run_all_tests();
printf("Tests %s. Result: %d\n", result == 0 ? "PASSED" : "FAILED", result);
// Exit with test result code (semihosting)
_exit(result);
}Build with semihosting:
arm-none-eabi-gcc \
-mcpu=cortex-m3 -mthumb \
-specs=rdimon.specs \
-lrdimon \
-T linker.ld \
-o test_firmware.elf \
src/*.c tests/*.cUnity Test Framework on QEMU
Unity is the standard unit test framework for embedded C. Combine it with QEMU:
// tests/test_sensor_driver.c
#include "unity.h"
#include "sensor_driver.h"
void setUp(void) {
sensor_init();
}
void tearDown(void) {
sensor_deinit();
}
void test_sensor_read_returns_valid_range(void) {
int16_t reading = sensor_read_temperature();
TEST_ASSERT_INT_WITHIN(50, 0, reading); // -50 to +50 °C
}
void test_sensor_read_increments_counter(void) {
uint32_t before = sensor_get_read_count();
sensor_read_temperature();
uint32_t after = sensor_get_read_count();
TEST_ASSERT_EQUAL(before + 1, after);
}
void test_sensor_error_flag_clears_on_read(void) {
sensor_inject_error(SENSOR_ERR_TIMEOUT); // Test hook
TEST_ASSERT_TRUE(sensor_has_error());
sensor_read_temperature(); // Should clear error
TEST_ASSERT_FALSE(sensor_has_error());
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_sensor_read_returns_valid_range);
RUN_TEST(test_sensor_read_increments_counter);
RUN_TEST(test_sensor_error_flag_clears_on_read);
return UNITY_END();
}Run on QEMU:
# Build test binary
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \
-specs=rdimon.specs -lrdimon \
-T tests/test_linker.ld \
-I src/ -I unity/src/ \
unity/src/unity.c \
src/sensor_driver.c \
tests/test_sensor_driver.c \
-o test_sensor.elf
# Run on QEMU, capture output
timeout 10 qemu-system-arm \
-machine mps2-an385 \
-cpu cortex-m3 \
-kernel test_sensor.elf \
-nographic \
-semihosting-config enable=on,target=native \
2>&1
# Exit code from QEMU reflects firmware's _exit() codeRenode: A Better Alternative for Complex Targets
Renode from Antmicro is a more powerful emulator for complex embedded systems, especially when you need peripheral simulation:
# Install Renode
wget https://github.com/renode/renode/releases/latest/download/renode-*.linux-portable.tar.gz
tar -xf renode-*.tar.gz
# Create a Renode script for your board
cat > tests/test_platform.resc << 'EOF'
using sysbus
mach create "test-board"
machine LoadPlatformDescription @platforms/boards/stm32f4_discovery.repl
sysbus LoadELF $ORIGIN/test_firmware.elf
# Set up serial output capture
showAnalyzer sysbus.uart2
# Run for up to 10 seconds
machine StartGdbServer 3333
emulation RunFor "00:00:10"
EOF
# Run tests
renode --disable-gui tests/test_platform.rescRenode Peripheral Mocking
Renode lets you script peripheral behavior in Python:
# tests/mock_sensor.py — Renode peripheral mock
from Antmicro.Renode.Peripherals.SPI import ISPIPeripheral
class MockTemperatureSensor(ISPIPeripheral):
def __init__(self):
self.temperature = 25 # degrees C
self.call_count = 0
def Transmit(self, data):
self.call_count += 1
if data == 0x01: # Read temperature command
# Return temperature as 2-byte big-endian
return [(self.temperature >> 8) & 0xFF, self.temperature & 0xFF]
return [0xFF] # NACK for unknown commandsGDB-Based Test Debugging
QEMU's GDB server lets you debug firmware tests exactly like you would hardware:
# Start QEMU with GDB server
qemu-system-arm \
-machine mps2-an385 \
-cpu cortex-m3 \
-kernel test_firmware.elf \
-nographic \
-semihosting-config enable=on,target=native \
-S -gdb tcp::3333 & # -S = start paused
# Connect GDB
arm-none-eabi-gdb test_firmware.elf << 'EOF'
target remote :3333
load
break main
continue
# Set breakpoint on test failure
break unity_fail
continue
quit
EOFCI/CD Integration
# .github/workflows/firmware-tests.yml
name: Firmware Tests
on:
push:
paths: ['src/**', 'tests/**', 'CMakeLists.txt']
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install ARM toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
gcc-arm-none-eabi \
binutils-arm-none-eabi \
qemu-system-arm
- name: Build test firmware
run: |
mkdir build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-none-eabi.cmake \
-DBUILD_TESTING=ON \
-DCMAKE_BUILD_TYPE=Debug \
..
make -j$(nproc)
- name: Run tests on QEMU
run: |
EXIT_CODE=0
for test_elf in build/tests/*.elf; do
echo "Running: $test_elf"
timeout 30 qemu-system-arm \
-machine mps2-an385 \
-cpu cortex-m3 \
-kernel "$test_elf" \
-nographic \
-semihosting-config enable=on,target=native \
2>&1 | tee /tmp/test_output.txt
TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ] || grep -q "FAIL" /tmp/test_output.txt; then
echo "FAILED: $test_elf"
EXIT_CODE=1
else
echo "PASSED: $test_elf"
fi
done
exit $EXIT_CODECMake Configuration
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(firmware C)
set(CMAKE_C_STANDARD 11)
# Main firmware
add_executable(firmware
src/main.c
src/sensor_driver.c
src/ring_buffer.c
)
target_compile_options(firmware PRIVATE
-mcpu=cortex-m3
-mthumb
-Wall -Wextra
-Os
)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
# tests/CMakeLists.txt
add_executable(test_sensor
test_sensor_driver.c
${CMAKE_SOURCE_DIR}/src/sensor_driver.c
${CMAKE_SOURCE_DIR}/unity/src/unity.c
)
target_include_directories(test_sensor PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/unity/src
)
target_compile_options(test_sensor PRIVATE
-mcpu=cortex-m3 -mthumb
-specs=rdimon.specs
)
target_link_options(test_sensor PRIVATE
-T${CMAKE_CURRENT_SOURCE_DIR}/test_linker.ld
-specs=rdimon.specs
-lrdimon
)
add_test(
NAME sensor_driver_tests
COMMAND qemu-system-arm
-machine mps2-an385
-cpu cortex-m3
-kernel test_sensor
-nographic
-semihosting-config enable=on,target=native
)What to Test on QEMU vs. Real Hardware
QEMU is great for testing logic. It's not a substitute for hardware in all cases:
| Test Type | QEMU | Real Hardware |
|---|---|---|
| Business logic in firmware | ✓ Fast and reliable | ✓ |
| State machine transitions | ✓ | ✓ |
| Protocol parsing | ✓ | ✓ |
| Memory layout / alignment | ✓ | ✓ |
| Interrupt timing | Approximate | ✓ Required |
| DMA transfers | Limited | ✓ Required |
| RF/radio behavior | ✗ | ✓ Required |
| Power consumption | ✗ | ✓ Required |
| Analog peripherals (ADC) | Limited | ✓ Required |
The right strategy: use QEMU for the 80% of logic tests that don't require hardware, and reserve physical hardware tests for the 20% that do.
Summary
QEMU enables a proper test-driven workflow for firmware:
- Write Unity tests alongside your C code
- Build test binaries with the ARM cross-compiler and semihosting
- Run on QEMU — fast, no hardware needed, works in CI
- Use GDB integration for debugging test failures
- Automate with GitHub Actions on every push
The result: instant feedback on firmware regressions, independent of hardware availability.