Why the FPU Needs Explicit Enabling

Cortex-M4F and M7 cores ship with the FPU logic on silicon but powered down by default. This is intentional: the FPU consumes around 0.9–1.5 mW at 168 MHz (STM32F4 typical), and a firmware that never uses floats should not pay that power tax. The FPU is gated by the Co-Processor Access Control Register (CPACR), specifically bits CP10 and CP11 in the system register space at address 0xE000ED88.

Writing CP10=0b11 and CP11=0b11 (full access) is all it takes to turn on the FPU. But there are two catches:

  1. The write must happen before any floating-point instruction executes — otherwise the core triggers a UsageFault with the NOCP (No Co-Processor) flag.
  2. The FPU registers (S0–S31, FPSCR, and friends) need a defined state after power-up. The FPCCR controls whether the hardware initialises them automatically.

The Registers You Must Know

RegisterAddressPurpose
CPACR0xE000ED88Enable/disable FPU (CP10, CP11 fields)
FPCCR0xE000EF34Lazy stacking enable, FPU state control
FPCAR0xE000EF38Address of last FP context save on stack
FPSCR0xE000EF48Floating-point status and control (rounding, flags)

The CPACR is by far the most important. Its lower 10 bits look like this:

Bit  9:8 → CP10 field  (FPU access)
Bit 11:10 → CP11 field (FPU access)
  0b00 = no access (privileged + user → UsageFault)
  0b01 = privileged access only (rarely used)
  0b11 = full access (privileged + user)

So the canonical enable sequence is:

/* Enable FPU: CP10=11, CP11=11 */
SCB->CPACR |= (3UL << 10) | (3UL << 11);
__DSB();
__ISB();

The __DSB() and __ISB() barriers are critical: they ensure the CPACR write is visible to the pipeline before the next floating-point instruction is fetched.

Where Does This Code Go?

In a CMSIS-based project, the FPU enable is handled inside SystemInit() in system_stm32f4xx.c (or the equivalent for your family):

/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
  SCB->CPACR |= (3UL << 10) | (3UL << 11);
  __DSB();
  __ISB();
#endif

The two macros are defined in the device header (stm32f4xx.h etc.) and controlled by the compiler's preprocessor defines. If you use -mfloat-abi=soft despite the chip having an FPU, __FPU_USED stays 0 and the FPU is never enabled — silently. The UsageFault will then trigger the moment any library or third-party code does a float operation.

On greenfield projects without CMSIS (pure register-level startup), you must add the three lines in Reset_Handler before __main or main is called. Any floating-point code in global constructors or static initialisers that runs before your enable will fault.

Lazy Stacking — The Performance Feature That Bites

Cortex-M4F and M7 implement lazy stacking for FPU registers. Instead of pushing all 32 float registers (S0–S31) plus FPSCR onto the stack on every interrupt — which costs 136 bytes — the core marks the FPU state as "abandoned" and only stacks it lazily when an interrupt handler actually uses a floating-point instruction.

The FPCCR (FPU Context Control Register) controls this:

  • LSPEN (bit 30): lazy state preservation enable. Set to 1 by default on reset — the FPU uses lazy stacking.
  • MONRDY (bit 27): monitor ready. Must be 1 for lazy stacking to work.

The problem: if an interrupt handler uses floating-point while the interrupted code was also using the FPU, the lazy-state machinery must save the old context first. If the stack pointer is invalid or the handler is in a special stack mode (like the MPU fault handler), the lazy save can push to the wrong address, corrupting the stack and causing a hard fault that looks nothing like a typical FPU error.

The rule: either ensure all interrupt handlers that touch floats are compiled with the FPU in register-banking mode (no lazy), or better, keep floating-point code out of ISRs entirely. Buffer the data in integers and process it in the main loop.

If you absolutely must use floats in an ISR, disable lazy stacking by clearing LSPEN in FPCCR and pay the full 136-byte context save on every entry and exit:

FPCCR->FPCCR &= ~(1UL << 30);  /* Disable lazy stacking */

Compiler ABI: Soft, SoftFP, Hard

The ARM EABI defines three floating-point calling conventions, and choosing the wrong one is the most frequent FPU deployment mistake:

FlagUses FPU RegistersFloats in FPU RegsFloats in Core Regs
-mfloat-abi=softNoAll through core regs + software libs
-mfloat-abi=softfpYesFirst 4 args, then stackCore regs and stack for remaining args
-mfloat-abi=hardYesAll float args through S0–S15

The critical detail: softfp and hard are incompatible at link time — they use different parameter-passing rules. A library compiled with -mfloat-abi=soft links correctly with a main that uses -mfloat-abi=soft or -mfloat-abi=softfp (both pass floats in core registers), but linking softfp-objects with hard-objects corrupts the first four float parameters because hard expects them in S0–S3 while softfp put them in R0–R3.

My recommendation for STM32 projects:

  • Always use -mfloat-abi=softfp -mfpu=fpv4-sp-d16 on Cortex-M4F (FPv4-SP-D16).
  • Use -mfloat-abi=softfp -mfpu=fpv5-d16 on Cortex-M7 (FPv5-D16, double-precision support).
  • Use -mfloat-abi=hard only if every single link-time component — including CMSIS, middleware, and third-party libraries — is compiled with the same hard-float ABI. A single softfp object will silently corrupt your arguments.

Practical Example: Enabling and Verifying the FPU on STM32F4

Here is a standalone test you can run on any STM32F4xx board. It enables the FPU, triggers a float operation, and blinks an LED on success:

