Embedded Systems · Firmware Updates
Dual-Bank Slots and How a Bootloader Picks One
Why two firmware slots turn an in-place overwrite into a recoverable operation, how the boot decision is made from metadata, and what the doubling of flash costs.
The naive firmware update writes the new image over the old one. For most of the operation there is no complete image in flash: the region holds a prefix of the new image and a suffix of the old. If power fails in that window, the device has neither. A slot arrangement removes the window by never overwriting the image that is currently capable of running.
The window an in-place update opens
Consider a 256 KiB application in a single flash region, updated by erasing the first sector and streaming the new image in. Between the erase and the final write, the region contains a partial image. Whether that partial image is runnable depends on where the interruption occurred, and the dependency is not something the bootloader can reason about. A partial image whose header is intact and whose body is truncated fails a length check, which is good. A partial image whose body was written from the end backwards, or whose header was written last, might pass every check the bootloader makes and then jump into an entry point that lands in erased flash.
The recovery path for a device in this state is a debug probe or a ROM boot mode. Both require physical access. A product in the field has neither, so the requirement that emerges is not “the update should usually succeed” but “an interrupted update must leave a runnable image”. That requirement is what a second slot provides, and it is stronger than a backup: a backup is a copy that must be restored, while a second slot is a complete image that can be run without any restore step.
Two slots and the metadata that selects between them
The layout is two regions of equal size, each able to hold a complete image, plus a small region for metadata. On a part with 512 KiB of flash and a 200 KiB application, two slots of 224 KiB each and a metadata region of a few sectors is a workable split, and the arithmetic is entirely determined by the largest image the design must support, not by the current one.
Each slot holds an image with the header described in image header and integrity validation. The metadata region holds, at minimum, which slot to boot, which slot holds a confirmed image, and which slot is being tested. It is small and it is the only mutable state the bootloader trusts.
The selection logic is a function of that state and nothing else. On reset the bootloader reads the metadata, determines the target slot, validates the image there, and boots it. If validation fails it falls back to the other slot. The fallback must be bounded: a bootloader that tries slot A, fails, tries slot B, fails, and resets has created a loop that a watchdog will eventually break, but the loop itself is the failure. The usual rule is that after a failed validation of the primary, the bootloader boots the secondary unconditionally if the secondary validates, and if neither validates it enters a recovery mode rather than resetting.
“Validates” here means the integrity check plus the policy checks, and the distinction matters for slot selection. An image can be structurally intact and still not permitted to boot, for example if its version is below a counter that has already been advanced. The bootloader must be able to say “this slot is intact but not bootable” as a distinct outcome from “this slot is damaged”, because the fallback rules differ.
Where the metadata lives and why it cannot be a single word
The metadata is written rarely and read on every boot, and it is the state that must survive a power loss at any instant. A single word in flash holding the active slot index fails this requirement: updating it requires an erase of its sector, and a power loss during the erase leaves the word in an indeterminate state that is neither the old value nor the new one.
The standard resolution is a log of small records in a region that is erased in sectors. Each record is a complete statement of the state, with a sequence number and a checksum, and a write appends a new record rather than modifying the old one. Reading the metadata means scanning the sector for the highest sequence number whose checksum matches. A power loss during an append leaves a record with a bad checksum, which is ignored, and the previous record remains authoritative. When the sector fills, the current state is written to the next sector before the old one is erased, so at no point is there only one copy of the state on the device.
The cost is a scan at boot, bounded by the sector size and record size. A 4 KiB sector holding 32-byte records contains at most 128 records, and because the scan stops at the first erased record it costs at most one sector read however old the log is. The cost in flash is one or two sectors permanently reserved, which on a small part is a real fraction of the total and on a large one is noise.
/* Metadata record: appended to a log sector, never modified in place.
seq increases monotonically; the highest valid seq wins. */
struct meta_record {
uint32_t seq;
uint8_t active_slot; /* 0 or 1 */
uint8_t confirmed_slot; /* slot holding a known-good image */
uint8_t trial_slot; /* 0xFF when no trial is in progress */
uint8_t trial_count; /* remaining boot attempts */
uint32_t crc; /* CRC-32 over the preceding 8 bytes */
};
static int read_meta(struct meta_record *out)
{
uint32_t best_seq = 0;
int found = 0;
for (uint32_t off = 0; off < META_SECTOR_SIZE; off += sizeof *out) {
struct meta_record r;
memcpy(&r, (const void *)(META_BASE + off), sizeof r);
if (r.seq == 0xFFFFFFFFu) break; /* erased tail: stop scanning */
if (crc32(&r, offsetof(struct meta_record, crc)) != r.crc) continue;
if (r.seq > best_seq) { best_seq = r.seq; *out = r; found = 1; }
}
return found;
}
Two details in that code are load-bearing. The scan stops at the first erased word rather than at the end of the sector, because an erased record has a sequence number of 0xFFFFFFFF and would otherwise sort above every real record. And the checksum covers the sequence number, so a record whose sequence field was partially written fails validation instead of winning.
Swap, direct-XIP, and the cost of each
Two placements are in common use and they differ in what happens at update time.
In a direct-XIP arrangement, each slot is independently executable and the bootloader boots whichever is marked active. Updating means writing the new image into the inactive slot, marking it active, and resetting. Nothing is copied at boot, so boot time is constant, and the two slots must each be large enough for a full image. Each image must be linked for the base address of the slot it occupies, because there is no loader to relocate it: either the build produces one binary per slot, or the code is position-independent so that a single binary runs from either address. Per-slot builds double the artefacts the release process has to sign and track, which is the cost this arrangement pays instead of the swap.
In a swap arrangement, the inactive slot receives the new image and the bootloader then exchanges the contents of the two slots, either at the end of the update or at the next boot. The application is linked for one address only, which is a large simplification. The price is that the swap itself is a multi-step operation over the whole image and is therefore the new place where a power loss can leave a broken state. Implementations handle this by making the swap resumable: progress is recorded per sector, so an interrupted swap continues from the last completed sector on the next boot rather than restarting. The metadata log above is what makes that record possible.
A third arrangement places the update in an external memory or a scratch region and copies it into the single application slot at boot. It avoids doubling on-chip flash, at the cost of the copy happening on every boot of the new image and of the copy itself being non-atomic.
What the second slot costs beyond flash
The obvious cost is capacity: the design needs room for two images, so the part must be chosen for twice the application size, and a design that outgrows the slot cannot be fixed by compression. On a 256 KiB part with a 120 KiB application there is no room for a second 120 KiB slot plus a bootloader and metadata, and the update strategy has to be different.
The less obvious cost is that flash wear is now concentrated. Updates write to the inactive slot, which alternates, so wear spreads across both slots rather than concentrating on one. That is favourable. The metadata log, however, is written on every state transition including every boot during a trial, and a sector that holds 128 records and is erased after each fill will be erased once per 128 transitions. Trial boots that decrement a counter each reset can consume records quickly on a device that resets often.
The third cost is conceptual and it is the one that produces bugs: there are now two images and their versions can differ, so every decision the bootloader makes has to specify which slot it refers to. “The version” is ambiguous. Code that reads the version from the slot it happens to be running from, while making a decision about the slot it is about to boot, will compare the wrong pair. Naming the fields explicitly, as the record above does with active, confirmed, and trial, is the cheapest defence.
Degenerate layouts
Equal-sized slots are the simple case and not the only one. A design can give the update slot less space than the running slot if the update is delivered as a delta rather than a full image, which is the subject of delta updates and patch application; the reconstructed image then has to be written somewhere, so the saving is often illusory.
A design can also place the bootloader itself in a slot, so that the bootloader is updatable. That requires a third region and a mechanism for the bootloader to replace itself, which is the hardest case because the code performing the write is the code being overwritten. The usual approach is a minimal immutable stage that validates and jumps to a full bootloader in a slot, so only the full bootloader is ever replaced. The immutable stage is small enough to be audited and is the only part of the system that cannot be fixed after shipment.
References
- MCUboot documentation: Design — MCUboot project
- ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm
- U-Boot documentation: DFU — Das U-Boot project