There’s now code worth testing in my project: address line decoding. Let’s take another sidebar journey into automated testing of embedded code, and see where it leads.

I’ve been claiming loudly for a while now that there’s no good reason to begin any new projects in C when there’s Rust available instead, and that there’s precious little reason for new projects in C++, barring libraries you just cannot expose to Rust and cannot live without. As a result, this project is done in Rust.

The basic problem facing me is that I’m extremely inexperienced with ARM assembly and the Thumb-2 instruction set. I’d like to be able to test my ideas and expectations before flashing code to my device, which means I want to execute my code on my development host.

I originally considered firing up QEMU. The current master branch has support for one STM32F4 processor, so it’s not an outrageous stretch to build a black pill emulator, but the support is threadbare and QEMU isn’t set up for being a unit test executor.

My solution uses the Unicorn engine, which is backed by QEMU, to set up the memory map of an STM32F401. I then load my ELF intermediate output, which has a symbol table, and copy the loadable segments into the emulator’s memory.

Tests are defined as a DSL that specifies the input and output register states, and selects a symbol to call.

The GitHub workflow for this project builds a binary, automatically tags a new release if the version number in Cargo.toml has changed, and publishes the binary to the release.

I can now take this local function for breaking my 8 bits of address space out of port B and into an address in either RAM or ROM:

        .type           memaddr, %function
memaddr:
        // the memory address is in PB0:1, 5:10
        movw            r2, 0b11111100

        // top 6 bits into r1
        and             r1, r2, r0, lsr 3

        // assemble full address into r0
        and             r0, r0, 0b11
        orr             r0, r1, r0

        // check if RAM or ROM
        movw            r1, 1
        eors            r1, r1, r0, lsr 7

        ite             eq
        ldreq           r2, =RAM_BASE
        ldrne           r2, =ROM_BASE

        add             r0, r0, r2

        bx              lr

        .size           memaddr, . - memaddr

… and confirm that my use of unfamiliar instructions like ite work the way I expect with a test script:

test: Memory Address decode

setup:
    r0 = 0b10111011101

call: memaddr

check:
    r0 = 0x200000b9
    r1 = 0

It’s easy to also wrap a download of the released binary from GitHub into an action in the RAM/ROM project and have automatic unit testing take place.

Constantly practicing repeatable and automated processes for building, testing, and deploying makes it easier and more habitual to do for everything. I may have lost two or three days of STM32F4 assembly coding time in writing my test executor and its workflow actions, but I’ve gained a lot more confidence that the code will work as expected once it hits the μC, and I expect I’ll easily make back that time in fewer mistakes on device, where it’s harder to inspect.