#include "stm32f4xx.h"

void SystemInit(void) {
    /* Minimal setup: enable FPU */
    SCB->CPACR |= (3UL << 10) | (3UL << 11);
    __DSB();
    __ISB();
}

int main(void) {
    /* Enable GPIOD clock (LED on PD12 for STM32F4-Discovery) */
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    GPIOD->MODER |= (1UL << 24);  /* PD12 as output */

    /* FPU is now enabled. Do a float operation. */
    volatile float a = 3.14159f;
    volatile float b = 2.71828f;
    volatile float c = a * b;    /* single-cycle multiply */

    /* If we reach here, FPU works. Blink forever. */
    while (1) {
        GPIOD->ODR ^= (1UL << 12);
        for (volatile uint32_t i = 0; i < 1000000; i++) { (void)i; }
    }
}

Compile with:

arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb \
  -mfloat-abi=softfp -mfpu=fpv4-sp-d16 \
  -DSTM32F407xx -T stm32f407.ld \
  -o test-fpu.elf main.c

If the LED blinks, the FPU is live. If it stays off, check your -mfpu flag and confirm SystemInit executes before main.

Common Hard Fault Traps

1. NOCP UsageFault — FPU not enabled

Symptoms: hard fault on the first float instruction. Check UFSR.NOCP in the UsageFault Status Register (at 0xE000ED2A). If set, your CPACR write never happened, or happened too late.

2. INVSTATE on lazy-save — handler stack corruption

Symptoms: hard fault on interrupt exit after a handler that used floats. The lazy-stacking hardware tried to save the aborted context to a corrupted or misaligned stack. Check BFARVALID in CFSR — if the BusFault Address Register has a value, the lazy save pushed to the wrong address.

3. UNDEFINSTR after context switch (RTOS)

Symptoms: working fine in main loop, crashing after the first RTOS context switch into a task that uses floats. The RTOS scheduler needs to explicitly save/restore the FPU context. FreeRTOS manages this with configUSE_TASK_FPU_SUPPORT = 1. Without it, the context switch corrupts FPSCR and the restored task sees garbage.

4. Subnormal NaN stalls on M7

Symptoms: on Cortex-M7, certain NaN or subnormal operands cause long stalls (up to 30 cycles) instead of fast exceptions. The FPSCR.FZ (flush-to-zero) bit should be set in real-time firmware to avoid these stalls:

/* Set flush-to-zero and default NaN modes */
FPSCR |= (1 << 24);   /* FZ = 1 */
FPSCR |= (1 << 25);   /* DN = 1 */

Practical Checklist

  1. CPACR — is the FPU enable in SystemInit or Reset_Handler? Verify with a debugger readback.
  2. Preprocessor — are __FPU_PRESENT and __FPU_USED correctly set for your device?
  3. Compiler flags — match -mfloat-abi across ALL compile units. Mixing softfp with hard causes silent arg corruption.
  4. CPU selection-mcpu=cortex-m4 for F4/L4/G4; -mcpu=cortex-m7 for H7. No --fix-cortex-m3-ldrd on M4/M7 unless you understand the erratum.
  5. RTOS — enable FPU context save in your RTOS config. FreeRTOS: configUSE_TASK_FPU_SUPPORT = 1.
  6. Flush-to-zero — set FPSCR.FZ and FPSCR.DN in time-critical loops.
  7. Lazy stacking — keep floats out of ISRs. If you cannot, disable lazy stacking via FPCCR.LSPEN.
  8. Linker script — ensure the stack is 8-byte aligned. The AAPCS requires this for LDRD/STRD, and lazy FPU saves will fault on a misaligned stack.

How I Would Approach This on a Client Project

On a new STM32F4 or H7 project, my first five minutes go to the FPU. Before any peripheral init, I write SCB->CPACR |= (3UL << 10) | (3UL << 11) in the reset handler, verify with a breakpoint readback, and then compile a trivial float multiply in the first main loop. Once the LED blinks, I commit the startup.

For migration projects (upgrading from STM32F1 without FPU to F4 or H7), the legacy codebase almost certainly uses -mfloat-abi=soft. I keep it that way for the first port — get all the peripherals working, validate timing, and only then switch to -mfloat-abi=softfp in a dedicated branch. This isolates FPU-related regressions from the much larger migration noise.

On FreeRTOS projects, I set configUSE_TASK_FPU_SUPPORT = 1 even if no current task uses floats — the cost is negligible and it prevents a future developer from hitting the INVSTATE hard fault when they add a PID controller to the sensor task.

On Cortex-M7 (STM32H7), I always set FPSCR.FZ and DN in the startup sequence. The flush-to-zero mode eliminates the subnormal edge case that costs 30 cycles per operation and generates jitter in time-critical DSP loops.

Sources and References

  • ARM Architecture Reference Manual ARMv7-M — B3.2: System Control Register descriptions (CPACR)
  • ARM Cortex-M4 Generic User Guide — 3.3.1: About the FPU
  • ARM Cortex-M7 Processor TRM — FPU context save and lazy stacking
  • ST Application Note AN4044 — Floating Point Unit (FPU) on STM32F4 devices
  • ST Application Note AN4942 — Floating Point Unit (FPU) on STM32H7 devices
  • FreeRTOS FAQ — How to use floating point with FreeRTOS tasks
  • CMSIS-Core 5.6.0 — core_cm4.h: __FPU_PRESENT and CPACR definitions