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 debug boots the firmware halted with Renode’s GDB server and attaches gdb-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 f407 cross build and the host test build.