Bootloading & flashing
Getting firmware onto a Core, and how the boot path keeps a board recoverable. SWD is the universal debug-and-flash interface; the USB-programmable Cores add touchless DFU so the edit-build-flash loop is a single command. This is the “how it works” companion to Install & flash.
Overview
Every Core supports SWD as its primary debug-and-program interface. The USB-programmable Cores — currently Core.ST.L4 and Core.ST.H5 — also support runtime USB DFU: run make flash-dfu and the board updates over USB with no probe, no pins, no manual steps. Underneath both, every ST chip carries a ROM bootloader that can’t be erased — the recovery path of last resort.
| Method | L0 | L4 | W5 | H5 | Notes |
|---|---|---|---|---|---|
| SWD (debug) | ✓ | ✓ | ✓ | ✓ | Two wires (SWCLK + SWDIO). Always available; needs an ST-Link or J-Link. |
| USB DFU (runtime) | ✗ | ✓ | ✗ | ✓¹ | Custom or ROM bootloader + 1200-baud touch. No BOOT0 needed. |
| ROM DFU (USB) | ✗ | ✓ | ✗ | ✓¹ | ST ROM bootloader over USB. Recovery and production flashing. |
| UART bootloader | ✓ | ✓ | ✓ | ✓ | ST ROM over USART. Universal; needs a USB-serial adapter. |
| I²C / SPI bootloader | ✓ | ✓ | ✗ | ✓ | ST ROM over I²C/SPI. Good for factory fixtures. The W5 ROM is UART-only. |
| BLE OTA | ✗ | ✗ | ⟳ | ✗ | Over-the-air via a BLE GATT service. Core.ST.W5 only — it has the radio. Roadmap. |
✓ available · ✗ not supported · ⟳ planned · ¹ Core.ST.H5 ROM DFU needs a power cycle after flashing (an H5 silicon limitation — see below).
SWD — debug & flash
SWD is the standard interface across every Core: live breakpoints, memory inspection, variable watching, and flashing — all over two wires (SWCLK + SWDIO) from an ST-Link or J-Link.
make flash # flash via SWD (OpenOCD)By default the SWD pads are reserved and kept out of pad assignment. If a design never needs a probe after production, release them as GPIO in config.json:
"debug": {
"interface": "swd",
"dedicated": false // release the SWD pads as GPIO after flashing
}make flash-dfu for the fast edit-compile-flash loop — they don’t interfere.USB DFU — touchless flashing
On Core.ST.L4 and Core.ST.H5, make flash-dfu updates the board over USB with no BOOT0 pin and no probe. The Makefile sends a 1200-baud touch to the USB serial port (the same convention Arduino uses); the CDC driver catches it and reboots into DFU, the host downloads the new image, and the board reboots into it — about three seconds end to end. It’s automatic: the touch handler is built into the USB CDC driver and core_init(), so your application needs no extra code.
Custom vs ROM bootloader
Pick a mode with the "bootloader" key in config.json; the Makefile reads it automatically.
{ "bootloader": "custom" } // or "rom"| Mode | Flash layout | DFU protocol | Core.ST.L4 | Core.ST.H5 |
|---|---|---|---|---|
| custom | 8 KB bootloader, app at 0x08002000 | DFU 1.1 (1209:0002) | Full auto | Full auto |
| rom | App at 0x08000000 (full flash) | DfuSe (0483:DF11) | Full auto | Power cycle after flash |
First-time setup
The custom bootloader is installed once per board, then you never touch BOOT0 again:
# 1. Enter ROM DFU: hold BOOT0 high while plugging in USB
dfu-util -l # verify: Found DFU: [0483:df11]
# 2. Flash the bootloader (one time)
make flash-bootloader
# 3. Flash your app — and every time after
make flash-dfuThe rom mode needs no bootloader install — hold BOOT0 for the very first flash, then make flash-dfu works on its own.
Under the hood
When the host opens the CDC port at 1200 baud and drops DTR, the USB driver writes a magic value (0xDEADBEEF) to a reserved word at the top of SRAM and resets the chip. SRAM survives the reset; on reboot the bootloader (custom) or core_init() (rom) sees the magic and enters DFU. The flash layout differs by mode:
custom: rom:
0x08000000 Bootloader 8 KB 0x08000000 Application (full flash)
0x08002000 Application — ROM bootloader lives in
120 KB (L4) / 504 KB (H5) system memory, not flashYou can also trigger DFU from firmware — e.g. behind a button or a command:
#include "hal_dfu.h"
hal_dfu_reboot(); // reboot into DFU mode (does not return)ROM bootloader — recovery
Every ST chip ships a ROM bootloader in system memory. It can’t be erased — no matter what’s in flash, BOOT0 always gets you back to it. It plays two roles:
- Primary DFU — with
"bootloader": "rom"there’s no custom bootloader to install and the app gets the full flash;make flash-dfudrives the ROM over the 1200-baud touch. - Emergency recovery — hold BOOT0 high at reset to enter ROM DFU regardless of what’s in flash, even if the app or custom bootloader is corrupt.
# Enter ROM DFU: hold BOOT0 high, plug in USB
dfu-util -l # verify: Found DFU: [0483:df11]
# Flash via the ROM (DfuSe protocol — note the address):
dfu-util -a 0 -s 0x08000000:leave -D build/my-firmware.binThe ROM uses ST’s DfuSe protocol (the -s 0x08000000 address flag), distinct from the plain DFU 1.1 the custom bootloader speaks. The Makefile handles the difference for you.
make flash-dfu, CubeProgrammer, or manual dfu-util alike). The custom bootloader has no such limitation.UART / I²C / SPI
The ST ROM bootloaders also accept firmware over UART, I²C, and SPI — handy for automated factory fixtures or anywhere USB isn’t available. Core.ST.L0, Core.ST.L4, and Core.ST.H5 support all three; the Core.ST.W5 (WBA55) ROM is UART-only.
These paths work at the hardware level today; make flash-uart and the matching isp config are coming in a near-term SDK update.
OTA — over-the-air (roadmap)
Wireless field updates need a radio, so OTA targets the one Core that has one: Core.ST.W5.
The WBA55’s 1 MB flash holds two firmware images. A small bootloader validates the staged image, swaps it in, then boots — new firmware arrives over BLE through a GATT OTA service, so the device stays in the field.
Protecting DFU recovery
Touchless DFU depends on the USB interrupt firing inside the CDC driver. If something stops that interrupt from running, the board can no longer enter DFU over USB — and on a board without BOOT0 or SWD access, that effectively bricks it. The SDK adds two automatic protections, but a few things in your code can still defeat them.
Built-in protections
- Always-on USB CDC —
core_init()brings up the CDC driver unconditionally on USB-capable Cores, even if yourmain.cnever callscore_usb_init(). The touch listener is live the momentcore_init()returns. - Fault → DFU reboot — when a DFU bootloader is configured, the HardFault handler blinks one SOS and then reboots into DFU. A null-pointer dereference or stack overflow drops you into the bootloader instead of hanging forever.
What can still go wrong
- Disabling interrupts globally.
__disable_irq()masks USB too. If you crash or loop before re-enabling, the touch can’t be detected. Keep critical sections short. - Entering STOP / STANDBY. Deep-sleep modes power down USB. Configure a wakeup source and re-init USB after waking, or the touch is dead until the next wake.
- Reconfiguring the USB pads as GPIO. The DP/DM pads are set to the USB alternate function by
core_init(); rewriting them as GPIO disconnects USB. Never touch those pads’ mode bits. - An infinite loop (with interrupts on) is safe. The touch is handled entirely in the USB interrupt, so a stuck main loop doesn’t block recovery.

