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.
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.
The wire protocol and variable registry are documented on the Reference page. This page is about the firmware project itself.
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.
| Region | Sector(s) | Address | Size | Role |
|---|---|---|---|---|
| Bootloader | 0 | 0x08000000 | 16 K | Never erased by an update |
| Settings A / B | 1 / 2 | 0x08004000 | 16 K each | Ping-pong, power-fail safe |
| (reserved) | 3–4 | 0x0800C000 | 80 K | Future |
| Exec | 5 | 0x08020000 | 128 K | The app runs here |
| Bank 0 / 1 | 6 / 7 | 0x08040000 / 0x08060000 | 128 K each | Stored app versions |
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
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.
# 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:
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:
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
The app and bootloader are two sibling PlatformIO projects; pass -d <dir> to target one.
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.
A brand-new board needs both the bootloader and an app in Exec. Merge them into one image and write it with ST-Link:
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
pio run -d app -t upload # flash the app to Exec (0x08020000)
pio run -d bootloader -t upload # flash the bootloader (sector 0)
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):
# 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.
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:
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.
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.
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}
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
The USR_LED (PB12) blinks a repeating ~1 Hz, active-low heartbeat so you can tell what's running at a glance:
Over serial, version confirms the app is alive and reports the firmware
version; sta streams live scale + servo state.
pio test -d app -e native. Add tests under app/test/.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.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.rampsHandler_t.shared struct — its field layout is the host contract; changes must stay in sync with the app.docs/migration_todo.md..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).