STM32 Bootloader Usage: A Practical Guide

ampheoampheo
5 min read

Here’s a practical, engineer-friendly guide to using the STM32 Bootloader—both the built-in ST ROM bootloader and a custom bootloader you place in Flash.


A) Using ST’s ROM Bootloader (a.k.a. “System Memory” boot)

The ROM bootloader lives in System Memory and lets you program the MCU without any user code—handy for production, recovery, or field updates.

1) How to enter the ROM bootloader

You must make the core boot from System Memory at reset. Do it in one of two ways (family-dependent):

  • BOOT0 pin method (classic):

    1. Pull BOOT0 = 1 (HIGH).

    2. Reset or power-cycle the MCU.

    3. The core vectors into System Memory (the ROM bootloader).

    4. After flashing, set BOOT0 = 0 and reset to run your application.

  • Option-bytes method (newer families):

    • Configure “boot from System Memory” in Option Bytes (e.g., nBOOT0, BOOT_ADDx).

    • Reset to enter the ROM bootloader without touching pins.

    • Keep a recovery path (don’t permanently lock yourself out).

Tip: For one-off recovery on boards that expose BOOT0 and NRST, the pin method is simplest.

2) Which interfaces can you use?

Depends on the exact STM32 family, but commonly:

  • UART/USART (very common)

  • USB DFU (Device Firmware Upgrade)

  • I²C, SPI

  • (F)DCAN

  • (Some families offer additional/alternative ports.)

Check your part’s reference table (ST’s “AN2606”) for which peripheral instance & pins the ROM bootloader uses.

3) Wiring & tools (quick recipes)

UART bootload (generic)

  • Wiring: USB-to-UART adapter

    • Adapter TX → MCU RX (the specified USARTx pin)

    • Adapter RX → MCU TX

    • GND ↔ GND

  • Enter bootloader: BOOT0=1 then reset (or OB route).

  • Flash with STM32CubeProgrammer (GUI or CLI):

    • GUI: “UART” → pick COM port → “Connect” → “Erasing & Programming”.

    • CLI example (HEX file):

        STM32_Programmer_CLI -c port=COM5 -w app.hex -v -rst
      
    • CLI example (BIN file):

        STM32_Programmer_CLI -c port=COM5 -w app.bin 0x08000000 -v -rst
      
    • (If parity/baud matters for your device, CubeProgrammer handles it; otherwise add br=115200 etc.)

USB DFU bootload

  • Wiring: Just USB (device enumerates as DFU).

  • Enter bootloader: BOOT0=1 then reset (or OB).

  • Flash with:

    • STM32CubeProgrammer (GUI: “USB” target).

    • dfu-util (open source), example for BIN:

        dfu-util -a 0 -s 0x08000000:leave -D app.bin
      

      (:leave resets to run your app after writing.)

I²C / SPI / (F)DCAN bootload

  • Use STM32CubeProgrammer with the matching interface (requires correct transceiver/wiring).

  • Useful in production jigs where UART/USB isn’t available.

4) Typical flashing workflow (ROM bootloader)

  1. Enter the bootloader (BOOT0=1 or OB).

  2. Connect with your tool (CubeProgrammer/dfu-util).

  3. Erase target Flash.

  4. Program your image (HEX/ELF/BIN at 0x0800_0000 by default).

  5. Verify, then reset (or leave in DFU).

  6. Restore normal boot (BOOT0=0, or revert OB) and reset to run the app.


B) Using your own bootloader (in main Flash)

You might want your own bootloader for OTA, BLE/Wi-Fi/Ethernet updates, SD-card updates, custom encryption, A/B images, etc.

1) Memory map (example)

  • Bootloader at start of Flash (e.g. 0x0800_0000 … 0x08003FFF)

  • Application at APP_ADDR (e.g. 0x0800_4000)
    Adjust sizes to your project and Flash page/sector boundaries.

2) Linker changes (application)

  • Set application Flash origin to APP_ADDR.

  • Place the vector table at APP_ADDR (so the first 8 bytes are MSP & Reset vectors).

Example (GNU ld snippet):

FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 0x100000 - 0x4000

3) Bootloader → Application handoff

  • Disable interrupts, stop SysTick, deinit peripherals you touched.

  • Point vector table at the app and jump.

#define APP_ADDR 0x08004000u
typedef void (*app_entry_t)(void);

static void jump_to_app(void) {
    uint32_t app_msp   = *(uint32_t *)(APP_ADDR + 0x0);
    uint32_t app_reset = *(uint32_t *)(APP_ADDR + 0x4);

    __disable_irq();
    SysTick->CTRL = 0;           // stop SysTick
    // Deinit clocks/peripherals as needed…

    SCB->VTOR = APP_ADDR;        // app’s vector table
    __set_MSP(app_msp);
    __DSB(); __ISB();
    ((app_entry_t)app_reset)();  // go!
}

4) Update strategies

  • Single-image: erase app region, program new app, verify, then jump.

    • Guard with CRC/SHA to ensure integrity before jumping.
  • Dual-image (A/B): write new app to the inactive slot; after verification, flip a flag/option and reboot.

    • Provides power-fail safety and rollback.

5) Entering your bootloader

Common triggers:

  • GPIO “boot key” held at reset (e.g., user button).

  • Double-tap reset timing window.

  • Invalid app check (bad CRC) → stay in bootloader.

  • Command from the running app (e.g., write a magic value to BKP/RTC RAM, then reset).

  • Authenticate the image (ED25519/ECDSA or at least HMAC) before activation.

  • Use encrypted transport if the link is exposed (BLE/Wi-Fi).

  • Consider device Readout Protection and write protection for the bootloader pages.

  • On TrustZone-capable parts, keep the secure root (verification) in the secure world.


C) Quick troubleshooting checklist

  • Nothing runs after flashing?

    • Did you program to the correct address (BIN needs explicit address)?

    • Is BOOT0 back low or OB set to boot from Flash?

    • Vector table correct for app (MSP/PC valid at APP_ADDR)?

  • Interrupts misbehave after jump?

    • Did you set SCB->VTOR = APP_ADDR and disable old IRQs?

    • Clear/disable peripheral IRQ flags before jumping.

  • USB DFU not enumerating?

    • Not all chips support DFU in ROM; confirm your part and pins.

    • For a custom USB bootloader, verify clock setup (48 MHz requirement).

  • UART bootloader won’t connect?

    • Check the exact USART instance & pins for your part’s bootloader.

    • Swap TX/RX, confirm GND, try a different baud, and reset right before connect.


TL;DR

  • ROM bootloader = fastest way to flash or recover: set boot mode → use CubeProgrammer/dfu-util → program Flash at 0x08000000.

  • Custom bootloader = full control: reserve a Flash region, relocate your app, implement jump + integrity checks, and choose your update transport.

  • Always keep a reliable recovery path (BOOT0 pads, or an app command that enters the ROM bootloader/custom BL).

0
Subscribe to my newsletter

Read articles from ampheo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ampheo
ampheo