• Test driven embedded rust development [Tutorial]

    12/22/2023 at 19:06 0 comments

    Introduction

    In this tutorial we will set up an embedded development environment in rust that focuses on test driven development (TDD for short). TDD is especially useful for embedded systems, as testing the code on the target hardware is significantly more difficult than testing software written for the host device. 

    TDD overview

    In essence Test Driven Development contains 4 major steps:

    1.  Write a failing test: With test driven development we don't write tests to test the code, instead we write code to meet the tests. Due to the inversion of order of operation it's inevitable that the test would fail.
    2. Write the minimum code to pass the test: The goal is not to create a complex solution, but to make the test succeed.
    3. Refactor the code: After the test has passed, refactor the code to improve it's structure, readability, and maintainability.
    4. Repeat the cycle: Over time the code would accumulate more unit tests as more features are added. We can gain confidence that adding a new feature won't break existing features, if TDD is maintained.

    In embedded development a big risk is the interaction with the hardware. TDD allows developers to write tests for these interactions early in the development process, helping to catch issues related to hardware communication.

    The hardware

    In this tutorial we will write a simple application for an STM32 NUCLEO development board. The microcontroller in this board is the STM32-F446RE.

    Setting up the project

    To get started we are going to generate the project skeleton from a template. Cargo can do this for us, just need to install the generator with:

    cargo install cargo-generate

    Now the project can be generated from the stm32-template with this command:

    cargo generate --git https://github.com/burrbull/stm32-template/

     The generator will take you through a couple questions. using the default value for most except for the device itself. For that I replaced the default with stm32f446re.

    Next, add the target architecture to rustup if you haven't done that already. In this case it's the following:

    rustup target add thumbv7em-none-eabihf

     If everything went alright, you should be able to build the project now:

    cargo build --release

    Setting up testing

    In this section we will get to a point where we can execute unit tests on the host machine and build for the target. To be able to do both, we need to chisel a bit on the template. 

    Create a sample test

    Let's create a rust file which will contain our application logic and the unit tests. For now let's start with an empty unit test.

    //app.rs
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_placeholder() {}
    }

    Add this new module to main.rs by adding this line:

    mod app;

     Usually you would be able to run the tests now, but if you try to run it, it would fail for multiple reasons:

    • The default target now is thumbv7em-none-eabihf instead of the host machine
    • Unit testing will need the std crate, which has been disabled. 
    • The default panic handler clashes with the new one defined by default.

    Fixing the issues

    To fix the issue with the default target, my way of going around it is to unset the build target in the .cargo/config.toml file by commenting out the following line in the [build] section:

    # target = "thumbv7em-none-eabihf"

     The result of this is that if we want to build for the target, now we need to specify the target as well:

    cargo build --release --target=thumbv7em-none-eabihf

     To fix the other issues, in the main.rs we need to disable some configuration flags depending on whether we are building for tests. The flags for this in rust are:

    #[cfg(not(test))]
    #![cfg_attr(not(test), ...)]

     With these switches we need to disable the no_std, no_main, and entry. Also the use of panic_halt needs to be disabled when running tests. With these changes our main.rs looks like the following:

    #![deny(unsafe_code)]
    #![cfg_attr(not(test), no_main)]
    ...
    Read more »