Perché l'FPU Va Abilitata Espressamente

I core Cortex-M4F e M7 montano l'FPU nel silicio ma la mantengono spenta di default. È una scelta voluta: l'FPU consuma circa 0.9–1.5 mW a 168 MHz (tipico STM32F4), e un firmware che non usa mai float non deve pagare quel costo. L'FPU è abilitata tramite il registro CPACR (Co-Processor Access Control Register), all'indirizzo 0xE000ED88, nei bit CP10 e CP11.

Scrivere CP10=0b11 e CP11=0b11 (accesso completo) è tutto ciò che serve per accendere l'FPU. Ma ci sono due insidie:

  1. La scrittura deve avvenire prima che qualsiasi istruzione floating-point venga eseguita — altrimenti il core scatena un UsageFault con flag NOCP (No Co-Processor).
  2. I registri dell'FPU (S0–S31, FPSCR e affini) necessitano di uno stato definito dopo l'accensione. FPCCR controlla se l'hardware li inizializza automaticamente.

I Registri Che Devi Conoscere

RegistroIndirizzoScopo
CPACR0xE000ED88Abilita l'FPU (campi CP10, CP11)
FPCCR0xE000EF34Controllo lazy stacking e stato FPU
FPCAR0xE000EF38Indirizzo ultimo salvataggio contesto FPU
FPSCR0xE000EF48Stato e controllo floating-point (arrotondamento, flag)

Il CPACR è il più importante. I primi 10 bit sono organizzati così:

Bit  9:8 → campo CP10  (accesso FPU)
Bit 11:10 → campo CP11 (accesso FPU)
  0b00 = nessun accesso (privilegiato + user → UsageFault)
  0b01 = solo accesso privilegiato (usato raramente)
  0b11 = accesso completo (privilegiato + user)

La sequenza di abilitazione canonica è:

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

I barrier __DSB() e __ISB() sono fondamentali: garantiscono che la scrittura del CPACR sia visibile alla pipeline prima che la successiva istruzione floating-point venga caricata.

Dove Va Messo Questo Codice?

