
FPU context switching on Cortex-M is a subtle but critical design decision that directly impacts worst-case interrupt latency and real-time determinism. The ARMv7-M and ARMv8-M architectures provide two mechanisms: lazy stacking (default) and eager state preservation. Choosing between them — or configuring them per-task — requires understanding the exact hardware behavior and your application’s FPU usage profile.
On exception entry, the Cortex-M processor hardware automatically stacks the general-purpose registers (R0-R3, R12, LR, PC, xPSR) onto the current stack. For the FPU registers (S0-S31, FPSCR), the behavior is controlled by FPCCR.LSPEN (Lazy State Preservation Enable, bit 30):
The lazy path saves cycles when the interrupted code never touches FPU — common in interrupt handlers that only manipulate integer state.
+--------------------------------------------------------------+| EXCEPTION ENTRY SEQUENCE |+--------------------------------------------------------------+| || Hardware auto-saves: R0-R3, R12, LR, PC, xPSR || (8 registers, ~12 cycles) || || If LSPEN=1 (lazy): || - Sets CONTROL.FPCA = 1 (FPU context active) || - No FPU registers stacked || - Cost: ~2 cycles || || If LSPEN=0 (eager): || - Pushes S0-S31, FPSCR (33 registers) || - Cost: ~34-50 cycles || |+--------------------------------------------------------------+
When the task resumes and executes its first FPU instruction (e.g., VADD.F32 S0, S1, S2), the processor detects CONTROL.FPCA=1 and takes a deferred stacking exception (a special exception type, not a standard interrupt). This exception:
CONTROL.FPCAThe latency of this deferred stacking is non-deterministic from the application’s perspective — it depends on when the first FPU instruction occurs. In a hard real-time system, this spike can cause deadline misses if it happens inside a high-priority control loop.
+--------------------------------------------------------------+| DEFERRED STACKING EXCEPTION FLOW |+--------------------------------------------------------------+| || Task Context Kernel/ISR Task Resumes || +------------+ +------------+ +------------+ || | ... | | Exception | | VADD.F32 | || | VADD.F32 | --> | Entry | --> | (traps) | || | ... | | (lazy) | | Deferred | || +------------+ +------------+ | Stacking | || | Exception | || | Push S0-31 | || | Clear FPCA | || | Retry VADD | || +------------+ || || Latency spike: 34-50 cycles on Cortex-M4/M7 || Unpredictable: depends on when first FPU insn executes || |+--------------------------------------------------------------+
Setting FPCCR.LSPEN = 0 forces the processor to push all 33 FPU registers on every exception entry, regardless of whether the interrupted code uses FPU. The cost is paid upfront and deterministically:
Eager preservation makes sense when:
A well-designed RTOS exposes FPU policy per-task. The typical implementation:
// Task creation with FPU policytypedef enum {FPU_POLICY_LAZY = 0, // Default: LSPEN=1FPU_POLICY_EAGER = 1, // Force eager: LSPEN=0FPU_POLICY_NONE = 2 // Task never uses FPU (compiler flag)} fpu_policy_e;BaseType_t xTaskCreateWithFPU(TaskFunction_t pxTaskCode,const char *pcName,configSTACK_DEPTH_TYPE usStackDepth,void *pvParameters,UBaseType_t uxPriority,TaskHandle_t *pxCreatedTask,fpu_policy_e fpuPolicy);// During task context switch (simplified)void vTaskSwitchContext(void) {// Save outgoing task's FPU state if FPU was activeif (pxCurrentTCB->fpuPolicy == FPU_POLICY_EAGER) {vFPU_SaveState(pxCurrentTCB->fpuContext);} else if (pxCurrentTCB->fpuPolicy == FPU_POLICY_LAZY) {// Lazy: FPCCR.LSPEN=1, hardware handles on first FPU useFPCCR |= (1 << 30); // LSPEN = 1}// Configure incoming taskif (pxNextTCB->fpuPolicy == FPU_POLICY_EAGER) {FPCCR &= ~(1 << 30); // LSPEN = 0 (eager)vFPU_RestoreState(pxNextTCB->fpuContext);} else {FPCCR |= (1 << 30); // LSPEN = 1 (lazy)}}
The RTOS must also manage FPCCR.ASPEN (Automatic State Preservation Enable, bit 31) which controls whether the processor automatically preserves FPU state on nested exceptions. For most RTOS ports, ASPEN=1 is correct.
| Scenario | Lazy Stacking | Eager Save |
|---|---|---|
| Task never uses FPU | +2 cycles (FPCA set) | +34-50 cycles wasted |
| Task uses FPU once per switch | +34-50 cycles (deferred) | +34-50 cycles (upfront) |
| Task uses FPU heavily | +34-50 cycles per switch | +34-50 cycles per switch |
| WCET determinism | Non-deterministic spike | Fixed, known cost |
| Nested interrupt latency | Variable (depends on FPCA) | Predictable |
Rule of thumb: If >80% of context switches involve FPU usage, eager save wins. If FPU usage is rare or sporadic, lazy stacking saves significant cycles.
FPU_POLICY_NONE) for tasks that provably never use floating-point — the compiler won’t emit FPU instructions, and the kernel skips all FPU context logic.Cortex-M FPU context switching is not a one-size-fits-all setting. Lazy stacking is the safe default for mixed workloads, but eager preservation eliminates non-deterministic latency spikes for FPU-intensive real-time tasks. A capable RTOS should expose this as a per-task policy, allowing the firmware engineer to match the hardware behavior to the task’s computational profile. The key insight: the deferred stacking exception is a hardware-managed trap with variable timing — if your deadline analysis cannot tolerate that variance, force eager save.
Quick Links
Legal Stuff





