Embedded Systems · Firmware Updates

Erase Granularity, Write Ordering, and Power Loss During a Flash Write

Why a firmware update cannot be made atomic at the byte level, how sector granularity dictates the update algorithm, and what a partially erased sector actually contains.

A hard disk presents a block device whose 512-byte logical sectors are conventionally treated as written atomically, so a power loss leaves each sector either old or new. NOR and NAND flash present no such guarantee, and the reason is physical rather than a matter of driver quality. A flash cell can be programmed in one direction and erased in the other, erasure affects an entire sector at once, and neither operation is instantaneous. A firmware update that assumes otherwise will eventually produce a device that boots into nothing.

One-way cells and the two operations

A NOR flash cell stores a charge on a floating gate. Programming injects charge, which moves the cell’s threshold voltage in one direction; erasing removes charge and moves it back. There is no operation that sets a single cell to an arbitrary value, because setting it would require both adding and removing charge. Programming can therefore only clear bits: a written byte is the bitwise AND of what was there and what was written. Erasing sets every bit in a region back to 1.

This asymmetry is visible in every flash driver. Writing 0x0F to a location that holds 0xF0 yields 0x00, not 0x0F. Writing 0xFF is a no-op. Recovering from a bad write requires an erase, and an erase is not addressable below the sector.

NAND adds a second asymmetry: it is organised in pages that are programmed as units, with a spare area per page holding error-correction data produced by the controller. Programming is per page, erasure is per block containing many pages, and a block wears out after a bounded number of erase cycles. The endurance figure is a specification, not a measurement, and it is typically quoted per block for a stated data-retention target.

The sizes are what force the algorithm. A small NOR part might have 4 KiB sectors; a larger one 64 KiB or 128 KiB. A NAND block is commonly 128 KiB or more, composed of pages of 2 KiB or 4 KiB. Whatever the numbers, the ratio between the smallest erasable unit and the smallest writable unit is large, and an update must be planned around the erasable unit because that is the granularity at which old content disappears.

What a partially erased sector contains

Erase is a single command with a duration measured in milliseconds per sector, and the device is busy for that whole time. If power fails mid-erase, the sector is not half old and half new in any orderly sense. Cells that finished erasing read as 1; cells that did not read as whatever they held, which may be a valid value for the old data, an intermediate threshold that reads ambiguously, or a value that has drifted. Over a sector, the result is a mixture whose pattern depends on the device’s internal erase sequencing, which is not documented.

The practical consequence is that the sector must be treated as containing arbitrary data after an interrupted erase. It cannot be read as a partial image and patched. It must be erased again before use, and the erase must complete. A bootloader that reads a sector, finds a plausible header, and concludes the image is intact is trusting a region whose contents are the result of an interrupted operation.

Programming is more forgiving in one respect and less in another. A page program that is interrupted leaves a page whose cells are partially programmed, and the direction of the change is known: bits only move from 1 to 0. So a partially programmed page contains a value that is bitwise a superset of ones compared to the intended value. That is detectable if the intended value is known and a check is available, and it is why write-verify is a standard step: read back the page, compare against the buffer, and retry or fail. The retry must re-erase, because the failed programming already consumed the ones.

Why ordering the writes is the real atomicity mechanism

Since no single flash operation is atomic across a power loss, the only way to make an update recoverable is to arrange the operations so that any prefix of the sequence leaves a decidable state. This is the same reasoning as a write-ahead log, applied to a medium with much coarser granularity.

The rule that follows is that no step may destroy information a later step still needs. Applied to a slot-based update, it means the slot being written is never the slot that is currently bootable, which is the arrangement described in dual-bank slots and boot selection. Within the slot being written, the rule means the header is written last, because a slot whose body is complete but whose header is absent is unambiguously incomplete, while a slot whose header is present and whose body is partial is a slot the bootloader will try to validate and reject.

That ordering has a cost: the header write becomes the commit point, and it is a separate operation from the body. If the header is in the same sector as the first bytes of the body, writing it last requires erasing and rewriting that sector, which is impossible without destroying the body. The standard resolution is to put the header in its own sector, or to write the header with a field that is only set after the body is verified, using the one-way property: a “valid” flag that starts as 0xFF and is programmed to 0x00 as the last step. A partially programmed flag byte reads as something other than 0x00 and is treated as invalid, which fails in the safe direction.

