Skip to content
C++
Domain Deep-Dive
Expert

Bootloader Design in Embedded C++

Writing a minimal bootloader in C++ for Cortex-M: memory validation, firmware update over UART/SPI/CAN, jump to application, and security considerations.

Bootloader layout in FLASH

cpp
FLASH (256 KB example):
┌────────────────────┐ 0x0800_0000  ← vectors + reset vector points here
│    Bootloader      │  16 KB
│    (sector 0)      │
├────────────────────┤ 0x0800_4000
│    Application     │  224 KB
│    (sectors 1-7)   │
├────────────────────┤ 0x0803_C000
│    Update buffer   │  16 KB (optional: dual-bank update)
└────────────────────┘ 0x0804_0000

Linker script for the bootloader:

MEMORY {
    FLASH (rx): ORIGIN = 0x08000000, LENGTH = 16K
    RAM   (rw): ORIGIN = 0x20000000, LENGTH = 128K
}

Linker script for the application:

MEMORY {
    FLASH (rx): ORIGIN = 0x08004000, LENGTH = 224K  /* offset by 16K */
    RAM   (rw): ORIGIN = 0x20000000, LENGTH = 128K
}

Bootloader decision logic

cpp
static constexpr uint32_t APP_START = 0x08004000;
static constexpr uint32_t MAGIC_ADDR = 0x20000000; // First word of RAM
static constexpr uint32_t UPDATE_MAGIC = 0xDEADBEEF;

struct AppHeader {
    uint32_t magic;    // Application magic (e.g., 0xC0DEBEEF)
    uint32_t size;     // Application size in bytes
    uint32_t crc32;    // CRC32 of application
    uint32_t version;  // Firmware version
};

bool IsAppValid() {
    auto* header = reinterpret_cast<const AppHeader*>(APP_START);
    if (header->magic != 0xC0DEBEEF) return false;
    if (header->size == 0 || header->size > 224 * 1024) return false;

    // Verify CRC32 of the application
    uint32_t computed = ComputeCrc32(
        reinterpret_cast<const uint8_t*>(APP_START + sizeof(AppHeader)),
        header->size - sizeof(AppHeader));
    return computed == header->crc32;
}

bool UpdateRequested() {
    // Check RAM magic (set by app before reset to trigger update)
    return *reinterpret_cast<volatile uint32_t*>(MAGIC_ADDR) == UPDATE_MAGIC;
}

extern "C" void Reset_Handler() {
    InitClocks();
    InitUart();

    if (UpdateRequested() || !IsAppValid()) {
        // Clear the magic
        *reinterpret_cast<volatile uint32_t*>(MAGIC_ADDR) = 0;
        RunFirmwareUpdate();  // Wait for new firmware over UART/CAN/SPI
    }

    // Jump to application
    JumpToApplication(APP_START);
}

Jumping to the application

cpp
void JumpToApplication(uint32_t app_address) {
    // 1. Disable all interrupts
    __disable_irq();

    // 2. Disable SysTick (if used in bootloader)
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL  = 0;

    // 3. Clear pending interrupts
    for (int i = 0; i < 8; ++i) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    // 4. Reset the RCC (clock config) if bootloader changed clocks
    RCC->CR |= RCC_CR_HSION;
    while (!(RCC->CR & RCC_CR_HSIRDY)) {}
    RCC->CFGR = 0;

    // 5. Relocate the vector table
    SCB->VTOR = app_address;

    // 6. Set stack pointer from app vector table
    uint32_t app_sp = *reinterpret_cast<uint32_t*>(app_address);
    __set_MSP(app_sp);

    // 7. Jump to app reset handler (second word of vector table)
    uint32_t app_reset = *reinterpret_cast<uint32_t*>(app_address + 4);
    void (*reset_fn)() = reinterpret_cast<void(*)()>(app_reset);

    __enable_irq();
    reset_fn();

    // Should never reach here
    while (true) {}
}

