Series Context: This is Part 16 of our 20-part CMSIS Mastery Series. A bootloader is the first code that runs after power-on and the last line of defence against a bricked device. Part 3 (startup/vector table) and Part 15 (TrustZone/secure boot) are essential prerequisite reading for this part.
1
Overview & ARM Cortex-M Ecosystem
CMSIS layers, Cortex-M families, memory map, toolchains
Completed
2
CMSIS-Core: Registers, NVIC & SysTick
core_cmX.h, register access, interrupt controller, SysTick timer
Completed
3
Startup Code, Linker Scripts & Vector Table
Reset handler, BSS init, scatter files, boot process
Completed
4
CMSIS-RTOS2: Threads, Mutexes & Semaphores
Thread management, synchronization primitives, scheduling
Completed
5
CMSIS-RTOS2: Message Queues & Event Flags
Inter-thread comms, ISR-to-thread, real-time design patterns
Completed
6
CMSIS-DSP: Filters, FFT & Math Functions
FIR/IIR filters, FFT, SIMD optimizations
Completed
7
CMSIS-Driver: UART, SPI & I2C
Driver abstraction layer, callbacks, DMA integration
Completed
8
CMSIS-Pack & Software Components
Pack files, device support, dependency management
Completed
9
Debugging with CMSIS-DAP & CoreSight
SWD/JTAG, HardFault analysis, ITM tracing
Completed
10
Portable Firmware: Multi-Vendor Projects
HAL vs CMSIS, cross-platform BSPs, reusable libraries
Completed
11
Interrupts, Concurrency & Real-Time Constraints
Interrupt latency, critical sections, lock-free programming
Completed
12
Memory Management in Embedded Systems
Static vs dynamic, heap fragmentation, memory pools
Completed
13
Low Power & Energy Optimization
Sleep modes, clock gating, tickless RTOS, power profiling
Completed
14
DMA & High-Performance Data Handling
DMA basics, peripheral transfers, zero-copy techniques
Completed
15
Security: ARMv8-M & TrustZone
Secure/non-secure worlds, secure boot, firmware protection
Completed
16
Bootloaders & Firmware Updates
OTA updates, dual-bank flash, fail-safe strategies
You Are Here
17
Testing & Validation
Unity/Ceedling unit tests, HIL testing, integration testing
18
Performance Optimization
Compiler flags, inline assembly, cache (M7/M33), profiling
19
Embedded Software Architecture
Layered design, event-driven, state machines, component-based
20
Tooling & Workflow (Professional Level)
CI/CD for embedded, MISRA, static analysis, Doxygen
Bootloader Fundamentals
A bootloader is a small, standalone firmware image that executes first on every power-on and reset. Its job is to decide what to run next — either the existing application, a newly received firmware update, or a recovery image — and to perform that transition safely. In a product context, "safely" means: never leave the device in an unbootable state, even if power is cut mid-update.
The simplest possible bootloader has three responsibilities:
- Check whether a firmware update is pending (via a flag in non-volatile storage)
- If an update is pending: receive the new image, verify its integrity, write it to flash
- Verify the application image integrity, then jump to it
Bootloader Design Decisions
| Strategy |
Fail-Safety |
Max Firmware Size |
Complexity |
Typical Use Case |
| Single-bank (overwrite) |
Low — bricked if power cut mid-write |
Full flash minus bootloader |
Low |
Factory programming only; no OTA |
| Single-bank with staging area |
Medium — new image staged in external flash, overwritten atomically |
Half flash (internal) + external flash for stage |
Medium |
Low-cost MCUs with external SPI flash |
| Dual-bank (A/B) |
High — active bank executes while inactive bank receives update; swap is atomic |
Half of total flash per image |
Medium-High |
Production IoT, automotive, industrial |
| External flash staging |
High — new image in external flash; verified before overwriting internal |
Full internal flash; external flash holds update |
Medium |
MCUs with small internal flash but large image |
| MCUboot managed |
Very High — swap, direct-xip, overwrite modes; rollback counter; encrypted images |
Configurable slot sizes |
High (but framework handles complexity) |
Professional products targeting Zephyr, TF-M, PSA |
Update Flag in RTC Backup Registers
The bootloader needs a way to know if an update was requested by the application (e.g., after downloading a new image over BLE). RTC backup registers are ideal: they survive soft resets, are battery-backed, and retain their value across power cycles on most STM32 devices.
/**
* Bootloader entry — check RTC backup register for update request.
* RTC_BKP0R: update flag (0xDEAD1234 = update pending)
* RTC_BKP1R: update result (set by bootloader after write attempt)
*
* Application triggers update by:
* 1. Writing firmware to staging area (external flash or bank B)
* 2. Writing 0xDEAD1234 to RTC_BKP0R
* 3. Triggering a system reset (NVIC_SystemReset())
*/
#include "stm32h7xx.h"
#include "core_cm7.h"
#define UPDATE_MAGIC 0xDEAD1234u
#define UPDATE_OK 0xCAFEBABEu
#define UPDATE_FAILED 0xDEADBEEFu
#define APP_BASE 0x08040000u /* Bank A app start on STM32H743 */
#define APP_MAGIC_OFFSET 0x00000000u /* first word of app vector table (SP) */
typedef void (*app_func_t)(void);
/* Check if the value stored in SP position looks like a valid SRAM address */
static bool is_valid_sp(uint32_t sp_value)
{
/* STM32H743 internal SRAM: 0x20000000 – 0x2007FFFF */
return (sp_value >= 0x20000000u && sp_value <= 0x20080000u);
}
int main(void)
{
/* Enable RTC backup register access */
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
uint32_t flag = RTC->BKP0R;
if (flag == UPDATE_MAGIC) {
/* Clear the flag immediately */
RTC->BKP0R = 0u;
/* Perform firmware update — see later sections */
int32_t result = perform_firmware_update();
RTC->BKP1R = (result == 0) ? UPDATE_OK : UPDATE_FAILED;
/* Reset to boot into new image (or roll back on failure) */
NVIC_SystemReset();
}
/* No update pending — verify and jump to application */
uint32_t app_sp = *(volatile uint32_t *)APP_BASE;
if (!is_valid_sp(app_sp)) {
/* No valid application — enter recovery/DFU mode */
enter_recovery_mode();
for (;;) {}
}
/* Verify CRC32 of application before jumping */
if (verify_application_crc() != 0) {
enter_recovery_mode();
for (;;) {}
}
/* Disable SysTick and all interrupts before jumping */
SysTick->CTRL = 0u;
for (uint32_t i = 0u; i < 8u; i++) {
NVIC->ICER[i] = 0xFFFFFFFFu;
NVIC->ICPR[i] = 0xFFFFFFFFu;
}
__DSB();
__ISB();
/* Relocate vector table to application */
SCB->VTOR = APP_BASE;
__DSB();
/* Set MSP from application vector table */
__set_MSP(app_sp);
__ISB();
/* Jump to application Reset_Handler */
uint32_t app_reset = *(volatile uint32_t *)(APP_BASE + 4u);
app_func_t jump = (app_func_t)app_reset;
jump();
for (;;) {}
}
Application at a Fixed Offset
VTOR Relocation — Linker Script Changes
When the bootloader occupies the first N KB of flash, the application must be linked to start at flash base + N. This requires both a linker script change in the application project and a VTOR relocation at startup. The application's linker script must match the flash origin the bootloader will jump to.
# Application linker script — adjust FLASH origin to skip bootloader
# Bootloader occupies 0x08000000 – 0x0803FFFF (256 KB)
# Application starts at 0x08040000
MEMORY
{
FLASH (rx) : ORIGIN = 0x08040000, LENGTH = 1792K /* 2M - 256K bootloader */
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* ... rest of sections ... */
}
Application Startup: Relocating VTOR
The application's own startup code (Reset_Handler) must relocate VTOR to match its actual load address. When the bootloader writes SCB->VTOR = APP_BASE, interrupts will vector correctly. But the application's startup code should also confirm this at boot so it can be debugged independently without a bootloader.
/**
* Application Reset_Handler — called by bootloader jump.
* Relocate VTOR, initialise data/BSS, then call main().
* This is normally in startup_stm32h7xx.s — shown here as C pseudocode.
*/
#include "stm32h7xx.h"
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void)
{
/* Relocate VTOR to application base (bootloader already did this,
* but do it here too for standalone debugging without bootloader) */
SCB->VTOR = 0x08040000u;
__DSB();
/* Initialise .data section — copy from Flash to SRAM */
uint32_t *src = &_sidata;
for (uint32_t *dst = &_sdata; dst < &_edata; ) {
*dst++ = *src++;
}
/* Zero-fill .bss section */
for (uint32_t *bss = &_sbss; bss < &_ebss; ) {
*bss++ = 0u;
}
/* Call platform init (SystemInit) then main */
SystemInit();
main();
/* Should never return */
for (;;) { __NOP(); }
}
Common Mistake: Forgetting to update the linker script flash origin when adding a bootloader. If the application is linked at 0x08000000 but the bootloader occupies that region, the first instruction executed will be part of the bootloader code — the application will immediately crash or corrupt the bootloader's data region.
Dual-Bank Flash & A/B Update Strategy
Dual-bank flash is the gold standard for fail-safe firmware updates. The MCU's flash is divided into Bank A (currently executing) and Bank B (receives the new image). Programming Bank B never interrupts execution in Bank A. Once Bank B is verified, a hardware bank-swap register setting causes Bank B to appear at address 0 on next reset — becoming the new Bank A.
Dual-Bank Flash Swap — STM32H743
/**
* Dual-bank flash swap on STM32H743.
*
* Flash layout (2 MB):
* Bank A: 0x08000000 – 0x080FFFFF (1 MB) — bootloader + current app
* Bank B: 0x08100000 – 0x081FFFFF (1 MB) — receives new firmware
*
* After programming Bank B and verifying its CRC:
* 1. Unlock FLASH option bytes
* 2. Set SWAP_BANK bit in OPTSR_PRG
* 3. Trigger option byte reload (OPTSTRT)
* 4. On next reset, Bank B appears at 0x08000000 — becomes new Bank A
*/
#include "stm32h7xx.h"
#include "stm32h7xx_hal_flash.h"
#include "stm32h7xx_hal_flash_ex.h"
#define BANK_B_BASE 0x08100000u /* Bank B start */
#define BANK_B_SIZE 0x00100000u /* 1 MB */
/* Write a page of firmware to Bank B */
int32_t write_firmware_page(uint32_t dest_offset, const uint32_t *src,
uint32_t word_count)
{
HAL_FLASH_Unlock();
for (uint32_t i = 0u; i < word_count; i += 8u) {
/* STM32H7 flash program unit is 256 bits (8 x 32-bit words) */
uint32_t dest_addr = BANK_B_BASE + dest_offset + (i * 4u);
HAL_StatusTypeDef st = HAL_FLASH_Program(
FLASH_TYPEPROGRAM_FLASHWORD,
dest_addr,
(uint32_t)(src + i));
if (st != HAL_OK) {
HAL_FLASH_Lock();
return -1;
}
}
HAL_FLASH_Lock();
return 0;
}
/* Trigger bank swap after successful verification */
int32_t trigger_bank_swap(void)
{
FLASH_OBProgramInitTypeDef ob_cfg = {0};
/* Read current option bytes */
HAL_FLASHEx_OBGetConfig(&ob_cfg);
/* Toggle SWAP_BANK */
ob_cfg.OptionType = OPTIONBYTE_USER;
ob_cfg.USERType = OB_USER_SWAP_BANK;
if (ob_cfg.USERConfig & OB_SWAP_BANK_ENABLE) {
ob_cfg.USERConfig = OB_SWAP_BANK_DISABLE;
} else {
ob_cfg.USERConfig = OB_SWAP_BANK_ENABLE;
}
HAL_FLASH_OB_Unlock();
HAL_StatusTypeDef st = HAL_FLASHEx_OBProgram(&ob_cfg);
if (st != HAL_OK) {
HAL_FLASH_OB_Lock();
return -1;
}
/* Launch option byte reload — this triggers a system reset */
HAL_FLASH_OB_Launch();
/* Should not reach here — system resets */
for (;;) {}
}
Rollback on Boot Failure
After a bank swap, the new firmware must "confirm" itself as good within a watchdog timeout. If the application fails to set a "confirmed" flag within N seconds, the watchdog resets the device and the bootloader rolls back to the previous bank.
/* Bootloader: check if new firmware confirmed itself */
#define CONFIRMED_MAGIC 0x600DF00Du
#define BOOT_TRIES_MAX 3u
static void check_and_rollback(void)
{
uint32_t confirmed = RTC->BKP2R;
uint32_t tries = RTC->BKP3R;
if (confirmed == CONFIRMED_MAGIC) {
/* Application confirmed — reset try counter */
RTC->BKP3R = 0u;
return; /* proceed to boot application */
}
/* Application did not confirm — count failed attempts */
tries++;
RTC->BKP3R = tries;
if (tries >= BOOT_TRIES_MAX) {
/* Roll back: swap banks back to last known good */
RTC->BKP3R = 0u;
trigger_bank_swap(); /* resets system with previous bank */
}
}
/* Called from application after successful initialisation */
void firmware_confirm(void)
{
HAL_PWR_EnableBkUpAccess();
RTC->BKP2R = CONFIRMED_MAGIC;
RTC->BKP3R = 0u;
}
OTA Transport Protocols
UART XMODEM Receive — Simplified Implementation
XMODEM is the simplest reliable protocol for firmware transfer over UART. It divides the file into 128-byte blocks, each with a simple checksum, and provides basic error recovery. Despite its age (1977), it remains practical for factory programming and debug-port updates.
/**
* Simplified XMODEM receive — for firmware update over UART.
* Full XMODEM-CRC (with 16-bit CRC instead of checksum) is recommended
* for production; this shows the core protocol structure.
*
* Protocol:
* Receiver: sends 'C' to request CRC mode (or NAK for checksum mode)
* Sender: replies with 128-byte blocks:
* SOH | block_num | ~block_num | 128 bytes data | CRC16_hi | CRC16_lo
* Receiver: sends ACK after each valid block, NAK for retry, CAN to abort
*/
#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"
#define SOH 0x01u
#define EOT 0x04u
#define ACK 0x06u
#define NAK 0x15u
#define CAN 0x18u
#define XMODEM_BLOCK_SIZE 128u
#define XMODEM_TIMEOUT_MS 3000u
extern UART_HandleTypeDef huart1;
static uint8_t xmodem_buf[XMODEM_BLOCK_SIZE + 5u]; /* SOH+BLK+~BLK+DATA+CRC */
/* CRC16-CCITT */
static uint16_t crc16(const uint8_t *buf, uint32_t len)
{
uint16_t crc = 0u;
for (uint32_t i = 0u; i < len; i++) {
crc ^= (uint16_t)buf[i] << 8u;
for (uint8_t j = 0u; j < 8u; j++) {
crc = (crc & 0x8000u) ? ((crc << 1u) ^ 0x1021u) : (crc << 1u);
}
}
return crc;
}
static void uart_send_byte(uint8_t b)
{
HAL_UART_Transmit(&huart1, &b, 1u, 100u);
}
static int32_t uart_recv_byte(uint8_t *b, uint32_t timeout_ms)
{
return (HAL_UART_Receive(&huart1, b, 1u, timeout_ms) == HAL_OK) ? 0 : -1;
}
/**
* xmodem_receive() — receive a firmware image over UART XMODEM-CRC.
* Writes each verified block to flash at dest_base + (block_num-1)*128.
* Returns total bytes received, or -1 on error.
*/
int32_t xmodem_receive(uint32_t dest_base)
{
uint8_t b = 0u;
uint8_t blk_exp = 1u;
uint32_t total = 0u;
uint32_t retries = 0u;
/* Request CRC mode */
uart_send_byte('C');
for (;;) {
if (uart_recv_byte(&b, XMODEM_TIMEOUT_MS) != 0) {
if (++retries > 10u) return -1;
uart_send_byte('C');
continue;
}
if (b == EOT) {
uart_send_byte(ACK);
return (int32_t)total;
}
if (b != SOH) {
uart_send_byte(NAK);
continue;
}
/* Read rest of block: BLK + ~BLK + 128 data bytes + CRC16 (2 bytes) */
if (HAL_UART_Receive(&huart1, xmodem_buf, 4u + XMODEM_BLOCK_SIZE,
XMODEM_TIMEOUT_MS) != HAL_OK) {
uart_send_byte(NAK);
continue;
}
uint8_t blk = xmodem_buf[0];
uint8_t nblk = xmodem_buf[1];
/* Validate block number */
if ((uint8_t)(blk + nblk) != 0xFFu || blk != blk_exp) {
uart_send_byte(NAK);
continue;
}
/* Validate CRC16 */
uint16_t rx_crc = ((uint16_t)xmodem_buf[2 + XMODEM_BLOCK_SIZE] << 8u)
| xmodem_buf[3 + XMODEM_BLOCK_SIZE];
uint16_t calc_crc = crc16(xmodem_buf + 2u, XMODEM_BLOCK_SIZE);
if (rx_crc != calc_crc) {
uart_send_byte(NAK);
continue;
}
/* Write 128 bytes to flash */
uint32_t dest = dest_base + ((blk_exp - 1u) * XMODEM_BLOCK_SIZE);
if (flash_write_block(dest, xmodem_buf + 2u, XMODEM_BLOCK_SIZE) != 0) {
uart_send_byte(CAN);
return -1;
}
total += XMODEM_BLOCK_SIZE;
blk_exp = (blk_exp == 255u) ? 0u : blk_exp + 1u;
retries = 0u;
uart_send_byte(ACK);
}
}
OTA Transport Comparison
| Transport |
Bandwidth |
Range |
Power |
Security |
Best For |
| UART (XMODEM) |
~10 KB/s @ 115200 |
Direct cable |
Low |
None (add CRC/sign at app layer) |
Factory programming, debug updates |
| USB DFU |
~1–5 MB/s (Full/Hi-Speed) |
5 m (USB cable) |
Medium |
Host-side signing; device verifies |
Consumer devices, field updates via PC |
| BLE (Nordic DFU) |
~50–100 KB/s (BLE 5) |
10–100 m |
Very Low |
Encrypted BLE link + app-level signing |
Wearables, IoT sensors, smart home |
| Wi-Fi (HTTPS) |
1–10 MB/s |
Infrastructure |
High |
TLS + digital signature; strongest |
Smart home hubs, industrial gateways |
| LoRa (FUOTA) |
0.3–5 KB/s |
2–15 km |
Very Low (RX windows) |
Fragmented FUOTA + signing required |
Smart meters, agriculture, remote sensing |
CRC32 & Signature Verification
Hardware CRC32 Using CMSIS
CRC32 provides basic integrity verification — detecting accidental corruption but not intentional tampering. STM32 devices include a hardware CRC peripheral that computes CRC32 in hardware, significantly faster than a software implementation. The firmware image is typically padded with its own CRC32 value appended at a known offset (or in a trailer structure).
/**
* Hardware CRC32 verification using STM32 CRC peripheral.
* STM32 CRC peripheral uses polynomial 0x04C11DB7 (CRC32/MPEG-2).
*
* Image layout:
* [Application code and data — N bytes]
* [CRC32 value — 4 bytes at APP_BASE + APP_SIZE - 4]
*/
#include "stm32h7xx.h"
#include "stm32h7xx_hal_crc.h"
extern CRC_HandleTypeDef hcrc;
#define APP_BASE 0x08040000u
#define APP_SIZE 0x00040000u /* 256 KB application area */
int32_t verify_application_crc(void)
{
/* CRC32 is stored in the last 4 bytes of the application area */
const uint32_t *app_data = (const uint32_t *)APP_BASE;
uint32_t word_count = (APP_SIZE - 4u) / 4u;
uint32_t stored_crc = *(volatile uint32_t *)(APP_BASE + APP_SIZE - 4u);
/* Reset CRC peripheral */
__HAL_RCC_CRC_CLK_ENABLE();
__HAL_CRC_DR_RESET(&hcrc);
/* Compute CRC32 over the application (excluding the stored CRC word) */
uint32_t computed_crc = HAL_CRC_Calculate(&hcrc,
(uint32_t *)app_data,
word_count);
return (computed_crc == stored_crc) ? 0 : -1;
}
ECDSA-P256 Signature Verification with mbedTLS
CRC32 catches accidental corruption but does not protect against malicious firmware. For production products, ECDSA-P256 or Ed25519 signature verification ensures only firmware signed with your private key (kept securely off-device) can run on your hardware.
/**
* Ed25519 signature verification using mbedTLS (standalone, no PSA).
* The firmware image is signed offline:
* openssl genpkey -algorithm ed25519 -out private.pem
* openssl pkey -in private.pem -pubout -out public.pem
* python3 sign_image.py firmware.bin private.pem firmware_signed.bin
*
* Signed image layout:
* [Original firmware — N bytes]
* [Ed25519 signature — 64 bytes appended as TLV or at fixed offset]
*/
#include "mbedtls/eddsa.h"
#include "mbedtls/sha512.h"
/* Ed25519 public key — embedded in bootloader at manufacture */
static const uint8_t boot_public_key[32] = {
/* 32-byte Ed25519 public key — replace with actual key */
0xAB, 0xCD, 0xEF, 0x01, /* ... 28 more bytes ... */
};
int32_t verify_image_signature(const uint8_t *image, uint32_t image_len,
const uint8_t *sig, uint32_t sig_len)
{
if (sig_len != 64u) return -1; /* Ed25519 signature is always 64 bytes */
mbedtls_eddsa_context ctx;
mbedtls_eddsa_init(&ctx);
/* Load the Ed25519 public key */
int rc = mbedtls_eddsa_read_public(&ctx, boot_public_key,
sizeof(boot_public_key));
if (rc != 0) {
mbedtls_eddsa_free(&ctx);
return -2;
}
/* Verify signature over the image (Ed25519 signs the message directly,
* no pre-hashing required — the hash is internal to Ed25519) */
rc = mbedtls_eddsa_verify(&ctx, image, image_len, sig);
mbedtls_eddsa_free(&ctx);
return (rc == 0) ? 0 : -3;
}
MCUboot Integration
MCUboot (originally from the Mynewt RTOS project, now widely adopted) is an open-source, standards-based secure bootloader. It handles the entire update lifecycle: image verification, slot management, A/B swap, rollback, and encrypted images. Rather than building a bootloader from scratch, professional products should seriously consider MCUboot — it has been audited, is PSA-aligned, and is supported by Zephyr, TF-M, NXP MCUXpresso, and ST's STM32Cube ecosystem.
MCUboot Slot Layout & Image Header
/**
* MCUboot image header structure (simplified from bootutil/image.h).
* The imgtool utility (Python) prepends this header and appends TLV metadata
* when signing a firmware image.
*
* Usage:
* pip install imgtool
* imgtool sign \
* --key ed25519.pem \
* --header-size 0x200 \
* --align 8 \
* --version 1.2.0+0 \
* --slot-size 0x80000 \
* firmware.bin firmware_signed.bin
*/
#include <stdint.h>
#define IMAGE_MAGIC 0x96F3B83Du
typedef struct {
uint32_t ih_magic; /* IMAGE_MAGIC */
uint32_t ih_load_addr; /* load address (0 = slot-relative) */
uint16_t ih_hdr_size; /* size of this header (must be power of 2) */
uint16_t ih_protect_tlv_size; /* size of protected TLV area */
uint32_t ih_img_size; /* image body size in bytes */
uint32_t ih_flags; /* IMAGE_F_* flags */
struct {
uint8_t iv_major;
uint8_t iv_minor;
uint16_t iv_revision;
uint32_t iv_build_num;
} ih_ver; /* image version */
uint32_t _pad1;
} image_header_t;
/* MCUboot slot layout — configured in mcuboot/boot/zephyr/mcuboot.conf:
*
* Slot layout for STM32H743 (2 MB flash):
* 0x08000000 – 0x0801FFFF MCUboot bootloader (128 KB)
* 0x08020000 – 0x0809FFFF Primary slot (Slot 0) (512 KB) — active image
* 0x080A0000 – 0x0811FFFF Secondary slot (Slot 1) (512 KB) — update image
* 0x08120000 – 0x0813FFFF Scratch area (128 KB) — swap scratch
* 0x08140000 – 0x081FFFFF Application data / KVS (768 KB)
*/
MCUboot Configuration Essentials
# mcuboot/boot/zephyr/prj.conf (Zephyr-based MCUboot configuration)
# Or equivalent KConfig for other platforms
# Enable Ed25519 signature verification
CONFIG_BOOT_SIGNATURE_TYPE_ED25519=y
# Enable encrypted images (optional — adds AES-128 CTR encryption)
# CONFIG_BOOT_ENCRYPT_EC256=y
# Swap mode: overwrite (fast, no rollback) or swap-move (fail-safe rollback)
CONFIG_BOOT_SWAP_USING_MOVE=y
# Anti-rollback protection using security counter in OTP
CONFIG_MCUBOOT_DOWNGRADE_PREVENTION=y
CONFIG_MCUBOOT_HW_ROLLBACK_PROT=y
# Enable serial recovery (UART update port) as fallback
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_UART=y
# Flash driver configuration
CONFIG_FLASH=y
CONFIG_BOOT_MAX_IMG_SECTORS=256
MCUboot vs Custom Bootloader: For products that will receive OTA updates in the field, MCUboot's swap-move mode provides atomic updates with full rollback. The swap mechanism ensures that if power is lost during a flash write, the device will boot the previous firmware on next reset — not an unbootable state. Building this level of robustness from scratch requires significant testing and is error-prone.
Exercises
Exercise 1
Intermediate
UART Firmware Updater with CRC32 Check
Implement a complete bootloader that receives a firmware image over UART using the XMODEM-CRC protocol, writes it to flash at the application offset, verifies the CRC32 using the hardware CRC peripheral, and jumps to the application if valid. Test by: (1) sending a valid signed image and confirming the application boots, (2) deliberately corrupting a byte in the image and confirming the bootloader detects the error and does not jump.
XMODEM
CRC32
Flash Write
Bootloader Jump
Exercise 2
Intermediate
Dual-Bank Swap with Rollback on Boot Failure
On an STM32H743 (or similar dual-bank MCU), implement a complete A/B update system: (1) the bootloader reads a boot-try counter from RTC backup registers, (2) the application must call firmware_confirm() within 5 seconds or the bootloader rolls back on the next reset, (3) simulate a buggy firmware update by programming an image that never calls firmware_confirm(), and confirm the bootloader rolls back to the previous good image after three failed attempts.
Dual-Bank Flash
Bank Swap
Rollback
RTC Backup Registers
Exercise 3
Advanced
Ed25519 Signature Verification with mbedTLS
Extend your bootloader to verify an Ed25519 signature appended to the firmware image: (1) generate a key pair using OpenSSL, (2) create a signing script using Python's cryptography library that appends the signature as a 64-byte TLV at the end of the binary, (3) embed the public key in the bootloader, (4) use mbedTLS to verify the signature in the bootloader before accepting any firmware update, (5) confirm that a firmware image signed with a different private key is rejected.
Ed25519
mbedTLS
Code Signing
imgtool
Bootloader Design Planner
Use this tool to document your bootloader architecture — flash bank layout, OTA transport, verification strategy, and fail-safe design. Download as Word, Excel, PDF, or PPTX for design review documentation.
Conclusion & Next Steps
A professional bootloader is one of the most consequential pieces of firmware you will ever write. In this part we covered the complete picture:
- Bootloader fundamentals: the three responsibilities of a bootloader, the update flag pattern using RTC backup registers, and the decision table for single-bank vs dual-bank vs MCUboot strategies.
- Application offset and VTOR: correctly linking the application at its flash offset, the VTOR relocation sequence, and the step-by-step jump procedure (disable interrupts, set MSP, set VTOR, branch).
- Dual-bank flash on STM32H7: programming Bank B while executing from Bank A, the OPTKEY bank-swap procedure, and the boot-try counter rollback mechanism.
- OTA transports: UART XMODEM-CRC implementation with CRC16 validation, and the transport comparison table covering UART, USB DFU, BLE, Wi-Fi, and LoRa.
- Signature verification: hardware CRC32 for integrity checking, and Ed25519 verification using mbedTLS for cryptographic authenticity.
- MCUboot: slot layout, image header structure, imgtool signing, and the key configuration choices (swap-move, anti-rollback, serial recovery).
Next in the Series
In Part 17: Testing & Validation, we'll bring rigour to embedded development — unit testing with Unity and Ceedling, hardware-in-the-loop (HIL) testing frameworks, mocking hardware peripherals for off-target testing, integration test strategies, and code coverage measurement for CMSIS-based firmware.
Related Articles in This Series
Part 15: Security — ARMv8-M & TrustZone
Secure boot (Part 15) and the bootloader (Part 16) are tightly coupled — the TrustZone secure world verifies the NS application before the bootloader jumps to it in a full PSA-compliant system.
Read Article
Part 3: Startup Code, Linker Scripts & Vector Table
The jump-to-application sequence depends entirely on the concepts from Part 3 — vector table structure, VTOR, MSP initialisation, and the Reset_Handler sequence.
Read Article
Part 17: Testing & Validation
Bootloader code is among the hardest firmware to test — Part 17 covers HIL testing frameworks and strategies for validating the update lifecycle, rollback, and signature verification without requiring hardware resets.
Read Article