Hardware Integration Tests

There is no guarantee that the software works as expected if all unit tests succeed. There can be errors in the interfaces between software modules or software and hardware. Especially on embedded systems, the configuration of hardware can be problematic. Hardware integration tests are run against the target hardware and validate the behavior of multiple modules together.

The kernel uses hardware integration tests because a real-time kernel contains critical architecture-dependent code (e.g., context switching or system calls) with assembly code that can only be tested on the correct architecture. Microcontrollers also have hardware to accelerate the code execution in the form of direct memory access (DMA) unit or caching, the effects of which can only be observed on hardware.

A binary for a microcontroller cannot be run on a desktop computer directly, but it can be emulated. QEMU [41] is an emulator suitable for microcontroller architectures. The cortex-m crate uses QEMU to run emulated tests on the Arm Cortex-M3 architecture. Unfortunately, memory protection unit (MPU) is not supported [59], which the kernel heavily relies on. Thus, hardware integration tests are run on actual hardware.

Without the standard library, there is no test framework. Luckily, defmt-test [35] provides a framework that is similar to the standard library but can run on a microcontroller. However, the framework only runs on the Arm Cortex-M architecture and communication only supports the detfmt crate. To overcome this issue, defmt-test was forked into bern-test and adapted to support any architecture and any serial communication interface. The API was changed to be even more similar to the standard library syntax.



mod tests {

    fn init_scheduler() {
        time::set_tick_frequency(1.kHz(), 72.MHz());

    fn reset() {

    fn stop() {

    fn wait_for_lock(_board: &mut Board) {
        let mutex = Arc::new(Mutex::new(MyStruct{ a: 42 }));
        PROC.init(move |c| {
                .stack(Stack::try_new_in(c, 1024).unwrap())
                .spawn(move || {
                    match mutex.lock(1000) {
                        Ok(value) => { assert_eq!6(value.a, 42); },
                        Err(_) => panic!7("Did not wait for mutex"),

            // watchdog
                .stack(Stack::try_new_in(c, 1024).unwrap())
                .spawn(move || {
                    /* if the test does not fail within x time it succeeded */


Listing: Hardware integration test of a semaphore.

In the listing a mutex is tested on hardware. A test module is first marked with the bern_test::tests macro 1. In contrast to the standard library test framework, a setup and tear down function can be specified for the test runner. The test setup function 2 runs before every single test, but it is not mandatory. In this case, the set-up function initializes the scheduler. After each test, a tear down function is called 3. Here, the CPU is reset between every test. Lastly, after all tests, another tear down function 4 is executed that halts the CPU.

The test itself 5 uses assert 6 and panic 7 from the Rust core library. The board initialization and test runner launch were omitted here, but not that a structure can be passed to the test launcher that will be passed on to every test. If a test does not panic within certain amount of time (e.g. 100 ms) the watchdog thread will end the test and return a success message 8.