STM32 Playground
FreeRTOS firmware for the STM32F407 Discovery, developed and tested entirely in the Renode emulator - no board required.
Highlights
- Bare-register GPIO and UART drivers on CMSIS headers only, no vendor HAL
- Two test tiers: host unit tests with ASan/UBSan, and Robot Framework tests against the emulated board
- Emulator tests assert real behaviour, like the green LED blinking at 1 Hz within ±5%
- Toolchain, emulator, and linters in one digest-pinned Docker image; the same ELF flashes to real hardware
Overview #
STM32 Playground is FreeRTOS firmware in C for the STM32F407 Discovery board, set up so that it can be written, run, debugged and tested without the board. The firmware boots in Renode, an open-source emulator that models the MCU and its peripherals well enough to run the same ELF you would flash with openocd.
The goal was a small, honest embedded project where the feedback loop looks like ordinary software development: make test on any machine with Docker, and CI that actually exercises the firmware rather than only compiling it. The arm-none-eabi toolchain, Renode, and the lint tools all live in one image pinned by digest, so the host only needs docker and make.
$ make run
stm32-playground: FreeRTOS V11.3.1, core clock 16000000 Hz
type 'help' for commands
> led blue on
blue led on
Firmware #
The target is the STM32F407VG, a Cortex-M4F built with the hard-float ABI and running from the 16 MHz internal oscillator. On top of the startup code and linker script, the firmware is split into layers:
flowchart TD Apps["<b>apps/</b><br/>blinky: green LED at 1 Hz<br/>shell: help, led, button, uptime, tasks"] Lib["<b>lib/</b> (hardware-free)<br/>ring buffer, CLI parser"] BSP["<b>firmware/bsp</b><br/>LEDs PD12-15, button PA0, console"] Drivers["<b>firmware/drivers</b><br/>register-level GPIO + USART2<br/>IRQ-driven RX, mutex-guarded TX"] RTOS["<b>FreeRTOS</b> V11.3.1<br/>ARM_CM4F port, heap_4"] HW["STM32F407 Discovery<br/>or Renode"] Apps --> Lib Apps --> BSP Apps --> RTOS BSP --> Drivers Drivers --> HW RTOS --> HW
- No HAL. The GPIO and UART drivers write registers directly using only the CMSIS device headers. USART2 receives through an interrupt into a ring buffer, and transmission is guarded by a FreeRTOS mutex so tasks can print without interleaving.
- Apps as tasks. Each application is a FreeRTOS task registered in a single table in
main.c. The shell is one of them, and it can list the running tasks. - Fail loudly. The assert, malloc-failed, stack-overflow and HardFault hooks all print a panic message over the UART and halt, instead of silently resetting.
- Pinned dependencies. FreeRTOS, CMSIS, the STM32F4 device headers and Unity are git submodules pinned to release tags.
Testing in two tiers #
Host tests. Everything in lib/ is hardware-free, so the ring buffer and CLI parser are compiled natively and tested with Unity under AddressSanitizer and UndefinedBehaviorSanitizer. These run in milliseconds and catch the off-by-one bugs before they get anywhere near an interrupt handler.
Emulator tests. Robot Framework drives Renode against the real firmware ELF. The tests boot to the shell prompt, type commands over the emulated UART, press the emulated user button, and check the GPIO state of the LEDs:
Should Blink Green LED At 1 Hz
Create LED Tester sysbus.gpioPortD.UserLED
Start Emulation
Assert LED Is Blinking testDuration=4 onDuration=0.5 offDuration=0.5 tolerance=0.05
Should Control LEDs From Shell
Create LED Tester sysbus.gpioPortD.RedLED
Boot To Prompt
Write Line To Uart led red on
Wait For Line On Uart red led on
Assert LED State true
Getting there took one fix to the emulated board itself. Renode’s stock F407 Discovery model assumes a 72 MHz SysTick and only wires up one of the four user LEDs. A small .repl overlay sets SysTick to 16 MHz to match the internal oscillator and connects the missing LEDs. Without it, the emulated FreeRTOS tick runs at the wrong rate and every timing assertion is off.
Tooling #
make debugboots the firmware halted with Renode’s GDB server and attachesgdb-multiarch.- A VS Code dev container uses the same image, with clangd reading
compile_commands.json. - pre-commit runs clang-format, cppcheck, gitleaks, hadolint and shellcheck.
- CMake presets cover both the
f407cross build and thehosttest build.