STM32 Dual-Firmware Bootloader

A minimal dual-slot bootloader for the STM32U575ZI, written in Rust. Two firmware slots, one word of SRAM to pick between them — no swap partition, no state machine, and no flash wear.

Flash Layout

┌─────────────────────────────────┐ 0x0800_0000
│  bootloader   (64 KiB)          │
├─────────────────────────────────┤ 0x0801_0000
│  Firmware A   (64 KiB)          │
├─────────────────────────────────┤ 0x0802_0000
│  Firmware B   (64 KiB)          │
└─────────────────────────────────┘

Each slot is a complete, independently linked firmware image with its own vector table. The three memory.x files differ only in FLASH ORIGIN; all of them map RAM at 0x2000_0000 with 768 KiB.

On every reset the bootloader reads a single word at 0x200A_0000 — mid-SRAM, clear of .data/.bss at the bottom and the stack at the top — and dispatches on it.

SRAM contents survive a system reset but are lost on a power cycle. That makes the selection purely a runtime choice: nothing is written to flash, so there is no state sector to wear out. It also means a power cycle always returns to Firmware A.

Tech Stack

Language: Rust (no_std)

MCU: STM32U575ZI (Cortex-M33)

Target: thumbv8m.main-none-eabihf

Runtime: Embassy

Tooling: probe-rs ≥ 0.24

Boot Flag

Flag value at 0x200A_0000Action
1Jump to Firmware B (0x0802_0000)
anything elseJump to Firmware A (0x0801_0000)

Defaulting everything that is not 1 to slot A means an uninitialised or corrupted word boots the known-good image rather than jumping somewhere undefined.

Update Flow

1.  running firmware receives a new image   (USB / UART / CAN / …)
2.  writes it into the *inactive* slot's flash range
3.  sets the boot flag in SRAM
4.  triggers a system reset
        │
        └─▶ bootloader reads the flag and dispatches to the new slot

Because the flag lives in SRAM, this is a try-once activation: the new image runs until the next power cycle, then the device falls back to slot A. That is the behaviour you want while bringing firmware up on the bench. A production OTA would need the selection persisted to flash plus a “confirm update” step once the new image proves it can boot.

Two Bootloader Implementations

BinaryFileHow it jumps
bootloadersrc/bin/bootloader.rsembassy-boot-stm32 BootLoader::load() — invalidates I-cache, sets VTOR, then bootloads
raw_bootloadersrc/bin/raw_bootloader.rsHand-rolled: disables interrupts, reads SP/PC from the vector table, sets VTOR, then inline asm

Both are functionally equivalent; bootloader is the default. The raw version exists to show that dispatching a Cortex-M image needs only three things — set VTOR, set the stack pointer, branch to the reset handler:

unsafe fn jump_to(app_start: u32) -> ! {
    cortex_m::interrupt::disable();

    let sp = read_volatile(app_start as *const u32);          // vector table +0x00
    let pc = read_volatile((app_start + 4) as *const u32);    // vector table +0x04

    write_volatile(0xE000_ED08 as *mut u32, app_start);       // SCB.VTOR

    asm!(
        "cpsie i",
        "msr msp, {sp}",
        "bx  {pc}",
        sp = in(reg) sp,
        pc = in(reg) pc,
        options(noreturn)
    );
}

Setting VTOR is the step most walkthroughs skip. Without it the CPU keeps consulting the bootloader's vector table, so the first interrupt after the jump lands in a stale handler.

Quick Start

1

Build and flash all three binaries

Bootloader plus both firmware slots. Each ELF carries its own load address, so probe-rs puts it in the right place.

make flash
2

Boot a slot and stream RTT

The target writes the boot flag over SWD, resets, and lets the bootloader dispatch. Firmware A prints "Hello from Firmware A!" once a second.

make run-a    # boot Firmware A (flag = 0)
make run-b    # boot Firmware B (flag = 1)
3

Try the hand-rolled bootloader

Same behaviour, but the jump is done with inline assembly instead of going through embassy-boot-stm32.

BIN=raw_bootloader make flash
BIN=raw_bootloader make run-b

Verifying the Slots

make verify reads the first two words back out of each slot. Distinct reset-handler addresses confirm three genuinely different binaries are in flash:

=== Flash slots: [initial-MSP] [Reset-Handler] ===
  bootloader @ 0x08000000  ->  200c0000 08000239
  app_a      @ 0x08010000  ->  200c0000 08010239
  app_b      @ 0x08020000  ->  200c0000 08020239

Related