Bootloaders and Firmware Updates: Memory Map, CRC Check and Jump
Once a product leaves the factory, a debug probe is no longer an option for loading new firmware. Bugs still need fixing and features still need adding, so the device must be able to update itself over UART, USB, CAN or a wireless link. The code that makes this possible is the bootloader: a small program that runs first after every reset, decides whether a valid application exists, optionally receives a new one, and then hands control to it.
This article covers flash partitioning, image validation with CRC-32, how a Cortex-M bootloader jumps to the application, and the update strategies that keep a device from being bricked by a power cut halfway through an update.
Built-In ROM Bootloaders vs Custom Bootloaders
Many microcontrollers ship with a bootloader in mask ROM. STM32 parts have a system memory bootloader entered through the BOOT0 pin that accepts firmware over UART and, on many parts, USB DFU. The RP2040 has a ROM bootloader that appears as a USB mass-storage drive. AVR boards such as the Arduino Uno use a small bootloader stored in a reserved flash section.
ROM bootloaders are excellent for production programming and recovery, but they typically need a pin held during reset and accept any image. A custom bootloader in your own flash can check signatures, update over your product's own link, keep two images, and roll back. That is what the rest of this article describes.
Partitioning the Flash
The bootloader and the application are separate programs, each built and linked for its own region of flash. The example below assumes an STM32F4 with flash at 0x08000000, where the first 16 KB sectors are small and convenient for a bootloader. Sizes and addresses are illustrative; match them to your part's sector layout, because flash can only be erased in whole sectors or pages.
0x08000000 +---------------------------+
| Bootloader (32 KB) | never updated in the field
0x08008000 +---------------------------+
| Image header (512 bytes) | magic, version, size, CRC
| Application vector table |
| Application code |
+---------------------------+
| Download / slot B area | new image is written here first
+---------------------------+
| Settings / boot flags | update requested, boot attempts
+---------------------------+The application's linker script must start flash at its new origin, and its vector table must be relocated there. On Cortex-M3 and above this is done by writing the Vector Table Offset Register, SCB->VTOR. Check the application's SystemInit too, because some vendor templates overwrite VTOR with the default flash base.
Validating an Image with a Header and CRC-32
Before jumping, the bootloader must be sure the application is complete and intact. A simple, effective scheme puts a small header in front of the image with a magic number, a version, the image length and a CRC-32 of the image. The program below runs on any PC: it implements the standard reflected CRC-32 (polynomial 0xEDB88320, as used by Ethernet and zip), checks it against the published check value for the string 123456789, and validates a fake image.
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#define IMG_MAGIC 0x464D5741UL /* arbitrary marker chosen for this project */
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t size; /* bytes of application following the header */
uint32_t crc32; /* CRC-32 of those bytes */
} img_header_t;
/* Bitwise CRC-32 (IEEE 802.3, reflected). Small code, no table needed. */
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t len)
{
crc = ~crc;
while (len--) {
crc ^= *data++;
for (int bit = 0; bit < 8; bit++) {
crc = (crc >> 1) ^ (0xEDB88320UL & (0UL - (crc & 1UL)));
}
}
return ~crc;
}
static int image_is_valid(const img_header_t *h, const uint8_t *body, uint32_t max_size)
{
if (h->magic != IMG_MAGIC) return 0;
if (h->size == 0 || h->size > max_size) return 0;
return crc32_update(0, body, h->size) == h->crc32;
}
int main(void)
{
const char *check = "123456789";
uint32_t c = crc32_update(0, (const uint8_t *)check, strlen(check));
printf("CRC-32 check value: 0x%08lX (expected 0xCBF43926)\n", (unsigned long)c);
uint8_t app[64];
for (unsigned i = 0; i < sizeof app; i++) app[i] = (uint8_t)(i * 7u);
img_header_t hdr = { IMG_MAGIC, 3, sizeof app, 0 };
hdr.crc32 = crc32_update(0, app, sizeof app); /* done by the build tool */
printf("intact image : %s\n", image_is_valid(&hdr, app, 1024) ? "valid" : "INVALID");
app[10] ^= 0x01; /* one flipped bit */
printf("corrupt image: %s\n", image_is_valid(&hdr, app, 1024) ? "valid" : "INVALID");
return 0;
}
CRC-32 check value: 0xCBF43926 (expected 0xCBF43926) intact image : valid corrupt image: INVALID
A CRC detects accidental corruption such as an interrupted transfer, but it is not security: anyone can compute a matching CRC for modified firmware. Products that must reject unauthorised firmware verify a digital signature, for example ECDSA, over a hash of the image, with only the public key stored in the bootloader.
Jumping to the Application on Cortex-M
The first word of a Cortex-M vector table is the initial stack pointer and the second is the address of the reset handler. To start the application, the bootloader cleans up its own use of the hardware, points VTOR at the application's table, loads the stack pointer and calls the reset handler. The code is MCU-specific and uses CMSIS names.
#include <stdint.h>
#include "stm32f4xx.h"
#define APP_VECTORS 0x08008200UL /* after an image header padded to 512 bytes */
typedef void (*entry_t)(void);
void jump_to_application(void)
{
uint32_t app_sp = *(volatile uint32_t *)(APP_VECTORS + 0);
uint32_t app_reset = *(volatile uint32_t *)(APP_VECTORS + 4);
/* Sanity check: the stack pointer must point into SRAM */
if ((app_sp & 0xFF000000UL) != 0x20000000UL) {
return; /* stay in the bootloader */
}
__disable_irq();
SysTick->CTRL = 0; /* stop the bootloader's tick */
for (unsigned i = 0; i < sizeof NVIC->ICER / sizeof NVIC->ICER[0]; i++) {
NVIC->ICER[i] = 0xFFFFFFFFUL; /* disable all interrupts */
NVIC->ICPR[i] = 0xFFFFFFFFUL; /* clear pending interrupts */
}
/* also de-initialise any peripherals and clocks the bootloader used */
SCB->VTOR = APP_VECTORS; /* table base must meet VTOR alignment rules */
__DSB();
__ISB();
__set_MSP(app_sp);
__enable_irq();
((entry_t)app_reset)(); /* does not return */
}
Why 512 bytes of header? VTOR requires the vector table address to be aligned to a power of two at least as large as the table itself. An STM32F4 vector table with around a hundred entries is a few hundred bytes long, so it needs 512-byte alignment. Padding the 16-byte header to 512 bytes keeps the table correctly aligned; the application's linker script then starts its flash region at 0x08008200. Check the vector count for your exact part before choosing the padding.
Making Updates Power-Fail Safe
- Never erase the only good image first. Download the new image into a second slot, verify it, and only then copy or switch.
- A/B slots: keep two complete application slots and a flag saying which is active. Dual-bank flash on some MCUs can swap banks in hardware.
- Trial boot and rollback: mark the new image as on trial, let the application confirm itself after a successful start, and roll back if the watchdog resets it before confirmation.
- Version checks: refuse older versions unless explicitly allowed, to prevent downgrade to a firmware with known vulnerabilities.
- Keep the bootloader minimal and never update it in the field unless the hardware offers a safe way to do so.
Summary
A bootloader owns the start of flash, validates the application with a header and CRC-32, and jumps to it by relocating the vector table, loading the stack pointer and calling the reset handler. Updates are written to a separate slot, verified and then activated, with a trial-boot and rollback mechanism so a power cut or a bad release never leaves the device without working firmware. Add signature checking when the product must refuse unauthorised code.