2026-06-22 · Davide Carrese

Debugging HardFault su STM32 — leggere i registri di fault Cortex-M senza debugger

STM32 · Cortex-M · Fault · Debugging

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:

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:

BitNomeLarghezzaFlag principali
31:16MemManage (MMFSR)8MMARVALID, MSTKERR, MUNSTKERR, DACCVIOL, IACCVIOL
15:8BusFault (BFSR)8BFARVALID, STKERR, UNSTKERR, IMPRECISERR, PRECISERR, IBUSERR
7:0UsageFault (UFSR)16DIVBYZERO, 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:

Registri di indirizzo del fault

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:

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:

  1. Aggiungi l'HardFault_Handler naked in main.c o in un fault_handler.c dedicato. Non affidarti al weak default — fa solo un loop.
  2. Abilita i fault configurabili in SystemInit() o all'inizio di main(). Scrivi in SCB->SHCSR per abilitare gli handler UsageFault, BusFault e MemManage. Senza, scalano silenziosamente.
  3. Abilita le trap DIVBYZERO e UNALIGNED via SCB->CCR. Catturare una divisione per zero con printf è infinitamente meglio che inseguire un HardFault random giorni dopo.
  4. 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.
  5. Cross-riferisci il PC con l'ELF del build. Tengo un target make fault-lookup che chiama arm-none-eabi-addr2line sull'ultimo artefatto di build.
  6. 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:

  1. Aggiungo un file fault_handler.c con il wrapper assembly naked e la funzione di cattura mostrati sopra.
  2. Abilito tutti e tre gli handler di fault configurabili in SystemInit_Ext (un hook chiamato presto).
  3. Instrado l'output UART sulla porta seriale di debug — tipicamente USART2 su STM32F4 Nucleo, o PA9/PA10 se uso UART1.
  4. Deployo il firmware, riproduce il fault, catturo l'output.
  5. Cerco il PC con addr2line e 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 da addr2line.

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

Comments

Have comments? Send me an email.