BERGSONNE
Get started

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.

MethodL0L4W5H5Notes
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 bootloaderST ROM over USART. Universal; needs a USB-serial adapter.
I²C / SPI bootloaderST ROM over I²C/SPI. Good for factory fixtures. The W5 ROM is UART-only.
BLE OTAOver-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
}
SWD and USB DFU coexist
Use SWD for live debugging and 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"
ModeFlash layoutDFU protocolCore.ST.L4Core.ST.H5
custom8 KB bootloader, app at 0x08002000DFU 1.1 (1209:0002)Full autoFull auto
romApp at 0x08000000 (full flash)DfuSe (0483:DF11)Full autoPower cycle after flash
Which mode?
Use custom on Core.ST.H5 (fully automatic, no power cycle). Use rom on Core.ST.L4 for the simplest setup and the full flash space. Both use the same 1200-baud trigger — switching modes needs no code changes.

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-dfu

The 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 flash

You 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-dfu drives 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.bin

The 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.

Core.ST.H5 needs a power cycle
The H5 ROM’s “Go” command (jump to app) doesn’t work, due to the RSS/SFSP secure-boot architecture — after a ROM-DFU flash on Core.ST.H5 you must power-cycle to boot the new firmware (via 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.

BLE OTA — Core.ST.W5 (STM32WBA55)

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 CDCcore_init() brings up the CDC driver unconditionally on USB-capable Cores, even if your main.c never calls core_usb_init(). The touch listener is live the moment core_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.
Last resort: BOOT0
If the board exposes the BOOT0 pad, holding it high at reset forces the ST ROM bootloader regardless of what’s in flash — it bypasses all software, even a fully corrupted USB stack. Routing BOOT0 to a test pad or jumper gives you a worst-case recovery path.