Embedded Systems · Microcontrollers

Flash Wait States and the Cost of Fetching Instructions from Embedded Flash

Why an on-chip flash array cannot feed a fast core at one word per cycle, how wait states and prefetch buffers change that, and where the timing stops being uniform.

A flash cell stores a bit as charge on a floating gate. Reading it means driving a word line, letting the cell’s current pull a bit line, and resolving the result through a sense amplifier. That chain is analogue, and its settling time does not shrink with process lithography the way digital logic does, because the sense amplifier needs a minimum voltage difference before its output is valid. The core’s clock is generated by a PLL multiplying a crystal, and it can run at frequencies the array has no chance of matching. The gap between the two is bridged by wait states, and understanding that bridge explains why firmware performance is not simply proportional to the clock frequency written in the data sheet.

What a wait state is

A wait state is one extra clock cycle inserted into a memory access. The flash controller drives a signal that tells the bus to hold; the core’s bus interface extends the access phase by that many cycles before sampling data. The count is programmed at boot into a field in the flash controller’s access-control register, and it must be set before the core’s clock is raised, not after. Raising the frequency first means the first fetch after the change is issued with too few wait states and returns whatever the array happened to be driving at that moment.

The required count comes from a single inequality. Let t_acc be the array access time in nanoseconds, guaranteed by the data sheet at the worst-case voltage and temperature for the part, and t_cyc the core clock period. The number of cycles needed is the ceiling of t_acc / t_cyc, and the number of wait states is that value minus one, because the access itself occupies the first cycle.

Take an array whose guaranteed access time is 25 ns at the part’s worst-case supply and temperature. At 24 MHz the period is 41.7 ns, so the access fits inside one cycle and zero wait states suffice. At 96 MHz the period is 10.4 ns, so three cycles are needed and the register gets 2. The data sheet’s access-time table is characterised at the supply and temperature extremes the part is rated for, and using a nominal figure measured at room temperature instead is a common way to build a device that is marginal at the corners of its range — the guaranteed number is the one to use, and the count derived from it is the one that has to be programmed.

Two further constraints sit alongside the wait-state count. Features that let the flash interface deliver more than one word per access, or that bypass the array entirely, are usually qualified only above a minimum supply voltage, so they must be disabled below it even though the wait-state count alone would permit them. And the count is a property of the whole configuration — clock frequency, supply voltage, and the part’s speed grade — so it has to be recomputed whenever any of the three changes rather than copied from another part in the same family.

The prefetch buffer and why sequential code is cheap

With three wait states, an instruction fetched from flash takes four cycles to arrive. A core that can execute one instruction per cycle would then run at a quarter of its clock, which is a large price for a modest frequency increase. The prefetch buffer exists to hide it.

The mechanism is a small FIFO between the flash interface and the core’s fetch unit. When the core requests the word at address A, the controller returns it and immediately begins reading A+4, A+8, and so on, filling the buffer while the core decodes and executes what it already has. A sequential instruction stream consumes the buffer at the rate it is filled once the pipeline is deep enough, and the wait states stop appearing in the instruction stream. The steady-state fetch rate becomes one word per access time rather than one word per wait-state-count cycles, and the effective throughput is set by the array rather than by the programmed count.

The buffer only helps if the fetch address advances predictably. On a branch, the buffer holds words the core will never use, and the fetch must restart from the target address. The restart costs the full access latency, and the buffer is empty at that moment, so the penalty is larger than a single wait state. This is the structural reason tight loops with short bodies behave differently from long straight-line code: a loop whose body fits in the buffer and whose backward branch is predicted correctly keeps fetching sequentially through the buffer, while a loop that straddles a buffer boundary refills on every iteration.

The prefetch buffer sits on the instruction path, but the array behind it is shared. On parts where program flash is reached by both the instruction bus and a data bus, a load from a constant table competes with the instruction stream for the same array port, and the two interleave. The visible symptom is that adding a lookup table to a hot loop costs more than the table’s size suggests.

When the flash is not the bottleneck