Firmware update over UART (XMODEM)

XMODEM is the simplest reliable binary transfer protocol:

cpp
static constexpr uint8_t SOH = 0x01;  // Start of 128-byte block
static constexpr uint8_t EOT = 0x04;  // End of transmission
static constexpr uint8_t ACK = 0x06;
static constexpr uint8_t NAK = 0x15;
static constexpr uint8_t CAN = 0x18;

bool XmodemReceive(uint32_t flash_dest, size_t max_size) {
    uint8_t block_num = 1;
    size_t total = 0;

    UartSend(NAK); // Prompt sender to start

    while (total < max_size) {
        uint8_t ctrl = UartReceive(3000); // 3s timeout

        if (ctrl == EOT) {
            UartSend(ACK);
            return true; // Success
        }

        if (ctrl != SOH) { UartSend(CAN); return false; }

        uint8_t blk     = UartReceive(1000);
        uint8_t blk_inv = UartReceive(1000);

        if (blk + blk_inv != 0xFF) { UartSend(NAK); continue; }

        uint8_t data[128];
        for (int i = 0; i < 128; ++i) data[i] = UartReceive(1000);
        uint8_t checksum = UartReceive(1000);

        // Verify checksum
        uint8_t sum = 0;
        for (uint8_t b : data) sum += b;
        if (sum != checksum) { UartSend(NAK); continue; }

        // Write to FLASH
        if (blk == block_num) {
            FlashWrite(flash_dest + total, data, 128);
            total += 128;
            block_num++;
        }

        UartSend(ACK);
    }
    return false;
}

Flash write and erase

FLASH on STM32 requires: unlock → erase sector → program → lock:

cpp
void FlashUnlock() {
    FLASH->KEYR = 0x45670123; // Key 1
    FLASH->KEYR = 0xCDEF89AB; // Key 2
}

void FlashLock() {
    FLASH->CR |= FLASH_CR_LOCK;
}

void FlashEraseSector(uint8_t sector) {
    while (FLASH->SR & FLASH_SR_BSY) {} // Wait not busy
    FLASH->CR &= ~FLASH_CR_SNB_Msk;
    FLASH->CR |= (sector << FLASH_CR_SNB_Pos) | FLASH_CR_SER;
    FLASH->CR |= FLASH_CR_STRT;
    while (FLASH->SR & FLASH_SR_BSY) {} // Wait erase done
    FLASH->CR &= ~FLASH_CR_SER;
}

void FlashWrite(uint32_t addr, const uint8_t* data, size_t len) {
    // Program byte by byte (or word-by-word for speed)
    FLASH->CR |= FLASH_CR_PG | (0b00 << FLASH_CR_PSIZE_Pos); // byte width
    for (size_t i = 0; i < len; ++i) {
        *reinterpret_cast<volatile uint8_t*>(addr + i) = data[i];
        while (FLASH->SR & FLASH_SR_BSY) {}
    }
    FLASH->CR &= ~FLASH_CR_PG;
}

Security: firmware signing

For production, verify a digital signature before jumping to app:

cpp
// Minimal Ed25519 verify (using a library like libsodium or micro-ecc)
bool VerifyFirmwareSignature(const AppHeader* header) {
    // Public key baked into bootloader (can't be changed without re-flashing bootloader)
    static constexpr uint8_t PUBLIC_KEY[32] = { /* ... embed your public key */ };

    const uint8_t* signature = reinterpret_cast<const uint8_t*>(header) + sizeof(AppHeader);
    const uint8_t* firmware  = signature + 64;
    size_t fw_len = header->size - sizeof(AppHeader) - 64;

    return ed25519_verify(signature, firmware, fw_len, PUBLIC_KEY) == 0;
}

Build chain: sign firmware with private key during CI → bootloader verifies with embedded public key → prevents unauthorized firmware from running.

Edit on GitHubUpdated 2026-05-24T00:00:00.000Z