/* Commit flag written as the final step of an update. Because flash
   programming only clears bits, an interrupted write of 0x00 leaves a
   value that is not 0x00, and the image stays uncommitted. */
#define IMAGE_COMMIT_OFFSET  12u
#define IMAGE_COMMIT_VALUE   0x00u

static int commit_image(uint32_t slot_base)
{
    uint8_t flag = 0x00;                /* 0xFF is the erased value; 0x00 commits */
    if (flash_program(slot_base + IMAGE_COMMIT_OFFSET, &flag, 1) != 0)
        return -1;                      /* still uncommitted, retry later */
    return 0;
}

static int image_is_committed(uint32_t slot_base)
{
    uint8_t flag;
    if (flash_read(slot_base + IMAGE_COMMIT_OFFSET, &flag, 1) != 0)
        return 0;
    return flag == IMAGE_COMMIT_VALUE;
}

The flag alone is not sufficient. A commit flag proves the writer reached the final step, not that the body is correct, so the integrity check from image header and integrity validation still has to run. The flag is an ordering device; the hash is the correctness device. Conflating them produces images that are committed and wrong.

Wear, retention, and the cost of rewriting metadata

Every erase degrades the oxide that isolates the floating gate, and the degradation accumulates until the cell can no longer hold a distinguishable charge. The number of cycles is finite and specified per part. A firmware update that rewrites a slot on every release consumes one erase cycle per sector per update, which for a device updated a few times a year is irrelevant and for a device updated daily over a decade is not.

Metadata is where wear concentrates, because metadata changes on every boot rather than every release. A design that stores a boot counter in a fixed location and rewrites it each reset will exhaust that sector long before the rest of the flash. The append-only record log described in boot metadata persistence and the commit record spreads writes across a sector and only erases when the log fills, which reduces the erase rate by the number of records per sector, typically two orders of magnitude.

Retention is the other half of the same physical property. Charge leaks slowly, and the rate depends on temperature and on how much charge was stored. A part specified for twenty years at room temperature may be specified for far less at elevated temperature. An image written once and never rewritten sits at the worst case for retention, while the metadata sectors, rewritten frequently, are periodically refreshed. This is an argument for periodically rewriting the application slot even when nothing changed, which almost no design does.

The degenerate cases that break drivers

Erased flash reads as all ones, and a driver that treats a read of 0xFF as an error will fail on the first valid erased sector. Conversely, a driver that treats a read of all zeros as valid data will accept a sector that has been over-programmed, which on some parts reads as zeros because the cells are stuck.

Programming across a page boundary is a defect in many drivers. The device latches a page into an internal buffer and writes it as a unit; an address range that starts mid-page and crosses into the next causes either a wrap within the page, silently duplicating bytes, or a rejection. A driver that accepts arbitrary offsets and lengths must split at page boundaries, and the split has to account for the destination offset, not just the length.

Reading while writing is the third trap. A flash device executing a program or erase is busy, and reads issued during that window return undefined data or are ignored depending on the part. Code that executes from the same flash it is writing must either run from RAM or stall until the operation completes. On a part with a single bank, this is unavoidable, and it is why some bootloaders copy a small write routine into RAM before touching the application region. On a part with two banks, code can execute from one while the other is programmed, which is a genuine reason to choose such a part for a design with frequent updates.

What this means for the update protocol

The protocol layer cannot fix flash granularity, but it must respect it. A transfer that delivers bytes out of order cannot be written directly, because the driver has to buffer a full page before programming and cannot revisit a completed page. A transfer that resumes after an interruption must resume at a page boundary, and the receiver must erase the sector containing that boundary before writing, which discards any earlier bytes in that sector. Resuming mid-sector is therefore not a resumption of the write; it is a resumption of the transfer followed by a rewrite of the whole sector. Protocols that track progress at sector granularity, rather than at byte or record granularity, align with what the medium can actually do, which is the concern of chunked transfer and retransmission.

References

  1. JEDEC JESD216: Serial Flash Discoverable Parameters (SFDP) — JEDEC Solid State Technology Association
  2. STM32F4 Series Reference Manual (RM0090): FLASH interface — STMicroelectronics
  3. JEDEC JESD218: Solid-State Drive Requirements and Endurance Test Method — JEDEC Solid State Technology Association