Domain Deep-Dive
ExpertBootloader 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_0000Linker 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