Three weeks before their first hardware delivery, a team in our early-access pilot found that every SPI write in its nRF9160 firmware addressed the wrong peripheral instance. The code selected SPIM0's base address, while the board schematic connected the sensor to SPIM1. The mistake affected every driver function. Without pre-silicon checks, it would have remained hidden until the boards arrived, forcing three weeks of firmware work to be repeated through a logic analyser session.
The Price of Late Bugs
Bring-up defects have an uneven cost. Finding and correcting a register offset mistake in development simulation takes 20 minutes. Finding it on hardware takes 2 to 4 hours, requiring the board, debugger connection, logic analyser or oscilloscope, and someone able to read the signal. A subtle fault, such as a wrong bitfield position that produces non-zero output, can take longer still.
The useful question is which bugs can be found before silicon, not whether pre-silicon testing matters. QEMU and Renode cover different areas, and neither stands in for hardware tests. This article outlines their coverage and their use with Embedd's generated test stubs.
QEMU on Cortex-M
QEMU's qemu-system-arm provides machine models for several Cortex-M targets, notably the STM32F405 and generic Cortex-M3/M4/M7 bare-metal machines. Its STM32F4 model includes UART0, connected to serial output, a basic timer, and the processor core with the NVIC. It does not represent SPI, I2C, ADC, or most peripheral registers beyond UART.
For Cortex-M, QEMU mainly runs application code and checks firmware boot, interrupt-vector wiring, and non-peripheral logic such as state machines, packet parsing, and calculations. Its peripheral coverage is too limited for register-level driver tests. A write to an SPI register address can complete silently because no peripheral model checks whether the bit pattern is valid.
QEMU is therefore most useful for Embedd drivers at the interrupt-wiring layer. You can trigger a software interrupt and check that the ISR runs, confirm the vector-table offset, and test that NVIC priority settings do not crash the RTOS scheduler. These can be genuine defects, even though they are not register-encoding errors.
Renode: More Peripheral Models
Renode, from Antmicro, offers a more capable embedded testing emulator. Its library represents more devices, and its design lets you write C# peripheral models that enforce valid register access. Community models cover STM32 UART, I2C, and SPI devices and can check configuration order. For example, enabling SPI before setting the clock prescaler causes the model to log an error.
Renode's script language, using .resc files, defines scenarios that provide peripheral inputs and inspect outputs:
:name: SPI loopback test
:description: Verifies SPIM1 register configuration sequence
using sysbus
mach create
machine LoadPlatformDescription @platforms/cpus/nrf9160.repl
machine StartGdbServer 3333
sysbus.spim1 Log "Monitor enabled"
start
# Wait for driver init sequence
sleep 0.1
# Verify ENABLE register bit set
sysbus.spim1 VerifyRegister ENABLE 0x00000007
# Verify SCK frequency configuration
sysbus.spim1 VerifyRegister FREQUENCY 0x02000000 # 125 kHz
The VerifyRegister calls confirm both the expected values and their order. For Renode-supported targets, Embedd's generated driver stubs include a corresponding .resc script for the initialization sequence. A wrong peripheral base address, or a write to SPIM0 instead of SPIM1, makes the script fail at once and identifies the mismatched register.
Unity and CMock Register Tests
Without a complete emulator, a practical register test uses a mock peripheral compiled for the x86 host. The test reserves a byte array at a fixed address, directs the driver's peripheral pointer to it, runs the driver functions, and examines the array for the right bytes in the right order.
Embedd's generated stubs use Unity and CMock to mock the peripheral:
/* embx_nrf9160_spim1_test.c -- generated by Embedd */
#include "unity.h"
#include "embx_nrf9160_spim1.h"
/* Mock peripheral memory at the SPIM1 base address */
static uint8_t mock_spim1_mem[EMBX_NRF9160_SPIM1_SIZE];
void setUp(void) {
memset(mock_spim1_mem, 0, sizeof(mock_spim1_mem));
/* Redirect peripheral pointer to mock memory */
embx_nrf9160_spim1_set_base((uintptr_t)mock_spim1_mem);
}
void test_spim1_init_sets_enable_register(void) {
embx_nrf9160_spim1_cfg_t cfg = {
.frequency = EMBX_NRF9160_SPIM1_FREQ_125K,
.mode = EMBX_NRF9160_SPIM1_MODE_0,
};
embx_nrf9160_spim1_init(&cfg);
/* ENABLE register at offset 0x500 must be 7 (enable SPIM) */
uint32_t enable_val;
memcpy(&enable_val, mock_spim1_mem + 0x500, 4);
TEST_ASSERT_EQUAL_HEX32(0x00000007, enable_val);
}
void test_spim1_init_sets_frequency_register(void) {
embx_nrf9160_spim1_cfg_t cfg = {
.frequency = EMBX_NRF9160_SPIM1_FREQ_125K,
.mode = EMBX_NRF9160_SPIM1_MODE_0,
};
embx_nrf9160_spim1_init(&cfg);
/* FREQUENCY register at offset 0x524 -- 125 kHz = 0x02000000 */
uint32_t freq_val;
memcpy(&freq_val, mock_spim1_mem + 0x524, 4);
TEST_ASSERT_EQUAL_HEX32(0x02000000, freq_val);
}
The embx_nrf9160_spim1_set_base function is generated with the driver for this pattern. It swaps the hardcoded peripheral base address for the test mock's pointer. It is the driver's only test-mode-aware function; the remaining functions use the indirection transparently.
Limits of These Tests
This qualification matters. Register-level unit tests and QEMU/Renode simulation check the driver's register encoding, but not timing-dependent behavior. An SPI clock too fast for a slave, a DMA burst conflicting with a higher-priority bus master, or a GPIO output briefly glitching during initialization will not appear in these tests. Such cases need hardware in the loop.
The generated tests also compare the configuration sequence with the datasheet specification, not with silicon behavior. Datasheet errata are absent. A register needing an errata workaround therefore requires a hardware-verified test, since the simulation model comes from the datasheet rather than the silicon.
A Testing Plan
Our pilot used three test layers. First, Unity mock tests run natively on x86 in CI with every push. They take under 10 seconds for a full driver set and catch address offsets, incorrect bitfield values, and initialization-order errors. Second, Renode runs nightly or on release branches, adding the peripheral interaction model missing from the mocks. Third, hardware in the loop runs on physical boards before release to cover timing, electrical behavior, and errata.
The first two layers can be automated completely. The hardware layer needs setup time, but a JTAG-connected test harness can automate part of it. Embedd's generated stubs provide a complete first layer on day zero and a starting point for the second when Renode supports the target.