Testing STM32 code
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 = 0It’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.