
The Cortex-M Floating-Point Unit (FPU) is a critical accelerator for DSP, motor control, and sensor fusion workloads. Yet its interaction with the NVIC exception model introduces a subtle but measurable latency penalty: lazy stacking. Understanding when lazy stacking helps, when it hurts, and how to control it is essential for firmware engineers targeting deterministic response times on Cortex-M4, M7, M33, and M55 cores.
The Cortex-M FPU provides 32 single-precision registers (S0-S31) plus the FPSCR status register — 33 registers total. On exception entry, the hardware must preserve the caller-saved state (S0-S15, FPSCR, plus one reserved padding word — 18 words total) so the interrupted context can resume floating-point computation seamlessly.
+----------------------------------------------------------+| EXCEPTION ENTRY WITHOUT LAZY STACKING |+----------------------------------------------------------+| || 1. Hardware detects exception || 2. Push 18 FPU words to stack (~17 additional cycles) || 3. Push core registers (R0-R3, R12, LR, PC, xPSR) || 4. Execute handler || 5. Pop FPU registers on exit (~17 additional cycles) || || Total FPU overhead: ~34 cycles per exception |+----------------------------------------------------------+
At 168 MHz, 34 cycles equals ~200 nanoseconds of pure register save/restore — before your handler executes a single instruction. For a high-frequency control loop, this deterministic overhead can add up.
ARMv7-M and ARMv8-M architectures introduced lazy stacking (controlled via FPCCR.LSPEN and FPCCR.ASPEN) to eliminate this overhead for exceptions that never touch the FPU.
+----------------------------------------------------------+| EXCEPTION ENTRY WITH LAZY STACKING |+----------------------------------------------------------+| || 1. Hardware detects exception || 2. Check: Has FPU been used (CONTROL.FPCA == 1)? || NO -> Push standard 8-word core frame only || YES -> Proceed to step 3 || 3. Allocate 26-word extended frame on stack, but || SKIP writing the 18 FPU words to memory. || 4. Set FPCCR.LSPACT = 1 (lazy state active) || 5. Write core registers only (~12 cycles) || 6. Execute handler || First FP instruction triggers deferred FPU save || 7. On exit: pop core registers, then FPU if LSPACT=1 || || Worst case (FPU used): ~34 cycles + deferral penalty |+----------------------------------------------------------+
The processor tracks FPU usage via the FPCCR.LSPACT bit and CONTROL.FPCA. When an exception occurs and the FPU was used, the hardware allocates the stack space but skips the 18-word memory write. The first floating-point instruction inside the handler triggers the deferred save — writing the FPU registers to the reserved stack space while the handler is already running.
Lazy stacking shines in systems where:
Typical savings: ~17 cycles per non-FPU interrupt entry/exit. On a Cortex-M4 at 168 MHz with 10,000 non-FPU interrupts/sec, that’s 340,000 cycles/sec recovered.
The deferred save creates a non-preemptible latency window. Consider this scenario:
+----------------------------------------------------------+| NESTED INTERRUPT LATENCY SPIKE |+----------------------------------------------------------+| || Time || ^ || | Low-priority IRQ enters (no FPU used yet) || | Lazy stacking enabled, LSPACT = 1 || | || | Low-priority handler executes VADD.F32 S0, S1, S2 || | -> FIRST FP INSTRUCTION || | -> Hardware DEFERS FPU save, begins memory writes || | -> Begins 18-word push to stack (~17 cycles) || | ^ || | | HIGH-PRIORITY IRQ FIRES HERE || | | (e.g., safety-critical watchdog, motor PWM) || | | || | | Processor CANNOT preempt until || | | deferred FPU save COMPLETES || | | (non-preemptible critical section) || | || | High-priority handler waits up to 17 cycles || | before it can even START || +----------------------------------------------------->|| |+----------------------------------------------------------+
This is the lazy stacking trap: a low-priority handler’s first FP instruction creates an ~17-cycle non-preemptible region that blocks all higher-priority interrupts. For hard real-time systems with ultra-strict deadlines, this jitter can be problematic.
Disable lazy stacking by setting FPCCR.ASPEN=1 and FPCCR.LSPEN=0 during FPU initialization. This forces the hardware to save the FPU state immediately on exception entry.
// Enable FPU with EAGER stacking (deterministic latency)void fpu_enable_eager_stacking(void) {// CPACR: Enable FPU access (bits 20-23 = 0b11)SCB->CPACR |= (0xF << 20);// FPCCR: Enable automatic state preservation (ASPEN=1)// Disable lazy state preservation (LSPEN=0)// FPCCR address: 0xE000EF34volatile uint32_t *fpccr = (volatile uint32_t *)0xE000EF34;*fpccr = (*fpccr & ~(1 << 30)) | (1 << 31);// DSB/ISB to ensure FPCCR write takes effect__DSB();__ISB();}
With eager stacking, every exception entry that interrupts FPU usage pays the 17-cycle FPU save cost, but no exception ever blocks on a deferred save. Latency becomes deterministic — critical for safety-critical systems.
If you want lazy stacking for non-FPU interrupts but need to eliminate the nested-interrupt spike, execute a dummy FP instruction during thread startup or context switch:
// Force FPU state to be "active" so lazy stacking doesn't deferstatic inline void fpu_force_state_active(void) {// Dummy FP instruction - compiler won't optimize awayvolatile float dummy = 0.0f;__asm volatile ("vmov.f32 s0, %0" : : "w" (dummy) : "s0");__DSB();}// Call during thread initialization or RTOS context switch outvoid task_init_fpu_context(void) {fpu_force_state_active();// Now LSPACT=0 and FPU state is "clean" -// next exception will eager-save if needed}
This ensures the FPU state is already marked as active, so the next exception entry performs eager stacking without deferral.
FreeRTOS and Zephyr both handle FPU context switching, but lazy stacking adds complexity:
// FreeRTOS port layer snippet (ARM_CM4F/ARM_CM7)#if (configUSE_TASK_FPU_SUPPORT == 2) // Lazy stacking enabled// Check if lazy state is active on outgoing taskif ((*(volatile uint32_t *)0xE000EF34) & (1 << 0)) { // FPCCR.LSPACT (bit 0)// Lazy state active - must save FPU registers manuallyvPortSaveFPUContext();*(volatile uint32_t *)0xE000EF34 &= ~(1 << 0); // Clear LSPACT}#endif
Key insight: The RTOS must check FPCCR.LSPACT during context switch. If lazy state is active, the FPU registers are partially saved on the interrupted task’s stack — the scheduler must complete the save before switching tasks.
On a typical Cortex-M4 at 168 MHz:
| Configuration | Non-FPU IRQ Entry | FPU IRQ Entry (first) | Nested IRQ Latency Spike |
|---|---|---|---|
| Eager stacking | 29 cycles | 29 cycles | 0 cycles (deterministic) |
| Lazy stacking (no FPU used) | 12 cycles | N/A | 0 cycles |
| Lazy stacking (FPU used) | 12 cycles | 12 cycles + deferral | Up to 17 cycles |
The “deferral” in the nested case is the time to complete the deferred push after the high-priority IRQ fires but before its handler runs.
+----------------------------------------------------------+| LAZY STACKING DECISION MATRIX |+----------------------------------------------------------+| System Profile | Recommendation |+------------------------------------+---------------------+| Hard real-time, safety-critical | EAGER (disable || (ISO 26262, IEC 61508) | lazy stacking) |+------------------------------------+---------------------+| Soft real-time, mixed workload | LAZY + force || (motor control + comms) | early save |+------------------------------------+---------------------+| FPU rarely used (<5% interrupts) | LAZY (default) || (sensor polling, UI) | |+------------------------------------+---------------------+| Deeply nested interrupts (>3) | EAGER || (complex priority scheme) | |+------------------------------------+---------------------+| RTOS with FPU-aware context switch | LAZY (with || (FreeRTOS, Zephyr) | RTOS support) |+------------------------------------+---------------------+
Lazy stacking is a powerful hardware optimization that eliminates FPU register memory write overhead for the common case where interrupts don’t use floating-point. However, it introduces a non-preemptible latency window when a low-priority handler’s first FP instruction defers the save, potentially blocking higher-priority interrupts for ~17 cycles.
For hard real-time systems, disable lazy stacking via FPCCR.LSPEN=0 and FPCCR.ASPEN=1 to achieve deterministic interrupt latency. For mixed workloads, keep lazy stacking enabled but force early FPU state activation during thread initialization to eliminate the deferral window. Always verify your RTOS context switch code correctly handles FPCCR.LSPACT — this is where lazy stacking bugs most often surface.
The FPU is a tool, not a free lunch. Configure its exception behavior to match your system’s timing requirements, not the hardware defaults.
Quick Links
Legal Stuff





