Firmware OTA Update Design Guide
A/B Partitions, Bootloader Handoff, Integrity, Power-Fail Safety and Rollback
1. Introduction — An Update That Cannot Brick the Device
Over-the-air firmware update is the feature that separates a product from a prototype: it lets you fix a bug, add a feature or patch a security hole in devices you will never touch. It is also the feature that can render every deployed unit useless if a write is interrupted, a hash is wrong or a rollback is unreachable. The core design question is not “how do I transfer the image” but “how do I guarantee that at every instant, including the instant the power fails, there is at least one complete, bootable image on the device”. Answering that question drives the partition scheme, the bootloader’s obligations, the integrity checks, the metadata format and the update state machine. This guide covers the image layout and A/B partitions, the bootloader’s responsibilities and the handoff to the application, integrity and authenticity (CRC, hash, signature), the update state machine with power-fail safety, rollback and anti-rollback version handling, the transport layer’s requirements on a constrained device, and a complete worked example of a 512 kB dual-slot update with resume. It complements the embedded bootloader and memory management guides on this site.
2. Partition and Image Layout
The two dominant schemes are single-slot with an external staging area and dual-slot (A/B). In the dual-slot scheme the flash is divided into a bootloader region (never updated, or updated only through a very careful separate procedure), slot A, slot B and a small metadata area. The device always boots from the “active” slot; an update is written into the inactive slot; a single metadata word (written atomically, with a valid flag and a CRC) marks which slot is active and whether the newly written slot is verified. This gives the fundamental property that the active image is never modified during an update, so a power failure at any point leaves a bootable system. The single-slot scheme is cheaper in flash but requires the update to overwrite the running image, which is only safe if the bootloader can receive the whole image into a staging area (internal or external flash, or RAM for small images) before erasing the application. Layout arithmetic:
slot_size ≥ image_size + header; total_flash ≥ bootloader + 2·slot_size + metadata + (config, calibration, logs)
The upload must also be usable by the bootloader, which typically cannot write compressed images without an inflater and cannot use the application’s filesystem code; the image should therefore be delivered as a raw binary (or a format with a trivial, bootloader-contained decoder such as a simple RLE), not as (say) an ElF with sections or a tar archive. The slot sizes and the resulting usable application space must be computed from the linker script of each slot; build both slot configurations and check the map file rather than assuming the numbers from the datasheet.
3. The Bootloader’s Responsibilities
The bootloader is the trusted root of the update system, so it must be small, simple and correct rather than feature-rich. Its obligations:
| Responsibility | Why | Design consequence |
|---|---|---|
| Select the boot slot | The metadata decides which image runs | Metadata must be readable and valid before any jump |
| Verify integrity before jumping | Never run a half-written image | CRC/hash check on every boot, or a verified flag |
| Verify authenticity | Prevent malicious images | Signature check with a key stored in write-protected memory |
| Provide a recovery path | Never brick on a bad image | Fall back to the other slot or to a minimal serial/USB loader |
| Manage the update protocol | The application cannot erase its own slot | Bootloader owns the erase/write of the inactive slot |
| Jump with a correct environment | The application expects reset state | Disable interrupts, de-init peripherals, set VTOR/SP |
The handoff is where many designs fail. The application must be linked for its slot’s base address (the vector table offset), and the bootloader must set the vector table offset register (VTOR on Cortex-M) and the stack pointer before jumping. The bootloader must also disable all interrupts and de-initialize (or not initialize at all) the peripherals the application will reconfigure, and it must not leave the flash in a state (write-protected, half-erased) that the application cannot recover from. On Cortex-M the jump is: read the initial SP and reset vector from the image’s first two words, set MSP, set VTOR, then branch.
4. Integrity and Authenticity
Three levels are commonly confused: (1) a checksum or CRC detects accidental corruption (a bad transfer, a flash failure) but not tampering; (2) a cryptographic hash (SHA-256) detects both accidental corruption and casual tampering, but an attacker who can modify the image can also modify the stored hash if it lives in the same unprotected flash; (3) a digital signature (ECDSA/RSA over the hash) proves authenticity, because the private key never reaches the device. A production update system needs at least (2) for transport validation and (3) for accept-and-boot. Practical points: verify the signature in the bootloader (so a compromised application cannot bypass it); store the public key or its hash in a write-protected flash region; verify the whole image before marking it valid, and mark it valid only through an atomic write to the metadata; keep the hash/signature in the image header so that the header’s own CRC must also pass. If the device has a hardware cryptographic engine or a secure element, use it — software signature verification on a small MCU is possible but adds code size and boot time. A useful staging rule: the transport (application) verifies the image’s CRC and hash before releasing it to the bootloader; the bootloader verifies the signature before booting it. Two independent checks, at two different privilege levels.
5. Power-Fail Safety and the Update State Machine
The state machine is the heart of a safe update. States: IDLE → DOWNLOADING (image written to the inactive slot, resumable) → VERIFIED (CRC/hash/signature pass) → PENDING (metadata marks the inactive slot as candidate, reboot requested) → TRIAL (bootloader boots the new slot and starts a watchdog/”commit” timer) → CONFIRMED (application confirms, metadata marks it permanent) or → ROLLBACK (the trial fails or never confirms, the bootloader reverts to the old slot). The atomic primitives that make this safe: (a) never erase the active slot; (b) write the metadata through a two-phase commit — write the new value plus a CRC into a spare slot, then flip a single pointer/valid word so the change is atomic; (c) use a monotonic counter or a version number stored in a separate, append-only region so an attacker cannot roll back to a vulnerable version (anti-rollback); (d) resume the download from a byte offset stored in the metadata (or recompute it from the received length) so a power failure during download does not restart the whole transfer. The trial/confirm mechanism protects against a logically bad but integrity-valid image (one that crashes immediately): the application must explicitly confirm after it has run long enough to prove itself, otherwise the bootloader reverts. That single mechanism converts “a bad update bricks the fleet” into “a bad update costs one reboot”.
6. Transport on a Constrained Device
The transport must tolerate lossy links, be resumable, and fit the device’s RAM. Practical designs: a chunked protocol with an offset (each chunk carries the target offset and a CRC; the receiver acknowledges the last contiguous written offset), a fixed maximum chunk size (e.g. 1–4 kB) so the RAM buffer is bounded, a window size of 1 for the simplest implementation or a small sliding window for throughput, and a session identifier so that a reconnecting device resumes the right transfer. For cellular or Wi-Fi links add a timeout/retry policy at the chunk level rather than restarting the session, and keep a server-side record of the firmware version and the device’s last acknowledged offset. For very small devices with no network stack, the same state machine is driven over UART/CAN/USB with the host acting as the server. Whatever the transport, the download writes into the inactive slot through the bootloader’s (or application’s) flash driver with the address arithmetic validated at every chunk — a single address overflow that writes into the bootloader region is unrecoverable in the field. On a battery-powered device the download is usually the highest-power event in the product’s life, so the energy cost of the transfer must be budgeted against the battery: estimate the average current during the modem session and the session duration, and compare it with the pack’s usable capacity using the low power estimator — an update that drains a low battery to the brown-out point mid-write is a power-fail case that the design must survive.
7. Code Snippet — Atomic Metadata and the Boot Decision
#define MAGIC 0x4F544131u /* "OTA1" */
typedef struct {
uint32_t magic;
uint32_t seq; /* monotonic, for anti-rollback */
uint8_t active_slot; /* 0 = A, 1 = B */
uint8_t candidate_slot; /* slot holding a verified image, 0xFF = none */
uint8_t slot_valid[2]; /* per-slot validity */
uint8_t confirmed; /* 1 = candidate has been confirmed by the app */
uint32_t slot_crc[2];
uint32_t meta_crc; /* CRC over all preceding fields */
} ota_meta_t;
/* Two metadata copies; the valid one is the copy whose meta_crc matches
and whose seq is highest. A write always targets the *other* copy,
then the seq is incremented, so a power fail leaves one intact copy. */
static const ota_meta_t *meta_load(void)
{
const ota_meta_t *a = (const ota_meta_t *)META_A_ADDR;
const ota_meta_t *b = (const ota_meta_t *)META_B_ADDR;
int va = (a->magic == MAGIC && crc32(a, offsetof(ota_meta_t, meta_crc)) == a->meta_crc);
int vb = (b->magic == MAGIC && crc32(b, offsetof(ota_meta_t, meta_crc)) == b->meta_crc);
if (!va && !vb) return NULL; /* first boot: use defaults */
if (va && (!vb || a->seq > b->seq)) return a;
return b;
}
/* Boot decision made only by the bootloader, after verifying the image. */
void boot_select(const ota_meta_t *m)
{
uint8_t slot = m->active_slot;
if (m->candidate_slot < 2) { /* a pending update exists */
if (image_verify(m->candidate_slot) == OK) {
slot = m->candidate_slot; /* trial boot */
ota_mark_trial(slot); /* start the confirm timer */
} else {
ota_clear_candidate(); /* bad image: keep running old slot */
}
}
if (image_verify(slot) != OK) /* the active slot is damaged */
slot = 1 - slot; /* fall back to the other slot */
/* If neither slot verifies, stay in the bootloader's recovery mode. */
jump_to_application(slot);
}
Two implementation details are worth emphasising. First, jump_to_application() must read the stack pointer and reset vector from the image’s first two words, then set MSP, set VTOR to the slot base, and only then branch — and it must run with interrupts disabled. Second, ota_mark_trial() and the application’s confirm step must be mutually safe: if the application crashes before confirming, the bootloader’s watchdog or a boot counter (stored in the metadata) reverts to the other slot; a boot counter incremented in the bootloader and reset by the application’s confirmation is the simplest robust implementation.
8. Worked Example — 512 kB MCU, Dual Slot, 1 MB Flash
Target: an STM32-class MCU with 1 MB internal flash, 128 kB SRAM, a cellular modem for transport, a field installation that must never brick, and a 300 kB application.
- Memory map: bootloader 64 kB (0x08000000–0x0800FFFF), metadata 2 × 2 kB, slot A at 0x08010000 with 448 kB, slot B at 0x08080000 with 448 kB. Check with the flash size calculator that 448 kB comfortably holds the 300 kB image plus the header; the remaining ~150 kB is the growth headroom for future versions.
- Image format: a 512-byte header (magic, version, length, load address, image CRC, SHA-256, ECDSA-P256 signature) followed by the raw binary. The header is included in the signature.
- Transport: chunks of 2 kB with an offset and a per-chunk CRC; the device acknowledges the highest contiguous offset; the server resumes from the device’s last acknowledged offset after a reconnect. RAM budget: 2 kB chunk buffer + the modem’s own buffers; the SHA-256 context is 108 bytes.
- Flow: the running application downloads into the inactive slot (the bootloader exposes a flash write service or the application has a privileged flash driver), verifies the image CRC and hash, writes the metadata (candidate = inactive slot) with the two-phase commit, and requests a reboot. The bootloader verifies the signature, trial-boots the candidate, and the application confirms after 30 s of successful operation; if the reboot happens again without a confirmation, the boot counter trips and the bootloader reverts.
- Power-fail analysis: fail during download → the inactive slot is incomplete, the metadata still points at the active slot, the device boots normally and resumes the download. Fail during the metadata write → the other metadata copy is intact, the device boots the active slot. Fail during the trial boot → the boot counter in the metadata increments each boot until it trips the rollback. Fail during the confirmation → the trial continues and either confirms or trips the counter. Every path leaves one verified image.
- Anti-rollback: the metadata’s
seqis monotonic and compared with the image’s version in a separate append-only region; a signed image with a version lower than the recorded minimum is refused, so an attacker cannot force a downgrade to a firmware with a known vulnerability. - Recovery: if both slots fail to verify (flash damage), the bootloader stays in its recovery mode and exposes the serial bootloader; the field technician can then reflash without a JTAG connection.
9. Rollback, Field Recovery and Fleet Update Strategy
A resilient OTA design does not stop at “the new image boots”; it defines what happens when it does not, and how the fleet behaves when thousands of devices update at once.
Automatic rollback. The mechanism that makes field recovery possible is the trial-boot counter. The bootloader increments a counter before jumping to the new image; the application clears it (and marks the slot as confirmed) once it has reached a point in its start-up sequence where it is demonstrably healthy — for example after it has initialised the peripherals and established communication with the server, not merely after it has executed main(). If the counter reaches a threshold (typically 2–3), the bootloader marks the new slot as bad, switches the boot flag back to the previous image and boots it. The confirmation point is a design decision with real consequences: confirming too early lets a partially broken image pass the check, confirming too late means the device rolls back because of a transient network failure rather than a firmware fault.
A recovery path that does not depend on the application. The bootloader (or a separate recovery partition) must be able to receive a new image over the transport without the application’s help. If the application is what provides the transport, a device whose application fails to start can never be updated again. The practical options are a bootloader with its own minimal transport (a serial or CAN bootloader in ROM plus a fallback protocol), a factory partition holding a known-good image that can be copied over a corrupted slot, and — the last resort — a hardware access point (a test pad or a button-triggered download mode). Every one of these needs to be defined before the first field failure, not after.
Versioning and compatibility. The metadata must carry enough information for both sides to make safe decisions: firmware version, hardware revision compatibility, minimum bootloader version, image length, CRC and signature. A device that receives an image for the wrong hardware revision must refuse it, and the server must be able to express “this version requires at least bootloader X” so that a two-step update (bootloader first, then application) can be orchestrated.
Fleet-level strategy. Rolling out to many devices at once creates two risks: a bad image reaches everything before anyone notices, and the update traffic saturates the server or the network. The mitigations are standard: staged rollout by cohort (1%, 10%, 100%) with a health metric per cohort, a randomised delay before each device’s download so that the load spreads, resumable downloads for larger images, and a deliberately slow default so that a device only updates when it is on a suitable power source (or above a battery threshold) and, where relevant, idle. Power is the factor most often forgotten: an update that starts at 30% battery and dies at 5% may leave the metadata in a partially written state, which is exactly the case the atomic-metadata design in section 7 exists to survive.
10. Common Mistakes
- Updating in place without a staging copy: a power failure mid-erase leaves no bootable image; use A/B or a staging area.
- Trusting the transport’s CRC only: the link can be perfect and the flash image still corrupted; verify on the device, before booting.
- Storing the hash/signature in the same writable region as the image: an attacker replaces both; put the public key in write-protected memory.
- No confirm step: a logically broken image (integrity valid, functionality broken) becomes permanent; require an explicit confirmation with a boot counter fallback.
- Non-atomic metadata: a single record rewritten in place loses everything if the power fails during the write; use two copies and a sequence number.
- Skipped address validation in the flash driver: one bad chunk writes over the bootloader and the device is unrecoverable in the field.
11. FAQ
Q: Do I need A/B if I have a staging area? A: A staging area (external flash) plus a verified copy into the single application slot can work, but the copy phase itself is unprotected; A/B is simpler to reason about and is the recommended default.
Q: How big should the bootloader be? A: As small as possible while still owning slot selection, verification, the update protocol and recovery — typically 32–64 kB with signature verification.
Q: Is a CRC enough? A: For accidental corruption yes; for security no. Use a CRC for the transport and a signature for the boot decision.
Q: How do I test the power-fail cases? A: A programmable power supply that cuts the rail at randomized offsets during download, verification, metadata writes and trial boots, repeated hundreds of times; any run that leaves the device non-bootable is a design bug.
Q: Should the application or the bootloader do the update? A: The application owns the transport (it has the network stack); the bootloader owns the flash writes to the inactive slot and the verification. Splitting the responsibilities this way keeps the bootloader small and the application replaceable.
12. Conclusion
A safe OTA design is a small set of invariants: never modify the running image; verify before you boot; mark a new image valid only through an atomic, recoverable metadata update; trial-boot and confirm; and always be able to fall back. Get those invariants right and the update mechanism becomes a routine, boring, dependable part of the product — which is exactly what it should be.