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, and Core.ST.L4 goes one step further with serial update — reflashing over the USB serial port it already has, with no bootloader and no driver. 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. On Core.ST.L4, running firmware also accepts a serial update (make flash-serial, and Studio’s default), which never leaves the app’s own USB connection. Underneath all of them, 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 serial update✗✓✗✗Over the running firmware's USB serial port. No bootloader, no driver, no replug. Studio's default on L4.
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).

USB serial update — no bootloader

On Core.ST.L4, a Core that is running firmware built with a current SDK can be reflashed over the same USB serial port it already has. The host opens that port at 2400 baud; the firmware hands the Core to a small flasher in RAM, which rewrites flash on the same connection and restarts into the new image — about a second for a typical program.

make flash-serial    # Core.ST.L4 templates; Studio does this for you

Compared with the ROM bootloader path, nothing new appears on USB, so:

  • No driver on Windows. The serial port uses Windows’ built-in driver; there is no Zadig / WinUSB step.
  • ST’s tools are left alone. If STM32CubeProgrammer is installed, it keeps working — nothing asks you to swap its driver out.
  • No second device picker in Studio, and no unplug-and-replug after the flash.

In Studio, Flash on a Core.ST.L4 tries this first and falls back to the USB DFU path by itself when the Core doesn’t answer.

The first flash

Serial update lives in the firmware, so it needs firmware to be there. A blank Core, or one running a program built before serial update existed (September 2026), doesn’t answer: make flash-serial says so and exits, and Studio falls back to DFU. Flash it once with make flash-dfu; every program built since carries the flasher, so from then on updates skip the bootloader.

Running a workshop?
Pre-flash the Cores with any current build before handing them out. Attendees then never meet the ROM bootloader — which, on Windows, is where the driver setup lives.

If an update is interrupted

An update can fail, but it can’t leave a half-written program running. The image’s vector table is checked before anything is erased; the first page of flash is erased before any other page is touched, held in RAM, and written last — only after the whole image has passed a CRC check. On Core.ST.L4 an erased first page makes the chip boot the ST ROM bootloader, so a Core that loses power mid-update comes back as 0483:df11: recover it with make flash-dfu (or Studio’s blank-Core path) and one replug.

Under the hood

  • The trigger is the line coding: any host opening the Core’s serial port at 2400 baud hands it over. Something that does so by accident gets a 10-second pause, then the same program restarts.
  • The flasher adds about 5 KB to every Core.ST.L4 image. It isn’t built into the parked custom bootloader layout, and -DHAL_SERIAL_UPDATE_DISABLE leaves it out entirely.
  • The wire protocol, the safety rules and the bench results are in the SDK’s serial-update protocol.

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. On Core.ST.H5 it’s the everyday path; on Core.ST.L4, serial update has taken that role and DFU is for the first flash and for recovery. 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": "rom" }   // the standard; see "custom" below
ModeFlash layoutDFU protocolCore.ST.L4Core.ST.H5
custom (parked)8 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
Use rom — custom is parked
rom is what the fleet runs: it’s Studio’s default, what the SDK’s templates ship with, and where the watchdog brick-recovery lives. Its one cost is the Core.ST.H5 power cycle above.

custom is kept for future use but the build refuses it unless you also pass CUSTOM_BOOTLOADER_ACK=1. The hazard is that its app lives at 0x08002000: flash a custom-layout image onto a board that’s in ROM-DFU layout and you write over the middle of the resident app. Both modes use the same 1200-baud trigger, so no application code changes either way.

First-time setup

rom needs no bootloader install at all — hold BOOT0 for the very first flash, and make flash-dfu works on its own from then on.

The parked custom mode installs its bootloader once per board, after which BOOT0 is never needed 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

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.

Windows: the ROM bootloader needs a driver
The ROM bootloader has no Windows driver of its own, so browsers and dfu-util can’t open it until Zadig binds WinUSB to it (Options → List All Devices, pick USB ID 0483 DF11). If you’ve used STM32CubeProgrammer, ST’s driver already owns that device — Zadig replaces it, and CubeProgrammer can’t see the bootloader until you switch back. On Core.ST.L4, serial update avoids all of this after the first flash.
A blank Core.ST.L4 needs one replug after its first flash
With nothing in flash, the L4 boots the ROM bootloader. It only re-checks whether flash is empty at power-on, so after the first ROM-DFU flash the Core stays in the bootloader until it’s unplugged and plugged back in. Every flash after that restarts on its own.
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 and serial update both depend 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 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.