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, %functionmemaddr:
1, 5:10
// the memory address is in PB0:r2, 0b11111100
movw
6 bits into r1
// top r1, r2, r0, lsr 3
and
r0
// assemble full address into r0, r0, 0b11
and r0, r1, r0
orr
// check if RAM or ROMr1, 1
movw r1, r1, r0, lsr 7
eors
ite eqr2, =RAM_BASE
ldreq r2, =ROM_BASE
ldrne
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.