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:
- La scrittura deve avvenire prima che qualsiasi istruzione floating-point venga eseguita — altrimenti il core scatena un
UsageFaultcon flag NOCP (No Co-Processor). - 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
| Registro | Indirizzo | Scopo |
|---|---|---|
| CPACR | 0xE000ED88 | Abilita l'FPU (campi CP10, CP11) |
| FPCCR | 0xE000EF34 | Controllo lazy stacking e stato FPU |
| FPCAR | 0xE000EF38 | Indirizzo ultimo salvataggio contesto FPU |
| FPSCR | 0xE000EF48 | Stato 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:
| Flag | Usa Registri FPU | Float nei Registri FPU | Float nei Registri Core |
|---|---|---|---|
-mfloat-abi=soft | No | — | Tutti tramite registri core + librerie software |
-mfloat-abi=softfp | Sì | Primi 4 argomenti, poi stack | Registri core e stack per argomenti restanti |
-mfloat-abi=hard | Sì | 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-d16su Cortex-M4F (FPv4-SP-D16). - Usa
-mfloat-abi=softfp -mfpu=fpv5-d16su Cortex-M7 (FPv5-D16, supporto doppia precisione). - Usa
-mfloat-abi=hardsolo 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
- ☐ CPACR — l'abilitazione FPU è in
SystemInitoReset_Handler? Verifica con debugger readback. - ☐ Preprocessore —
__FPU_PRESENTe__FPU_USEDsono correttamente impostati per il tuo dispositivo? - ☐ Flag del compilatore —
-mfloat-abicorrisponde in TUTTE le unità di compilazione. Mischiare softfp con hard causa corruzione silenziosa degli argomenti. - ☐ Selezione CPU —
-mcpu=cortex-m4per F4/L4/G4;-mcpu=cortex-m7per H7. Niente--fix-cortex-m3-ldrdsu M4/M7 a meno che non conosci l'erratum. - ☐ RTOS — abilita il salvataggio contesto FPU nella configurazione del tuo RTOS. FreeRTOS:
configUSE_TASK_FPU_SUPPORT = 1. - ☐ Flush-to-zero — imposta
FPSCR.FZeFPSCR.DNnei loop time-critical. - ☐ Lazy stacking — tieni i float fuori dalle ISR. Se non puoi, disabilita il lazy stacking via FPCCR.LSPEN.
- ☐ 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_PRESENTe CPACR

💬 Commenti
Hai domande, correzioni o esperienze con l'FPU sugli STM32? Scrivimi un'email. Includi lo slug dell'articolo nell'oggetto.