Nei progetti basati su CMSIS, l'abilitazione dell'FPU è già gestita in SystemInit() dentro system_stm32f4xx.c (o l'equivalente per la tua famiglia):

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

Le due macro sono definite nell'header del dispositivo (stm32f4xx.h ecc.) e controllate dalle define del preprocessore del compilatore. Se usi -mfloat-abi=soft nonostante il chip abbia l'FPU, __FPU_USED resta 0 e l'FPU non viene mai abilitata — in silenzio. Il UsageFault scatta appena una libreria o un codice di terze parti fa un'operazione in virgola mobile.

Nei progetti greenfield senza CMSIS (startup a livello di registro puro), devi aggiungere le tre righe in Reset_Handler prima di chiamare __main o main. Qualsiasi codice floating-point in costruttori globali o inizializzatori statici che viene eseguito prima dell'abilitazione causerà un fault.

Lazy Stacking — La Feature di Performance Che Morde

Cortex-M4F e M7 implementano il lazy stacking per i registri FPU. Invece di salvare tutti i 32 registri float (S0–S31) più FPSCR nello stack a ogni interrupt — 136 byte — il core marca lo stato FPU come "abbandonato" e lo salva solo quando un handler usa effettivamente un'istruzione floating-point.

Il registro FPCCR (FPU Context Control Register) controlla questo comportamento:

  • LSPEN (bit 30): lazy state preservation enable. Impostato a 1 di default al reset — l'FPU usa il lazy stacking.
  • MONRDY (bit 27): monitor ready. Deve essere 1 perché il lazy stacking funzioni.

Il problema: se un handler di interrupt usa il floating-point mentre il codice interrotto stava già usando l'FPU, il meccanismo di lazy state deve prima salvare il contesto precedente. Se lo stack pointer non è valido o l'handler è in una modalità stack speciale (come l'MPU fault handler), il salvataggio lazy può scrivere all'indirizzo sbagliato, corrompendo lo stack e causando un hard fault che non assomiglia affatto a un tipico errore di FPU.

La regola: o ti assicuri che tutti gli handler di interrupt che toccano float siano compilati in modalità di register-banking (senza lazy), o meglio ancora, tieni il codice floating-point fuori dalle ISR. Bufferizza i dati in interi e processali nel main loop.

Se devi assolutamente usare float in una ISR, disabilita il lazy stacking azzerando LSPEN in FPCCR e paga il costo pieno di 136 byte di salvataggio contesto a ogni entrata e uscita:

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

ABI del Compilatore: Soft, SoftFP, Hard

L'ARM EABI definisce tre convenzioni di chiamata per il floating-point, e scegliere quella sbagliata è l'errore di deployment più frequente:

FlagUsa Registri FPUFloat nei Registri FPUFloat nei Registri Core
-mfloat-abi=softNo—Tutti tramite registri core + librerie software
-mfloat-abi=softfpSìPrimi 4 argomenti, poi stackRegistri core e stack per argomenti restanti
-mfloat-abi=hardSìTutti gli argomenti float via S0–S15—

Il dettaglio critico: softfp e hard sono incompatibili al link time — usano regole di passaggio parametri diverse. Una libreria compilata con -mfloat-abi=soft linka correttamente con un main che usa -mfloat-abi=soft o -mfloat-abi=softfp (entrambi passano i float nei registri core), ma linkare oggetti softfp con oggetti hard corrompe i primi quattro parametri float perché hard li aspetta in S0–S3 mentre softfp li ha messi in R0–R3.

La mia raccomandazione per progetti STM32:

  • Usa sempre -mfloat-abi=softfp -mfpu=fpv4-sp-d16 su Cortex-M4F (FPv4-SP-D16).
  • Usa -mfloat-abi=softfp -mfpu=fpv5-d16 su Cortex-M7 (FPv5-D16, supporto doppia precisione).
  • Usa -mfloat-abi=hard solo se ogni singolo componente al link time — CMSIS, middleware e librerie di terze parti — è compilato con la stessa ABI hard-float. Un singolo oggetto softfp corrompe silenziosamente i tuoi argomenti.

Esempio Pratico: Abilitare e Verificare l'FPU su STM32F4

Ecco un test standalone che funziona su qualsiasi scheda STM32F4xx. Abilita l'FPU, esegue un'operazione float e lampeggia un LED in caso di successo:

#include "stm32f4xx.h"

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

int main(void) {
    /* Abilita clock GPIOD (LED su PD12 per STM32F4-Discovery) */
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    GPIOD->MODER |= (1UL << 24);  /* PD12 come output */

    /* FPU ora abilitata. Eseguiamo un'operazione float. */
    volatile float a = 3.14159f;
    volatile float b = 2.71828f;
    volatile float c = a * b;    /* moltiplica in un ciclo */

    /* Se arriviamo qui, l'FPU funziona. Lampeggia per sempre. */
    while (1) {
        GPIOD->ODR ^= (1UL << 12);
        for (volatile uint32_t i = 0; i < 1000000; i++) { (void)i; }
    }
}

Compila con:

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

Se il LED lampeggia, l'FPU è attiva. Se resta spento, controlla il flag -mfpu e conferma che SystemInit venga eseguito prima di main.

Hard Fault Comuni e Come Diagnosticarli

1. UsageFault NOCP — FPU non abilitata

Sintomi: hard fault alla prima istruzione float. Controlla UFSR.NOCP nel UsageFault Status Register (a 0xE000ED2A). Se è impostato, la scrittura sul CPACR non è mai avvenuta, o è avvenuta troppo tardi.

2. INVSTATE durante lazy-save — corruzione stack handler

Sintomi: hard fault all'uscita da un interrupt dopo che un handler ha usato float. L'hardware lazy-stacking ha tentato di salvare il contesto abortito su uno stack corrotto o allineato male. Controlla BFARVALID in CFSR — se il BusFault Address Register ha un valore, il lazy save ha scritto all'indirizzo sbagliato.

3. UNDEFINSTR dopo context switch (RTOS)

Sintomi: funziona nel main loop, crash al primo context switch RTOS verso un task che usa float. Lo scheduler RTOS deve salvare/ripristinare esplicitamente il contesto FPU. FreeRTOS gestisce questo con configUSE_TASK_FPU_SUPPORT = 1. Senza, il context switch corrompe FPSCR e il task ripristinato vede dati spazzatura.

4. Stallo da subnormal NaN su M7

Sintomi: su Cortex-M7, certi operandi NaN o subnormal causano stalli lunghi (fino a 30 cicli) invece di eccezioni rapide. Il bit FPSCR.FZ (flush-to-zero) va impostato nei firmware real-time per evitare questi stalli:

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

Checklist Pratica

  1. ☐ CPACR — l'abilitazione FPU è in SystemInit o Reset_Handler? Verifica con debugger readback.
  2. ☐ Preprocessore — __FPU_PRESENT e __FPU_USED sono correttamente impostati per il tuo dispositivo?
  3. ☐ Flag del compilatore — -mfloat-abi corrisponde in TUTTE le unità di compilazione. Mischiare softfp con hard causa corruzione silenziosa degli argomenti.
  4. ☐ Selezione CPU — -mcpu=cortex-m4 per F4/L4/G4; -mcpu=cortex-m7 per H7. Niente --fix-cortex-m3-ldrd su M4/M7 a meno che non conosci l'erratum.
  5. ☐ RTOS — abilita il salvataggio contesto FPU nella configurazione del tuo RTOS. FreeRTOS: configUSE_TASK_FPU_SUPPORT = 1.
  6. ☐ Flush-to-zero — imposta FPSCR.FZ e FPSCR.DN nei loop time-critical.
  7. ☐ Lazy stacking — tieni i float fuori dalle ISR. Se non puoi, disabilita il lazy stacking via FPCCR.LSPEN.
  8. ☐ Linker script — assicurati che lo stack sia allineato a 8 byte. L'AAPCS lo richiede per LDRD/STRD, e i salvataggi lazy dell'FPU vanno in fault su stack non allineati.

Come Lo Affronterei Su un Progetto Cliente

Su un nuovo progetto STM32F4 o H7, i primi cinque minuti li dedico all'FPU. Prima di qualsiasi inizializzazione periferica, scrivo SCB->CPACR |= (3UL << 10) | (3UL << 11) nell'handler di reset, verifico con un breakpoint readback, e poi compilo una moltiplicazione float banale nel primo main loop. Appena il LED lampeggia, faccio commit dello startup.

Per progetti di migrazione (aggiornamento da STM32F1 senza FPU a F4 o H7), è quasi certo che la codebase legacy usi -mfloat-abi=soft. Lo mantengo così per il primo porting — faccio funzionare tutte le periferiche, valido i tempi, e solo dopo passo a -mfloat-abi=softfp in un branch dedicato. Questo isola le regressioni legate all'FPU dal rumore di fondo della migrazione.

Nei progetti FreeRTOS, imposto configUSE_TASK_FPU_SUPPORT = 1 anche se nessun task corrente usa float — il costo è trascurabile e previene un futuro sviluppatore dal beccare l'hard fault INVSTATE quando aggiunge un controllore PID al task del sensore.

Su Cortex-M7 (STM32H7), imposto sempre FPSCR.FZ e DN nella sequenza di startup. La modalità flush-to-zero elimina il caso limite del subnormal che costa 30 cicli per operazione e genera jitter nei loop DSP time-critical.

Fonti e Riferimenti

  • ARM Architecture Reference Manual ARMv7-M — B3.2: descrizione registri System Control (CPACR)
  • ARM Cortex-M4 Generic User Guide — 3.3.1: introduzione all'FPU
  • ARM Cortex-M7 Processor TRM — salvataggio contesto FPU e 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: definizioni __FPU_PRESENT e CPACR