Board firmware · drdro-firmware-f4

The STM32 board firmware

Firmware for the drDRO controller board (STM32F411CEU6): FreeRTOS, a custom RS-485 line protocol, trapezoidal servo motion, and a dual-bank IAP bootloader for safe field updates. Built with PlatformIO. This page covers setting it up, building, flashing, configuring and developing it.

Firmware repo ↗ STM32F411CEU6 · Cortex-M4F · 100 MHz 512K flash / 128K RAM

Overview

The firmware is the real-time half of drDRO — it reads the encoders, drives the axis, keeps the persistent settings, and speaks the protocol the host app talks to.

  • Reads 4 quadrature scales (TIM1–TIM4 in encoder mode).
  • Drives a step/dir servo with trapezoidal accel/decel ramps — jog, indexing and scale-synchronised motion — from a TIM9 interrupt relocated to RAM, so it keeps stepping even during a flash write.
  • Speaks the RS-485 line protocol on USART1 @ 115200 8N1 (auto-direction).
  • Stores rarely-changed settings (scale ratios, servo config, boot mode, active bank) in flash, power-fail-safe.
  • Field-updatable over RS-485 via a two-bank A/B bootloader — keep two versions, switch or roll back.
🔗

The wire protocol and variable registry are documented on the Reference page. This page is about the firmware project itself.


Flash map & project layout

The 512 KB flash is partitioned for a dual-bank bootloader. The app is linked once for the Exec region; the bootloader copies the active bank into Exec and jumps to it (copy-on-activate). Switching banks or rolling back just changes the active-bank byte in settings.

RegionSector(s)AddressSizeRole
Bootloader00x0800000016 KNever erased by an update
Settings A / B1 / 20x0800400016 K eachPing-pong, power-fail safe
(reserved)3–40x0800C00080 KFuture
Exec50x08020000128 KThe app runs here
Bank 0 / 16 / 70x08040000 / 0x08060000128 K eachStored app versions

Repository layout

drdro-firmware-f4/
app/          # the application (linked at Exec 0x08020000)
  src/        # app + frozen CubeMX glue (hand-maintained; NOT regenerated)
  include/    # app + config headers (FreeRTOSConfig.h, hal_conf.h)
  lib/FreeRTOS# vendored middleware
  test/       # native unit tests + HAL/RTOS mocks
bootloader/   # IAP bootloader (sector 0, 0x08000000)
shared/       # Bootloader.h, Settings.h, BlinkCode.h (app<->bl contract)
tools/        # dro_update.py, make_factory.py, build-release.sh
docs/         # protocol_design, dualbank_design, HARDWARE.md, trackers

Toolchain setup

The project builds with PlatformIO (platform ststm32@~19.4.0, framework = stm32cube, board genericSTM32F411CE). The ARM toolchain and framework are fetched automatically on the first build.

Install PlatformIO Core (the pio CLI), then clone the repo. The VS Code PlatformIO extension works too.

bash
# PlatformIO Core (official installer, or: pipx install platformio)
python3 -c "$(curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py)"

git clone https://github.com/bartei/drdro-firmware-f4.git
cd drdro-firmware-f4

A ready-to-use VS Code dev container ships in .devcontainer/ with PlatformIO, the STM32 toolchain, serial terminals and the GitHub CLI. Open the folder and Reopen in Container; the first build warms the cache. ST-Link USB passthrough is Linux-host only (see the container README).

On hosts without pio on PATH (e.g. NixOS), wrap the commands:

bash
nix-shell -p platformio --run 'pio run -d app'
🔌

Flashing hardware: an ST-Link (v2/v3) is used to program the board over SWD. On Linux, install PlatformIO's udev rules so the probe is accessible without root:

bash
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/master/platformio/assets/system/99-platformio-udev.rules \
  | sudo tee /etc/udev/rules.d/99-platformio-udev.rules
sudo udevadm control --reload && sudo udevadm trigger

Build & test

The app and bootloader are two sibling PlatformIO projects; pass -d <dir> to target one.

bash
pio run  -d app                  # build the application  -> firmware.{elf,bin,hex}
pio run  -d bootloader           # build the bootloader
pio test -d app -e native        # host-side protocol/settings unit tests (no hardware)
pio run  -d app -t clean         # linker-script changes need a clean relink

Artifacts: app/.pio/build/drdro_f411ce/firmware.{elf,bin,hex} and bootloader/.pio/build/bootloader/firmware.{elf,bin,hex}. The native tests link only Protocol.c and mock the HAL/RTOS — they run anywhere, no board required.


Flashing the board

First-time flash — combined factory image

A brand-new board needs both the bootloader and an app in Exec. Merge them into one image and write it with ST-Link:

