Embedded Bootloader Design Guide
Flash Layout, Vector Table Relocation, DFU over UART, CRC Validation & Robust Update
1. Introduction — The Code That Decides How the Code Starts
An embedded bootloader is the small program that runs first at reset: it initializes the bare essentials, decides whether to (a) start the application, (b) enter a firmware-update (DFU) mode, or (c) fall back to a recovery path when the application is corrupt. Every device that ships firmware in the field — a battery sensor, a motor controller, an STM32 board — does a firmware update at some point, and a robust bootloader is what makes the update safe and failure-recoverable. This guide covers the flash memory layout and the linker map, vector-table relocation, the reset sequence and how the bootloader hands over to the app, the update protocol over UART (or I2C/SPI), CRC/checksum validation and the A/B (slot) strategy, the DFU wait/trigger window with timeouts, watchdog integration, and the failure modes an OTA design must handle. It complements the state machine and low-power MCU guides on this site.
2. Flash Layout and the Memory Map
The classic scheme splits the flash into two regions: the bootloader at the lowest addresses (e.g. 0x0800_0000, size 16–32 KB) and the application at a fixed offset (e.g. 0x0800_8000). The application must then be linked at its own offset, and the CPU’s reset vector must be pointed at the bootloader’s start. For STM32, the most common mechanism is the dedicated boot pins / BOOT0 (system bootloader), but for custom updates you implement your own in the user bootloader region. The layout (example on a 512 KB STM32):
| Region | Address | Size | Content |
|---|---|---|---|
| Bootloader | 0x0800_0000 | 32 KB | Reset vector, DFU code |
| Application slot A | 0x0800_8000 | 224 KB | Active firmware |
| Application slot B | 0x0804_0000 | 224 KB | Staged/pending firmware |
| Metadata / config | 0x0807_E000 | 4 KB | Firmware version, CRC, slot flags |
The linker script (or the OTA framework) must place the application at its slot base and generate a binary whose reset vector handles a relocated stack/Main pointers. The bootloader region must be reserved with the erase-protection flags where supported, so a runaway app cannot erase itself away from recovery.
3. Vector Table Relocation
When the application runs at 0x0800_8000, every interrupt causes the CPU to read the vector table. Two mechanisms set where the table lives:
- VTOR (Vector Table Offset Register) — available on Cortex-M3/M4 (and up) parts; set
SCB->VTOR = 0x08008000in the app’s startup before enabling any interrupt. - Remap via SYSCFG (e.g. SYSCFG_MEMRMP on STM32F1/Cortex-M3 without VTOR) — map the flash sector to the base address.
Failing to relocate the vector table is the classic “app runs but any interrupt crashes the MCU” bug — the core jumps into the bootloader’s vectors, undefined behavior ensues. The relocated table also needs the app’s own stack pointer (initial SP) and reset handler in the first two words of the slot. After relocation, the app’s SysTick and NVIC priorities must be re-armed by the app itself, not assumed from the bootloader’s state.
4. The Boot Sequence and Hand-Off
- Reset: the bootloader runs first, initializes the minimum (clock, a GPIO/UART for the DFU trigger, the watchdog).
- Decision: check the DFU trigger (a magic number in backup RAM, a button held, or a host command within the wait window) and validate the application image (CRC/version).
- Normal boot: if the app is valid and no update requested, set VTOR to the app, load the app’s initial SP, and jump to the app’s reset handler (with interrupts still disabled and the sysclk at a known state the app expects).
- Update mode: if triggered or the app is invalid, enter DFU: wait on the UART for a frame, compute the CRC of the received image, then erase/write the target slot.
- Fallback: on CRC failure or an update aborted mid-flash, stay in DFU (or boot the previous slot in an A/B scheme) — never brick the device by booting a half-written app.
The hand-off detail that costs hours in the field: at the jump, the core’s stack pointer and vector table must be exactly what the app expects, the interrupts must be disabled during the transition (a pending interrupt before the app re-arms its own NVIC causes a crash), and the watchdog must be re-fed by the app from its first line — otherwise the app “resets instantly” in a watchdog loop.
5. Update Protocol and CRC Validation
A minimal over-UART update exchange (host → bootloader):
Host: SYNC 0xAB 0xCD
Boot: ACK or ERROR
Host: DATA [addr32][len16][payload...] (chunked, e.g. 1 KB)
Boot: ACK per chunk, updates CRC incrementally
Host: CRC [crc32] (over the whole image)
Boot: verify CRC == computed; erase+write already staged in RAM?
-> ACK "valid", then commit (mark slot flag)
Boot: reset
Two design choices matter. First, chunked ACK vs fire-and-forget: with ACK per chunk the link is self-clocking and the host can retransmit; without it a dropped byte corrupts the whole image silently. Second, stage-then-commit: write the new image into the inactive slot (B) while keeping the running slot (A), then flip a metadata flag, so a power loss during the flash leaves the old app intact — the A/B or dual-slot strategy. The CRC check is done on the full image before switching the active flag; only a validated image becomes bootable. Treat the CRC polynomial and the metadata layout as a versioned contract between the bootloader and the update tool.
6. Watchdog, Timeouts and the DFU Wait Window
A bootloader with no timeouts is a failure: if the device waits forever in DFU on a silent link, the field device never starts. Design the waits explicitly:
- DFU wait window: on reset, wait a bounded time (e.g. 1–2 s) for the update host to send SYNC; if nothing, boot the app. A button or a magic value gives an unbounded “force DFU” path for recovery.
- Inter-chunk timeout: each expected DATA/CRC frame has a watchdog or a timer; a missed frame resets the update state and restarts the handshake, not a dead wait.
- Watchdog integration: kick the IWDG/WWDG in the bootloader loop only while making progress; a corrupt/no-watchdog-fed app is caught by the watchdog reset bouncing back into the bootloader, which then validates and falls back.
The watchdog is the safety net that turns a hung app into a recoverable reset; pairing it with the bootloader’s validation gives the “brick-proof” self-healing behavior. When the bootloader idles in the wait window, the power cost of that window matters on battery devices — the low-power estimator quantifies the added average current of a 2-second awake window at every reset, which is why the wait window should be short and the default path boot immediately.
7. Worked Example — STM32F4 2-Slot Bootloader with UART DFU
Target: add a 32 KB bootloader to a 1 MB STM32F4 with a 384 KB slot A and slot B, update over UART at 921600 baud, recover-from-corruption safe.
- Linker: app linked at 0x0806_0000 (slot A); bootloader at 0x0800_0000; the app’s linker script reserves the bootloader + metadata.
- Vector table: in the app’s SystemInit, set
SCB->VTOR = 0x08060000before any IRQ is enabled. - DFU: at 921600 baud (8N1), the 384 KB image = 3145728 bytes = ~3.4 s raw at 11.5 kbyte/s.* Actually 921600 baud ≈ 90 kbyte/s → ~35 s for the image; chunk at 1 KB with per-chunk ACK keeps the link reliable and lets the host pace the flash-erase time.
- Validation: CRC32 over the staged image in RAM buffer chunks; only after the last chunk’s CRC matches is the slot flag flipped. If a power loss lands mid-write, the flag still points at slot A and the bootloader boots it.
- Watchdog: IWDG ~4 s timeout, kicked in the bootloader’s main loop; on the jump to the app, the app must kick it within its first cycle (confirm with a scope on the reset pin: no infinite reset loop).
- Baud/clock: compute the USART baud from the running PLL clock (see the clock calculator and the UART-DMA guide); a mismatch at 921600 corrupts the header bytes instantly.
- Battery note: on the sensor variant, the 2-second DFU window at every cold boot adds measurable average current — run the low-power estimator with the wake current and time to size the battery, then shorten the window or use the button path only.
The same STM32 peripheral knowledge chain applies as in the rest of the site: the STM32 clock configuration calculator and the UART+DMA guide complete the design if the bootloader uses DMA-assisted RX for the transfer.
8. Common Mistakes
- Forgotten vector-table relocation: the app boots but every interrupt hits the wrong vector — the top “worked in flash, crashed on interrupt” mystery.
- Booting a partially written image: a power cut during the flash leaves a corrupt app that the bootloader happily jumps into — validate before flipping any flag.
- No DFU timeout: a silent field device that waits forever for a host that left.
- Interrupts enabled at the jump: a pending IRQ fires with the bootloader’s vector table or the app not initialized — disable interrupts, set VTOR, then jump with a clean NVIC.
- Same linker base for app and bootloader: two images compiled for 0x0800_0000 stepping on each other.
- Trusting a single write without CRC: flash wear, marginal supply or a glitch can corrupt a “successful” chunk — always end the transfer with a full-image check.
9. FAQ
Q: Why do I need a separate bootloader if the STM32 has a ROM system bootloader? A: The ROM bootloader updates only the whole flash at fixed pins/interface and cannot implement your custom protocol, your slot strategy, your CRC, or your field-update security — a user bootloader is the standard for robust, feature-controlled OTA.
Q: What is the difference between single-slot and A/B? A: Single-slot overwrites the running app (a bad update bricks until recovery); A/B writes into a second slot and flips the active flag after validation — a power loss never corrupts the bootable image.
Q: How do I make a field device “brick-proof”? A: Validate the image before commit, keep the previous slot, add a watchdog that bounces into the bootloader, and keep force-DFU (button/magic) reachable — then no single point erases recovery.
Q: Can the bootloader be too big? A: It should be small enough to leave the app room and to be immune to later app changes; 16–64 KB is typical, linked at a fixed, never-moved address so old tools still flash it.
10. Conclusion
The bootloader is the contract between the physical flash, the field-update protocol and the running application. Lay out the slots, relocate the vector table, validate before commit with CRC, time out every wait, and let the watchdog turn hangs into recovery. When the design is right, a field update is a routine event: the device stages, validates, flips the slot, and boots — and when it goes wrong, it falls back instead of bricking. That is the difference between firmware that ships and firmware that ships safely.