Debugging HardFault su STM32 — leggere i registri di fault Cortex-M senza debugger
Niente di peggio di una scheda in produzione che non dà segni di vita, un watchdog che abbaia ogni pochi secondi e nessun JTAG/SWD a portata di mano. L'HardFault_Handler è la tua ultima linea di difesa — ecco come farlo parlare.
Gli HardFault sono l'equivalente embedded del segfault su Linux. Su Cortex-M, la CPU entra nel vettore di eccezione HardFault quando incontra una condizione di errore irrecoverabile — o quando il fault scala da un'eccezione configurabile (MemManage, BusFault, UsageFault) il cui handler non è stato abilitato o installato. Il risultato è lo stesso: il program counter salta a HardFault_Handler e a meno che tu non catturi il contesto, non hai idea di cosa sia andato storto.
In produzione raramente hai un debugger collegato. UART seriale, un dump flash di ultima istanza, o persino un LED lampeggiante potrebbero essere i tuoi unici canali di output. Questo articolo spiega l'architettura dei fault su Cortex-M, i registri da leggere, e un'implementazione collaudata di HardFault_Handler che decompone la causa del fallimento senza GDB.
Anatomia di un HardFault su Cortex-M
Il modello di eccezione Cortex-M distingue quattro tipi di fault. Solo l'HardFault (eccezione numero 3, priorità -1) è sempre abilitato. Gli altri tre sono opzionali:
- MemManage Fault — violazione di regione MPU (solo Cortex-M3/M4/M7/M33). Scala a HardFault se l'handler MemManage non è abilitato o se il fault è avvenuto in modo asincrono durante la lettura della vettore table.
- BusFault — errore di accesso alla memoria sul bus di sistema: timeout slave, risposta di errore AHB/APB, o prefetch abort nel fetch di un'istruzione da un indirizzo inesistente.
- UsageFault — istruzione indefinita, accesso non allineato con SCB.CCR.UNALIGN_TRP impostato, divisione per zero con CCR.DIV_0_TRP impostato, o un valore EXC_RETURN non valido.
Quando uno di questi si verifica e l'handler corrispondente non è configurato, o il fault è avvenuto durante il fetch del vettore stesso, il processore scala all'handler HardFault a priorità fissa. Capire quale fault sottostante ha innescato la scalata è il primo passo.
I registri da leggere
Tutti i dati diagnostici vivono nei registri del System Control Block (SCB), accessibili a indirizzi fissi. Devi leggerli dentro HardFault_Handler prima di qualsiasi operazione che potrebbe modificarli (chiamate a funzione, push sullo stack con certe ottimizzazioni).
Configurable Fault Status Register — CFSR (0xE000ED28)
Il CFSR a 32 bit è il registro più importante. Contiene tre sotto-registri:
| Bit | Nome | Larghezza | Flag principali |
|---|---|---|---|
| 31:16 | MemManage (MMFSR) | 8 | MMARVALID, MSTKERR, MUNSTKERR, DACCVIOL, IACCVIOL |
| 15:8 | BusFault (BFSR) | 8 | BFARVALID, STKERR, UNSTKERR, IMPRECISERR, PRECISERR, IBUSERR |
| 7:0 | UsageFault (UFSR) | 16 | DIVBYZERO, UNALIGNED, NOCP, INVPC, INVSTATE, UNDEFINSTR |
Su Cortex-M33/M55 la disposizione è la stessa ma esistono flag aggiuntivi (es. STKOF per stack overflow su M33 con Main Stack Extension).
HardFault Status Register — HFSR (0xE000ED2C)
Questo dice perché è stato preso l'HardFault. I due bit critici:
- FORCED (bit 30) — impostato quando un fault configurabile (MemManage/BusFault/UsageFault) è scalato. Devi leggere CFSR per scoprire quale.
- VECTTBL (bit 1) — impostato quando un bus fault è avvenuto durante la lettura della vettore table (un branch verso un indirizzo handler non valido). È uno show-stopper: la vettore table o VTOR è corrotta.
Registri di indirizzo del fault
- MMFAR (0xE000ED34) — valido quando MMFSR.MMARVALID è impostato. Contiene l'indirizzo che ha causato la violazione MPU.
- BFAR (0xE000ED38) — valido quando BFSR.BFARVALID è impostato. Contiene l'indirizzo che ha causato il bus fault preciso.
Questi sono preziosissimi: ti dicono quale indirizzo veniva acceduto, il che punta spesso direttamente a un dereferenziamento di NULL pointer, un puntatore stale, o un registro periferica acceduto prima che il suo clock fosse abilitato.
Recuperare il contesto impilato
Quando il processore prende un'eccezione di fault, spinge otto registri sullo stack corrente (MSP o PSP a seconda dello stack pointer attivo prima del fault): R0, R1, R2, R3, R12, LR (EXC_RETURN), PC (l'istruzione faultante), e xPSR.
Il PC impilato è il valore singolarmente più utile: è l'istruzione che stava venendo eseguita quando il fault è avvenuto. Con un file ELF/Map o una ricerca addr2line, ottieni la funzione e la linea di codice esatta. L'LR impilato ti dice il chiamante.
Per estrarre questi valori, ti serve il valore dello stack pointer al momento dell'ingresso nell'eccezione. Il modo più semplice senza assembly:
__attribute__((naked))
void HardFault_Handler(void) {
__asm volatile(
" movs r0, #4 \n"
" mov r1, lr \n" /* EXC_RETURN ci dice quale stack */
" tst r0, r1 \n"
" itte eq \n"
" mrseq r0, msp \n" /* se bit 1 clear → MSP */
" mrsne r0, psp \n" /* se bit 1 set → PSP */
" bl hard_fault_capture\n"
" b .\n" /* loop infinito; o NVIC_SystemReset */
);
}
typedef struct {
uint32_t r0, r1, r2, r3, r12, lr, pc, psr;
} FaultStack;
void hard_fault_capture(uint32_t *sp) {
FaultStack *frame = (FaultStack *)sp;
/* Legge i registri di fault */
uint32_t cfsr = *(volatile uint32_t *)0xE000ED28;
uint32_t hfsr = *(volatile uint32_t *)0xE000ED2C;
uint32_t mmfar = *(volatile uint32_t *)0xE000ED34;
uint32_t bfar = *(volatile uint32_t *)0xE000ED38;
/* Stampa su UART */
printf("=== HardFault ===\n");
printf("CFSR: 0x%08lX\n", (unsigned long)cfsr);
printf("HFSR: 0x%08lX FORCED=%lu VECTTBL=%lu\n",
(unsigned long)hfsr,
(unsigned long)((hfsr >> 30) & 1),
(unsigned long)((hfsr >> 1) & 1));
printf("Fault PC: 0x%08lX (stacked)\n", (unsigned long)frame->pc);
printf("Fault LR: 0x%08lX (caller)\n", (unsigned long)frame->lr);
if (cfsr & (1 << 7)) /* MMFSR.MMARVALID */
printf("MMFAR: 0x%08lX\n", (unsigned long)mmfar);
if (cfsr & (1 << 15)) /* BFSR.BFARVALID */
printf("BFAR: 0x%08lX\n", (unsigned long)bfar);
}
Su Cortex-M7 con cache L1 abilitata, attenzione: il PC e LR impilati potrebbero mostrare un indirizzo obsoleto se il fault è impreciso (BFSR.IMPRECISERR impostato). Un BusFault impreciso significa che la scrittura era bufferizzata nello store buffer e l'istruzione faultante è molto più avanti. In quel caso BFAR non è popolato e devi usare una barriera DSB prima del punto di fault, o abilitare BFHFNMIGN (a tuo rischio).
Esempio pratico: NULL pointer dereference su STM32F401
Considera uno scenario tipico da contractor: un progetto STM32F401 con FreeRTOS, un driver sensore custom, e una struttura dati condivisa allocata via pvPortMalloc. Il dispositivo crasha dopo esattamente 47 minuti di uptime.
Aggiungi l'HardFault_Handler qui sopra, deployi il firmware, e il prossimo crash produce:
=== HardFault === CFSR: 0x00008200 HFSR: 0x40000000 FORCED=1 VECTTBL=0 Fault PC: 0x08003A4C Fault LR: 0x08004B10
Decodifica di CFSR 0x00008200:
0x8200= bit 15:8 =0x82= BFSR. Bit 1 impostato = PRECISERR (errore preciso sul bus dati), bit 7 impostato = BFARVALID.- Questo è un BusFault preciso — l'accesso ai dati all'indirizzo in BFAR è fallito.
Leggi BFAR:
BFAR: 0x00000004
Ora cerchi il PC del fault (0x08003A4C):
arm-none-eabi-addr2line -e build/project.elf 0x08003A4C # Output: sensor_driver.c:142
La linea 142 di sensor_driver.c è:
status = sensor_ctx->config->sample_rate;
Il puntatore sensor_ctx era valido, ma config era NULL. BFAR = 0x00000004 significa che la CPU ha cercato di caricare dall'offset 4 della pagina NULL — cioè ((ConfigStruct *)0)->sample_rate. La causa principale: un puntatore config mai inizializzato prima che il sensore venisse usato in un percorso codice particolare, attivato da un timeout raro di lettura del sensore dopo 47 minuti.
Senza la cattura dell'HardFault, guarderesti una scheda morta. Con essa, la correzione è una guardia di inizializzazione di una riga.
Checklist pratica per progetti cliente
Quando inizio un nuovo contratto STM32 o mi unisco a un progetto esistente, la prima cosa che faccio è aggiungere una cattura HardFault al codice di startup. Ecco la mia checklist standard:
- Aggiungi l'HardFault_Handler naked in
main.co in unfault_handler.cdedicato. Non affidarti al weak default — fa solo un loop. - Abilita i fault configurabili in
SystemInit()o all'inizio dimain(). Scrivi inSCB->SHCSRper abilitare gli handler UsageFault, BusFault e MemManage. Senza, scalano silenziosamente. - Abilita le trap DIVBYZERO e UNALIGNED via
SCB->CCR. Catturare una divisione per zero conprintfè infinitamente meglio che inseguire un HardFault random giorni dopo. - Instrada l'output su una UART diagnostica (solo TX, 115200 8N1) con DMA o interrupt. Anche un buffer di 64 byte basta per dumpare CFSR/HFSR/PC.
- Cross-riferisci il PC con l'ELF del build. Tengo un target
make fault-lookupche chiamaarm-none-eabi-addr2linesull'ultimo artefatto di build. - Per firmware di produzione, aggiungi un log su flash. Scrivi CFSR, HFSR, BFAR/MMFAR e il PC impilato in un settore riservato della flash. Al prossimo boot, verifica la firma del fault e riportala su UART o BLE prima di continuare l'operazione normale.
Come lo affronterei su un progetto cliente
Quando mi unisco a un progetto con un HardFault intermittente, non modifico la logica applicativa. Invece:
- Aggiungo un file
fault_handler.ccon il wrapper assembly naked e la funzione di cattura mostrati sopra. - Abilito tutti e tre gli handler di fault configurabili in
SystemInit_Ext(un hook chiamato presto). - Instrado l'output UART sulla porta seriale di debug — tipicamente USART2 su STM32F4 Nucleo, o PA9/PA10 se uso UART1.
- Deployo il firmware, riproduce il fault, catturo l'output.
- Cerco il PC con
addr2linee correggo la causa principale — non il sintomo. Se BFAR punta a un indirizzo vicino a zero, il primo sospetto è un puntatore non inizializzato nella funzione riportata daaddr2line.
Questo approccio mi ha salvato giorni di debugging in almeno cinque progetti cliente distinti. Il costo una tantum di aggiungere l'handler (circa 30 minuti inclusa l'inizializzazione UART) si ripaga la prima volta che una scheda crasha dopo ore di operatività.
Fonti e approfondimenti
- How to debug a HardFault on an ARM Cortex-M MCU — Chris Coleman, Interrupt (Memfault)
- ARMv7-M Architecture Reference Manual (DDI 0403E) — Fault handling chapter (B1.5)
- STM32F401 Reference Manual (RM0368) — System and memory overview, SCB registers
- STM32H7 RM0433 — Cache coherency and imprecise BusFault handling
- ST Community — HardFault Handler debugging without debugger

Comments
Have comments? Send me an email.