Embedded Systems · Embedded Tooling

In-Circuit Programming: Why the Flash Algorithm Runs From RAM

How a probe erases and writes flash through the target core, why the routine cannot execute from the memory it is modifying, and what sector granularity costs an update.

Writing internal flash is not a memory write. The controller accepts a command, an address, and data into registers, starts an operation, and reports completion through a status register after a delay measured in milliseconds for an erase and tens of microseconds for a word program. The core that issues those register writes has to keep executing while the controller works, and it must not be executing from the array being modified. Both constraints shape the tooling.

The erase-before-write rule

Flash cells program in one direction. A program operation can clear bits — turn a 1 into a 0 — but cannot set them back to 1. Restoring a bit requires an erase, which operates on a whole sector or page rather than on individual bytes. Every write path therefore begins with an erase of a region at least as large as the write, and the sector size is a hardware property that ranges from a few hundred bytes on some small parts to tens or hundreds of kilobytes on large ones.

The consequence for tooling is that updating a small region costs the erase of a large one. A 64-byte constant changed in a 128 KiB sector forces a 128 KiB erase and a 128 KiB rewrite. The cost is time and endurance: cells tolerate a finite number of erase cycles, commonly specified in the low thousands to low hundreds of thousands depending on the process, and every unnecessary erase of a large sector consumes a share of that budget for the whole sector rather than for the bytes that changed.

Two strategies mitigate it. The first is to place rarely changing data — bootloaders, calibration tables, configuration — in their own sectors, so an application update does not touch them. The second is a bootloader that writes new images to a staging region and swaps by pointer, so the running image is never erased in place. Both are layout decisions made in the linker script, and both depend on the sector boundaries being visible there as separate MEMORY regions rather than as one flat range.

Why the algorithm must not run from flash

A flash controller typically stalls the bus during a program or erase operation, or at minimum makes the array being modified unreadable. Code executing from that array stops fetching, and the core either stalls until the operation completes or takes a bus fault if the controller returns an error. On parts that stall, execution resumes correctly after the operation, which makes the failure mode intermittent: a routine that happens to fit in a cache or a prefetch buffer works until it does not.

The standard solution is a flash algorithm: a small position-independent routine, typically a few hundred bytes to a couple of kilobytes, loaded into RAM by the probe and executed with the program counter there. It exposes a handful of entry points — initialise, erase a sector, program a page, verify — each taking an address and, where relevant, a buffer pointer and length, and returning a status code. Because the routine executes from RAM, the erase and program operations it performs cannot interrupt its own instruction fetch, which is the entire reason for loading it there.

The interface is deliberately minimal because the probe cannot call arbitrary code with arbitrary arguments. Parameters go into registers according to the target’s calling convention, and the probe sets the program counter and stack pointer directly, then waits for the routine to return or for a breakpoint at the end to fire. No operating system, no linker, no loader. The routine is a raw binary blob whose entry addresses the probe knows from a device description.

The sequence the probe performs

A complete programming operation on a Cortex-M target with a memory access port proceeds through a fixed series of steps, and each has a failure mode:

  1. Halt the core and read the current stack pointer and a few registers, so the target’s state can be restored afterwards.
  2. Disable interrupts, or at least those that would run from flash. An interrupt taken while the array is unreadable vectors through a table in that array.
  3. Enable the flash controller’s clock and remove write protection, which on many parts means writing a key sequence to a lock register.
  4. Write the algorithm image to RAM and the parameter block to a known address.
  5. Set the program counter to the algorithm’s entry and the stack pointer to a scratch area, then resume.
  6. Poll for completion — either by reading a status word in the parameter block or by watching for a breakpoint the algorithm triggers on return.
  7. Restore registers, re-enable interrupts, and verify by reading back.

Step 7 is optional and expensive. Verification doubles the transfer, since every byte is read through the debug port at the port’s clock rate. On SWD at a few megahertz, reading a megabyte takes seconds regardless of how fast the flash writes. Whether to verify is a trade between confidence and session time, and the answer differs between a one-off production load and a debug cycle repeated hundreds of times a day.

The clock rate matters more than it appears. Every byte transferred goes over the debug transport, one transaction per word at best, so programming throughput is bounded by the transport rather than by the flash. A 128 KiB image at 32 bits per transaction needs over 32,000 transfers before any flash time is counted, and each transfer is several protocol packets with acknowledge phases — which is a reason the text image formats are converted to a raw binary before transfer rather than streamed as records.

Interrupts, watchdogs, and the things that reset the part

Three hazards recur across implementations.

An interrupt that fires during the erase vectors through a table in the array being erased. The core fetches from an unreadable region, faults, and the probe sees a connection drop or a hard fault at an address that makes no sense. Disabling interrupts is the fix, and it must include the non-maskable interrupt, which cannot be disabled on most parts and therefore must be handled by ensuring its source does not assert during programming.

A watchdog that is running when the algorithm starts will reset the part during a long erase unless it is serviced or disabled. Erasing a large sector can take hundreds of milliseconds; a watchdog with a timeout in the tens of milliseconds fires first. The algorithm can service the watchdog itself, but the register access then depends on the specific part and must be part of the algorithm rather than of the probe.

A reset during programming leaves a partially erased sector. On most parts this is not recoverable by resuming: the sector’s contents are undefined and the image is invalid. The probe must erase the whole sector again rather than assume the previous attempt left it blank, and the bootloader must be able to detect the state, which is why an image header with a checksum and a validity marker is worth its space. That check is the same one a bootloader performs on a staged image, and it is what allows an interrupted update to be retried rather than bricking the device.

Sector granularity as a design constraint

The sizes are visible in the reference manual and they should be visible in the build. A part with 1 KiB sectors can hold a bootloader in two sectors and a configuration block in one, giving fine-grained updates. A part with a uniform 128 KiB sector cannot, and the layout must instead put the bootloader and the application in separate sectors and accept that a configuration change rewrites the whole block.

Dual-bank parts change the arithmetic again: two banks allow an image in one bank to be programmed while code executes from the other, which is what makes a live update possible without a staging area. The bank swap is then a register write plus a reset, and the failure mode is a reset in the middle of the swap.

Reading the erase-block geometry from the part itself, where the part reports it, is preferable to hard-coding it. Where the geometry is not discoverable, it belongs in a device description consumed by the programming tool, alongside the memory map, the algorithm blob, and the register addresses it touches. That description is what distinguishes a tool that supports a family of parts from one that supports a single board, and it is the same class of information the probe’s transport layer needs in order to reach the controller at all.

References

  1. ARMv7-M Architecture Reference Manual — Arm Ltd, DDI 0403
  2. Open On-Chip Debugger User Guide — OpenOCD project
  3. The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors — Joseph Yiu; Newnes, 2013