A core running from flash with a working prefetch buffer is limited by the array’s sustained read rate, not by its clock. Two things change that.

The first is a cache. Some parts put a small instruction cache in front of the flash instead of a FIFO, and a cache line is typically several words wide. A cache hit is served at core speed with no wait states at all, so a loop that fits entirely in cache runs at the full clock. A miss costs the line fill, and the fill is a burst read from the array that occupies the flash port for the whole line. The trade against a prefetch buffer is capacity against simplicity: the buffer cannot hold a loop that does not fit its depth, and the cache can, but the cache’s fills are coarser and its behaviour is harder to reason about from the source.

The second is execution from SRAM. Copying a timing-critical routine into SRAM and calling it there removes the flash from the path entirely: an SRAM access at the core’s own clock needs no wait states, so the routine runs at one instruction per cycle subject only to the pipeline. The cost is the copy itself, the SRAM the routine occupies, and the loss of the flash’s execute-only protection. It is worth doing for interrupt handlers with hard cycle budgets and for routines that must run while the flash controller is being reprogrammed.

There is a third case worth naming because it is easy to get wrong: executing from flash while erasing or writing flash. Most controllers stall the entire array during a program or erase operation, so any fetch from it blocks until the operation finishes. A routine that writes a flash page must therefore execute from SRAM, with its interrupt vectors redirected there as well, or the core will fetch from a stalled array and hang until the write completes. The vector table relocation described in the memory map article is the mechanism that makes this possible.

The measurement that separates the two regimes

The distinction between “limited by wait states” and “limited by something else” is testable without instrumentation. The same loop compiled twice, once linked into flash and once copied into SRAM, is compared by cycle count. If the SRAM copy is materially faster, the fetch path is on the critical path and the loop is fetch-bound. If the two are equal, the loop is bound by its own dependencies, by a peripheral, or by a data access that the prefetch buffer cannot help with, and the SRAM copy buys nothing.

A second comparison separates the buffer from the array. A branch that is never taken, placed in the middle of a straight-line block, costs its own cycle but leaves the fetch sequential, so the buffer refills normally. A branch that is taken on every iteration empties the buffer at each restart. The difference between the two cycle counts measures the refill penalty directly, and it is usually larger than the wait-state count would suggest because the buffer is empty at the restart.

The register-level view is coarser but still useful. Reading the wait-state count back after boot confirms what was programmed, and comparing it against the vendor’s table for the actual clock and voltage confirms it is the right value. The failure mode this catches is the boot sequence that raises the PLL before writing the count: the device runs, the timing is marginal, and the corruption appears only at temperature extremes or under supply droop.

Where the model breaks

The simple model — wait states plus a prefetch buffer — assumes the array’s access time is constant. It is not. Access time varies with supply voltage and temperature, which is why the count is derived from the data sheet’s guaranteed figure at the rated extremes rather than from a typical value. Endurance cycling matters too, but not by changing the specified access time: repeated erase and program operations degrade the tunnel oxide and narrow the separation between a programmed cell and an erased one, which eats into the sense amplifier’s margin rather than into the number in the table. Firmware that budgets from a typical figure rather than the guaranteed one has no headroom left for either effect.

The model also assumes the flash controller is the only master of the array. On parts with a DMA controller that can read from program flash, the two masters contend, and the arbitration described in the bus matrix article decides who wins. A DMA transfer from flash during a tight loop is therefore worse than the transfer’s own cost, because it steals fetch bandwidth as well.

Finally, the wait-state count is a property of the device, not of the core. Two parts with the same core at the same clock can require different counts, and the number in one part’s reference manual is not transferable to another. The count belongs in the clock configuration code for the specific part, next to the PLL settings, and it should be checked whenever either the clock or the supply voltage changes.

References

  1. ARM Cortex-M4 Technical Reference Manual (ARM DDI 0439) — Arm Limited
  2. STM32F4 Series Reference Manual (RM0090) — STMicroelectronics
  3. Nonvolatile Memory Technologies with Emphasis on Flash — Brewer & Gill, Wiley-IEEE Press