Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Bootloaders
Bootloader architecture for Cortex-M. ROM vs flash bootloaders, vector-table relocation, dual-bank fail-safe update, signed and encrypted firmware, MCUboot, STM32 system bootloader, USB DFU.
A bootloader is the small, trusted, slow-changing program that runs first on a Cortex-M part and decides what to run next. On a finished product it might check a signature, verify a slot, swap an image, and then jump to the application. On a developer board it might also expose a USB DFU port, a UART menu, or a network listener so you can push new firmware without a JTAG probe. Either way, the bootloader is the piece of code most likely to brick the device if it's wrong — so it's worth designing carefully and changing rarely.
This page is a tour of bootloader architecture on modern Cortex-M parts: where the bootloader lives, how it hands off to the application via SCB->VTOR, how dual-bank layouts give you fail-safe field update, the cryptographic primitives that make secure boot real, and the major prebuilt bootloaders (MCUboot, the ST system bootloader, RP2040 picoboot, USB DFU class).
- Where it lives
- ROM-resident (built into silicon), or flash-resident (first sector of flash). Vendors usually ship both: a small immutable ROM bootloader plus an optional flash bootloader.
- Typical size
- 4 KB (minimal jump-to-app) to 64 KB (full MCUboot with crypto). 16-32 KB is the common range for production secure boot.
- Vector table relocation
- SCB->VTOR = APP_BASE; then load SP from APP_BASE[0], PC from APP_BASE[1]
- Signature schemes
- RSA-2048/3072/4096, ECDSA P-256/P-384, ED25519. ED25519 most modern (fast, deterministic, small keys)
- Encryption
- AES-CTR or AES-CBC for image confidentiality. Key wrapped by device root key or PUF.
- Update strategy
- A/B (dual-bank), single-bank-with-staging, overwrite (no rollback)
- Anti-rollback
- Monotonic counter in OTP or one-shot flash sector, checked against image version
What a bootloader does
In its simplest form a bootloader is a five-line program: load the stack pointer from the application's vector table, jump to the application's reset handler. Everything else — verification, update, recovery, communication — is policy you build on top of that primitive.
The minimal hand-off:
typedef void (*reset_handler_t)(void);
void boot_to_app(uint32_t app_base) {
uint32_t app_sp = *(uint32_t *)(app_base + 0);
uint32_t app_reset = *(uint32_t *)(app_base + 4);
SCB->VTOR = app_base; // re-point vector table
__set_MSP(app_sp); // load main stack pointer
__DSB();
__ISB();
((reset_handler_t)app_reset)(); // jump to reset_Handler
}This works because every Cortex-M vector table starts with [SP_initial, Reset_Handler, NMI_Handler, ...]. The application linker script places its vector table at APP_BASE (commonly 0x08008000 or 0x08010000 on STM32, 0x10000400 on RP2040). The bootloader loads those two words, sets VTOR, and jumps. After the jump, every subsequent interrupt finds its handler via the new VTOR, and the application takes over completely.
The hard part isn't the jump; it's deciding which application to jump to, after what checks.
ROM vs flash bootloaders
Almost every modern Cortex-M part ships with two layers of bootloader.
ROM bootloader: tiny program in mask ROM, immutable, runs from 0x00000000 on the first cycle out of reset. It checks a few pins (BOOT0/BOOT1 on STM32, BOOTSEL on RP2040) and a few flash bytes to decide whether to jump to the flash bootloader, enter DFU/USB mass-storage mode, or jump straight to the application. The ROM bootloader is what makes the part "always recoverable" — even if you completely corrupt flash, the ROM mode is available.
Flash bootloader: optional, lives at the start of flash, mutable (you wrote it). Does the project-specific update, signature check, recovery menu. This is what most "bootloader" discussions mean.
| Part | ROM bootloader features |
|---|---|
| STM32F4/F7/H7 | UART, USB DFU, I²C, SPI, CAN. Triggered by BOOT0=1. |
| STM32U5/H5 | RSS (Root Secure Services) — secure ROM that does signature verification before handing off |
| RP2040 | BOOTSEL pin enters USB MSC mode — drag-and-drop UF2 files |
| nRF52/53 | MBR (Master Boot Record) provides SoftDevice services; SDK ships a separate flash bootloader |
| NXP MIMXRT | FlexSPI ROM that can boot directly from external QSPI flash |
| ATSAM | SAM-BA bootloader over UART/USB, factory-programmed in ROM |
Knowing the ROM features matters because it tells you what your recovery path is if your flash bootloader bricks the device. Always test ROM-mode recovery before deploying.
Application slot layout
A typical dual-bank Cortex-M flash, 1 MB part:
0x08000000 +-----------------------------------+
| Bootloader (32 KB) |
0x08008000 +-----------------------------------+
| Slot A header (image metadata) |
0x08008100 | |
| Slot A image (480 KB) |
| |
0x08080000 +-----------------------------------+
| Slot B header |
0x08080100 | |
| Slot B image (480 KB) |
| |
0x080F8000 +-----------------------------------+
| Scratch / metadata (32 KB) |
0x08100000 +-----------------------------------+The bootloader inspects both slot headers, decides which to boot (newest valid version, or "test image" if one is pending), and jumps to that slot's reset vector. Slot A and slot B are interchangeable — either can boot. This is what makes A/B updates safe: you write the new image into the other slot while the current slot keeps running, and on the next reboot the bootloader picks the new one if it's valid.
The header carries:
- Magic value (so the bootloader can detect "no image present")
- Version number (monotonically increasing — see anti-rollback)
- Image size and load address
- Signature (RSA/ECDSA/ED25519)
- Optional encryption key wrap
- A flag indicating "this is a test image — confirm or revert"
Signature verification
For a non-trivial product, you must sign the image and verify the signature before booting it. The threat model: an attacker can write to flash (via JTAG, DFU, OTA, or physical reflashing) and substitute a malicious image. Signature verification ensures the bootloader only runs images authored with the private key you keep on a build server.
Three commonly used schemes:
| Scheme | Key size | Sig size | Verify cycles (M4F @ 100 MHz) |
|---|---|---|---|
| RSA-2048 | 2048-bit modulus | 256 B | ~50 M cycles (~500 ms) |
| ECDSA P-256 | 32-byte priv / 64-byte pub | 64 B | ~20 M cycles (~200 ms) |
| ED25519 | 32 B priv / 32 B pub | 64 B | ~10 M cycles (~100 ms) |
ED25519 is the modern choice — small keys, small signatures, fast verification, deterministic (no random nonce needed at sign time). The Mbed TLS, TinyCrypt, and BearSSL libraries all support all three.
The public key is embedded in the bootloader's flash, ideally in a region marked read-only by the MPU. The private key never leaves the build server (or, better, a hardware HSM). For products that need multiple-vendor signing or revocation, store a root public key, and authenticate intermediate keys via a chain.
Image encryption
Signing proves authenticity; encryption protects confidentiality. If you need to keep firmware secrets (algorithm IP, customer-specific keys) out of the hands of someone reading flash, encrypt the image.
Common pattern: AES-256-CTR with a per-device key. The key is derived from a chip-unique identifier (the STM32 96-bit UID, for example) plus a manufacturer secret, or from a PUF (Physically Unclonable Function) where the silicon provides the entropy. The bootloader decrypts the image on the fly into RAM, or decrypts each flash sector as it's read.
STM32H5 / U5 introduced on-the-fly decryption (OTFDEC) — a hardware block that decrypts flash as the CPU fetches instructions, with no software overhead. NXP iMX RT has a similar feature (BEE — Bus Encryption Engine).
MCUboot — the open-source standard
MCUboot is the de facto bootloader for Cortex-M secure boot in non-vendor-locked designs. Started at Apache Mynewt, now used by Zephyr, Nordic Connect SDK, Espressif IDF, and many custom builds. Apache 2.0 license.
What it gives you out of the box:
- Slot-based image layout (single-image, dual-bank, multi-image)
- ED25519, ECDSA-P256, and RSA signature verification (build-time choice)
- Optional AES-CTR encryption
- Swap-using-scratch or overwrite-only update modes
- Test+confirm boot flow with revert
- Anti-rollback via image version
- Hooks for hardware crypto accelerators
Configuring MCUboot is largely about laying out flash and picking crypto. The image format (the "image trailer") is documented and stable; tools like imgtool.py produce signed images that drop into the slot.
A minimal MCUboot config for an STM32H7:
CONFIG_BOOT_SIGNATURE_TYPE_ED25519=y
CONFIG_BOOT_BOOTSTRAP=n
CONFIG_BOOT_SWAP_USING_SCRATCH=y
CONFIG_BOOT_MAX_IMG_SECTORS=512
CONFIG_BOOT_ENCRYPT_EC256=y
CONFIG_BOOT_VALIDATE_SLOT0=yBuild it once with the public key embedded, flash to the boot region, and use imgtool sign --key ed25519.pem --version 1.2.3 app.bin app.signed.bin to produce updates.
STM32 system bootloader
ST's mask-ROM bootloader on every STM32 is a useful fallback. Triggered by holding BOOT0 high at reset (or BOOT1=0 + BOOT0=1 on some parts). Once active, it listens for commands on:
- UART1 (USART1 typically, at 115200 8N1)
- USB DFU (vendor 0x0483, product 0xDF11)
- I²C, SPI, CAN on parts that support them
- USB MSC (drag-and-drop .bin) on newer H5/U5 parts
The protocol is documented in AN3155 (UART), AN3156 (USB DFU), AN4221 (I²C), AN4286 (SPI), AN3154 (CAN). The host-side tool is STM32CubeProgrammer or open-source stm32flash / dfu-util.
This bootloader is always available — you can recover any STM32 via BOOT0 even if flash is completely corrupted. Build that into your factory test and field-recovery procedures.
RP2040 picoboot
The RP2040 ROM bootloader (entered by holding BOOTSEL at power-on) exposes a USB endpoint that appears as a mass-storage device. Drop a .uf2 file onto it and the ROM programs the external QSPI flash. There's also a vendor-class endpoint (picoboot) for tooling like picotool.
UF2 is a simple format: a sequence of 512-byte blocks, each with a magic header, a target address, and 256 bytes of payload. Trivial to generate. The combination of UF2 + USB MSC is the easiest reflash story in the industry — anyone with a USB cable can update firmware, no driver install, no Zadig dance.
USB DFU class
If you need standardised in-application firmware update, the USB Device Firmware Upgrade (DFU) class is the industry standard. Defined in USB DFU 1.1, supported by dfu-util on every host OS, no custom driver required.
The flow:
- App advertises DFU runtime interface alongside its normal endpoints
- Host sends
DFU_DETACH; app reboots into DFU mode (or stays in app and listens on DFU interface) - Host sends image in chunks via
DFU_DNLOAD - Device computes signature / writes to flash
DFU_GETSTATUSreports progress- Final
DFU_DNLOADof zero bytes commits; device reboots
ST, NXP, and Nordic all ship DFU implementations as part of their USB middleware. MCUboot has a DFU front-end for Zephyr.
Further reading
- OTA update flow — over-the-air variants, delta updates, rollback
- Startup & vector table — what the application sees on boot
- Linker scripts — splitting flash into bootloader and application regions
- TrustZone-M deep dive — secure boot leveraging hardware partitioning
- MCUboot documentation: docs.mcuboot.com
- ST AN3155, AN3156 (system bootloader protocols)
- USB.org DFU 1.1 specification