bash
pio run -d app && pio run -d bootloader          # build both first
python tools/make_factory.py factory.hex \
    bootloader/.pio/build/bootloader/firmware.hex \
    app/.pio/build/drdro_f411ce/firmware.hex
st-flash --format ihex write factory.hex

Iterating — flash a single project over ST-Link

bash
pio run -d app        -t upload   # flash the app to Exec (0x08020000)
pio run -d bootloader -t upload   # flash the bootloader (sector 0)

Field update — over RS-485, no probe

For bring-up/debug you can drive the whole dual-bank update cycle from a host PC over the serial link (self-contained YMODEM sender, baud 115200):

bash
# auto-pick the inactive bank, flash, select, boot, verify:
python tools/dro_update.py /dev/ttyACM0 app/.pio/build/drdro_f411ce/firmware.bin
python tools/dro_update.py /dev/ttyACM0 firmware.bin --bank 1        # force a bank
python tools/dro_update.py /dev/ttyACM0 firmware.bin --no-boot       # flash+select only

In production, end users don't need any of this — the drDRO host app flashes the board over the same RS-485 wire from its Firmware screen. The tools/ scripts are developer utilities for bench work and CI.

HW-1 (current PCB): BOOT0 floats. A hardware reset can intermittently boot the ST system ROM (serial returns a lone \x00). The firmware avoids this by jumping (never resetting) across app↔bootloader handoffs, so the update cycle is safe; on the bench, if a power-on/NRST lands in the ROM, just retry until the app answers version. The permanent fix is tying BOOT0 to GND on the next PCB revision.


Configure (persistent settings)

The board keeps its rarely-changed configuration in flash (ping-pong A/B sectors, magic + CRC32, power-fail safe). You rarely touch these directly — the host app reads them on connect and writes them when you change a value — but you can configure them over the CLI too:

serial @ 115200 (e.g. tio /dev/ttyUSB0 -b 115200)
settings              # dump the current persisted settings
set servo.max 3000    # change a value in RAM
save                  # persist RAM settings to flash
load                  # reload settings from flash (discard RAM changes)

Persisted fields include the scale sync ratios, servo config (max speed, acceleration, jog, index feedrate), the boot mode and the active bank. See Board vs host settings for what lives where and why.

CLI reference

Both CLIs share the wire format: key=value lines, a crc=HH line, then a blank line; requests may carry an optional *HH XOR-8 checksum.

Application USART1

sta · get/set <var> · settings · save/load · bank [0|1] · rollback · version · help · update (enter bootloader) · reset

Variables: scales.{pos,speed,num,den,sync,filt} (4-element) · servo.{max,acc,jog,idx,mode,pos,speed,tgt} · diag.{cycles,interval}

Bootloader greets bootloader=ready

version · info (bank mode/active/loaded/validity) · bank [0|1] · boot.mode <app|bl> · flash <0|1> (YMODEM receive) · erase <0|1> · crc <0|1> · copy · rollback · boot · reset · help

Diagnostics

The USR_LED (PB12) blinks a repeating ~1 Hz, active-low heartbeat so you can tell what's running at a glance:

  • 1 blink — the application is running.
  • 2 blinks — the bootloader is running.
  • Longer counts are reserved for app error codes.

Over serial, version confirms the app is alive and reports the firmware version; sta streams live scale + servo state.


Develop

  • Native unit tests exercise the transport-independent protocol logic (parser, dispatch, registry, checksum) with the HAL/CMSIS-RTOS mocked — pio test -d app -e native. Add tests under app/test/.
  • Don't regenerate CubeMX. The CubeMX glue in app/src/ is frozen and hand-maintained; the HAL comes from the pinned framework — don't add Drivers/, system_stm32f4xx.c, or a startup .s.
  • Board specifics baked into the build: Cortex-M4F hard-float, HSE_VALUE=8000000 (the board crystal is 8 MHz; the framework default of 25 MHz would give the wrong USART1 baud), and -Ofast to match the timing-sensitive motion ISR.
  • The register image is the rampsHandler_t.shared struct — its field layout is the host contract; changes must stay in sync with the app.
  • Peripheral/pin map and full architecture detail live in docs/migration_todo.md.

CI & releases

.github/workflows/ci.yml builds both projects and runs the native tests on every push. release.yml uses python-semantic-release: pushes to dev cut vX.Y.Z-beta.N prereleases, pushes to main cut stable releases. Each release publishes drdro-app.*, drdro-bootloader.* and drdro-factory.hex.

📝

Design docs to read before hacking on internals: docs/protocol_design.md (line protocol + IAP rationale), docs/dualbank_design.md (the A/B update system), and docs/HARDWARE.md (board bugs the firmware works around).