
Hard faults on ARM Cortex-M are the embedded equivalent of a blue screen — the CPU has encountered a condition it cannot recover from, and your carefully crafted firmware has come to an abrupt halt. Unlike higher-level exceptions, a HardFault provides no automatic recovery path. The processor saves context, vectors to HardFault_Handler, and waits. What happens next determines whether you ship a fix or ship a brick.
This article presents a systematic, production-hardened approach to HardFault diagnosis on Cortex-M3/M4/M7. We cover register capture, stack frame decoding, fault status register interpretation, and post-mortem analysis techniques that work in flash-constrained, RTOS-hosted environments.
Before diving into HardFault specifics, understand the escalation chain. Cortex-M implements a nested fault model:
MemManage Fault (CFSR.MMFSR) ---> Disabled or priority too high? ---> HardFault (HFSR.FORCED)BusFault (CFSR.BFSR) ---> Disabled or priority too high? ---> HardFault (HFSR.FORCED)UsageFault (CFSR.UFSR) ---> Disabled or priority too high? ---> HardFault (HFSR.FORCED)|vHardFault Handler (fixed priority -1)|vLockup (if HardFault itself faults)
Key registers:
CFSR (Configurable Fault Status Register) — 0xE000ED28: concatenation of MMFSR (byte 0), BFSR (byte 1), UFSR (bytes 2-3)HFSR (HardFault Status Register) — 0xE000ED2C: VECTTBL (bit 1), FORCED (bit 30), DEBUGEVT (bit 31)DFSR (Debug Fault Status Register) — 0xE000ED30: debug-related faultsMMFAR/BFAR — fault address registers (valid only if MMARVALID/BFARVALID set)Critical rule: CFSR bits are sticky. Read CFSR and HFSR before clearing them — writing 1 to CFSR bits clears them. If you clear CFSR first, you lose the root cause when HFSR.FORCED=1.
A production HardFault handler has one job: preserve context. No printf, no RTOS calls, no flash writes. Capture to a reserved RAM section, then reset.
// hardfault_handler.c// Place in .noinit section so data survives soft reset__attribute__((section(".noinit.hardfault"))) struct {uint32_t msp;uint32_t psp;uint32_t lr; // EXC_RETURN valueuint32_t cfsr;uint32_t hfsr;uint32_t dfsr;uint32_t mmfar;uint32_t bfar;uint32_t afsr;uint32_t abfsr; // Cortex-M7 Auxiliary Bus Fault Statusuint32_t stack_frame[8]; // R0-R3, R12, LR, PC, xPSRuint32_t magic; // 0xDEADBEEF = valid capture} hardfault_ctx;/* WARNING: This simple handler assumes MSP is valid. If a stack overflow corrupted* MSP, calling a C function here will trigger a double fault. A production* handler should test LR bit 2 in assembly, extract the active SP, and switch* to a safe dedicated stack before branching to C code. */void __attribute__((naked)) HardFault_Handler(void) {__asm volatile ("mrs r0, msp \n" // Main stack pointer"mrs r1, psp \n" // Process stack pointer"mov r2, lr \n" // Link register (EXC_RETURN)"b hardfault_capture\n"::: "r0", "r1", "r2", "memory");}void hardfault_capture(uint32_t msp, uint32_t psp, uint32_t lr) {// Determine which stack was active (bit 2 of EXC_RETURN)uint32_t *sp = (lr & 0x4) ? (uint32_t *)psp : (uint32_t *)msp;hardfault_ctx.msp = msp;hardfault_ctx.psp = psp;hardfault_ctx.lr = lr;hardfault_ctx.cfsr = SCB->CFSR;hardfault_ctx.hfsr = SCB->HFSR;hardfault_ctx.dfsr = SCB->DFSR;hardfault_ctx.mmfar = SCB->MMFAR;hardfault_ctx.bfar = SCB->BFAR;hardfault_ctx.afsr = SCB->AFSR;#if defined(__CORTEX_M) && (__CORTEX_M == 7U)hardfault_ctx.abfsr = SCB->ABFSR;#elsehardfault_ctx.abfsr = 0;#endif// Stack frame: R0-R3, R12, LR, PC, xPSR (8 words)for (int i = 0; i < 8; i++) {hardfault_ctx.stack_frame[i] = sp[i];}hardfault_ctx.magic = 0xDEADBEEF;// Disable interrupts, trigger watchdog reset__disable_irq();NVIC_SystemReset();// Should never reach herewhile (1) { __asm("wfi"); }}
Linker script reservation:
/* Reserve 256 bytes in RAM for fault context, no initialization */.noinit.hardfault (NOLOAD) :{. = ALIGN(4);*(.noinit.hardfault). = ALIGN(4);} > RAM
The ARMv7-M exception entry pushes 8 registers (32 bytes) onto the active stack:
+------------------+ <-- SP after exception entry (MSP or PSP)| xPSR | sp[7] -- EPSR T-bit (bit 24); 1 for Thumb state+------------------+| PC | sp[6] -- Return address (faulting instruction for precise faults)+------------------+| LR | sp[5] -- Saved Link Register (return address of interrupted function)+------------------+| R12 | sp[4]+------------------+| R3 | sp[3]+------------------+| R2 | sp[2]+------------------+| R1 | sp[1]+------------------+| R0 | sp[0]+------------------+
Extract faulting PC:
uint32_t fault_pc = hardfault_ctx.stack_frame[6]; // PC from stack frame// For precise faults, this is exactly the faulting instruction address.// (Do not subtract bytes unless unwinding an interrupt or imprecise fault).
EXC_RETURN (LR) decoding:
| Value (hex) | Meaning |
|---|---|
| 0xFFFFFFF1 | Return to Handler mode, use MSP |
| 0xFFFFFFF9 | Return to Thread mode, use MSP |
| 0xFFFFFFFD | Return to Thread mode, use PSP |
| 0xFFFFFFE1 | Return to Handler mode, use MSP (FPU context) |
| 0xFFFFFFE9 | Return to Thread mode, use MSP (FPU) |
| 0xFFFFFFED | Return to Thread mode, use PSP (FPU) |
Bit 2 (0x4) = 1 → PSP was active; 0 → MSP active. Bit 4 (0x10) = 0 → Extended frame (FPU state preserved); 1 → Standard frame.
MMFSR (CFSR[7:0]) - MemManage Fault StatusBit 0: IACCVIOL - Instruction access violation (MPU/Execute Never)Bit 1: DACCVIOL - Data access violation (MPU)Bit 3: MUNSTKERR - Unstacking error (MemManage on exception return)Bit 4: MSTKERR - Stacking error (MemManage on exception entry)Bit 5: MLSPERR - Lazy FPU state preservation errorBit 7: MMARVALID - MMFAR holds valid fault addressBFSR (CFSR[15:8]) - BusFault StatusBit 0: IBUSERR - Instruction bus error (prefetch abort)Bit 1: PRECISERR - Precise data bus error (BFAR valid)Bit 2: IMPRECISERR - Imprecise data bus error (write buffer)Bit 3: UNSTKERR - Unstacking bus errorBit 4: STKERR - Stacking bus errorBit 5: BLSPERR - Lazy FPU bus errorBit 7: BFARVALID - BFAR holds valid fault addressUFSR (CFSR[31:16]) - UsageFault StatusBit 0: UNDEFINSTR - Undefined instructionBit 1: INVSTATE - Invalid EPSR state (T-bit=0, illegal IT)Bit 2: INVPC - Invalid PC load (EXC_RETURN integrity)Bit 3: NOCP - No coprocessor (FPU disabled, CP10/11 access)Bit 8: UNALIGNED - Unaligned access (when CCR.UNALIGN_TRP=1)Bit 9: DIVBYZERO - Divide by zero (when CCR.DIV_0_TRP=1)
HFSR bits:
VECTTBL): Vector table read fault (hardfault on exception entry)FORCED): Configurable fault escalated to HardFaultDEBUGEVT): Debug event (breakpoint, watchpoint) with debug disabled+==============================================================+| CORTEX-M FAULT ESCALATION HIERARCHY |+==============================================================+| ||MemManage Fault BusFault UsageFault ||(MPU violation) (Bus error) (Undefined instr, ||CFSR[7:0] CFSR[15:8] div0, unaligned) || | | | || |Handler enabled Y/N| | || +---- NO ---------- + | || | | | || v v v || +-------------------+--------+--------------------+ || |HFSR.FORCED = 1 |escalated|to HardFault | || +-------------------------------------------------+ || | || v || +---------------------+ || |Reset (Priority -3) | || |NMI (Priority -2) | || |HardFault Handler | || |(Priority -1, | || |cannot be masked) | || +----------+-----------+ || | || v || +---------------------+ || |Post-Mortem | || |Analysis (JTAG SWD | || |SWD, .noinit dump) | || +---------------------+ || |+==============================================================+
IBUSERR / PRECISERR — Flash/Peripheral Access ViolationSymptoms: CFSR = 0x00010001 (IBUSERR) or 0x00000200 (PRECISERR + BFARVALID)
Causes:
Debug: Check BFAR/PC correlation. If PC points to 0x00000000 region → NULL function pointer. If PC in peripheral space → clock gating.
DACCVIOL / IACCVIOL — MPU Region ViolationSymptoms: CFSR = 0x00000002 (DACCVIOL) or 0x00000001 (IACCVIOL)
Causes:
Fix: Verify MPU region configuration. Enable stack guard region (Region 7, size 32 bytes, no access) at stack limit.
INVSTATE — EPSR.T Bit ClearedSymptoms: CFSR = 0x00020000
Causes:
Debug: Check PC alignment. All Thumb instructions require PC[0]=1. If PC is even, the T-bit was cleared on load.
NOCP — FPU Access with CP10/11 DisabledSymptoms: CFSR = 0x00080000
Causes:
-mfloat-abi=hard) but CPACR not configuredFix: In SystemInit() or before first FPU use:
#if (__FPU_PRESENT == 1)SCB->CPACR |= (0xF << 20); // Enable CP10/CP11 full access#endif
DIVBYZERO / UNALIGNED — Trappable UsageFaultsSymptoms: CFSR = 0x02000000 (DIVBYZERO) or 0x01000000 (UNALIGNED)
Causes:
CCR.DIV_0_TRP)LDRD/STRD or LDR/STR to non-aligned address (enabled via CCR.UNALIGN_TRP)Fix: Enable traps in debug builds only. In production, handle gracefully or let HardFault catch.
.noinit via SWD/JTAG# OpenOCD: dump 256 bytes from known symbol addressopenocd -c "init; reset halt; mdb <hardfault_ctx_addr> 256 hardfault_dump.bin; shutdown"
# parse_hardfault.pyimport structwith open("hardfault_dump.bin", "rb") as f:data = f.read()# Verify magicmagic = struct.unpack("<I", data[72:76])[0]assert magic == 0xDEADBEEF, "Invalid magic"msp, psp, lr = struct.unpack("<III", data[0:12])cfsr, hfsr, dfsr = struct.unpack("<III", data[12:24])mmfar, bfar, afsr, abfsr = struct.unpack("<IIII", data[24:40])stack_frame = struct.unpack("<8I", data[40:72])print(f"LR (EXC_RETURN): 0x{lr:08X} -> {'PSP' if lr & 0x4 else 'MSP'} active")print(f"CFSR: 0x{cfsr:08X}")print(f"HFSR: 0x{hfsr:08X}")if abfsr != 0:print(f"ABFSR: 0x{abfsr:08X} (M7 Cache/TCM Fault)")print(f"Fault PC: 0x{stack_frame[6]:08X}")print(f"Fault LR: 0x{stack_frame[5]:08X}")# Decode CFSRcfsr_bits = {0: "IACCVIOL", 1: "DACCVIOL", 3: "MUNSTKERR", 4: "MSTKERR", 5: "MLSPERR",7: "MMARVALID", 8: "IBUSERR", 9: "PRECISERR", 10: "IMPRECISERR",11: "UNSTKERR", 12: "STKERR", 13: "BLSPERR", 15: "BFARVALID",16: "UNDEFINSTR", 17: "INVSTATE", 18: "INVPC", 19: "NOCP",24: "UNALIGNED", 25: "DIVBYZERO"}for bit, name in cfsr_bits.items():if cfsr & (1 << bit):print(f" CFSR.{name} = 1")
# Find function containing fault PCarm-none-eabi-addr2line -e firmware.elf -f 0x0800XXXX# Or grep map filegrep -E " 0x0800[0-9A-F]{4} " firmware.map
When using FreeRTOS/Zephyr, the faulting task may use PSP. The HardFault handler captures PSP, but the task’s stack frame is buried under kernel context. To unwind:
// In hardfault_capture(), after saving basic context:if (lr & 0x4) { // PSP was active// PSP points to task stack frame (R0-R3, R12, LR, PC, xPSR)// But task may have been preempted -- need to walk stack frames// using frame pointers (R7) if compiled with -fno-omit-frame-pointer// Note: R7 is NOT pushed by hardware on exception entry. It must be captured// in the assembly handler directly from the CPU register, or read from// the task's TCB if it was preempted. Assuming we captured it into `cpu_r7`:uint32_t *fp = (uint32_t *)cpu_r7; // R7 = frame pointer in Thumbuint32_t fault_stack[16] = {0};int depth = 0;while (fp && depth < 16 && (uint32_t)fp > 0x20000000 && (uint32_t)fp < 0x20040000) {uint32_t saved_lr = fp[1]; // LR at fp+4uint32_t saved_fp = fp[0]; // Previous FP at fp+0fault_stack[depth++] = saved_lr;fp = (uint32_t *)saved_fp;}}
Compiler flags for reliable unwinding:
-fno-omit-frame-pointer
| Check | Tool/Method |
|---|---|
| MPU regions cover all RAM/Flash, guard stack | `arm-none-eabi-objdump -t firmware.elf | grep -E ‘stack |
| FPU enabled before any float use | grep -r "CPACR" src/ |
| Null pointer checks on all function pointers | Static analysis (Cppcheck, MISRA) |
| Division by zero guarded | assert(divisor != 0) |
| Stack watermarks monitored | FreeRTOS uxTaskGetStackHighWaterMark() |
| Assert handler captures context | Custom assert_failed() → HardFault-like capture |
| Watchdog enabled, windowed | IWDG/WWDG config in SystemInit() |
HardFaults are not mysteries — they are precise architectural signals. The difference between a 2-hour debug session and a 2-day one is a disciplined HardFault handler that captures CFSR, HFSR, PC, LR, and stack frame to non-volatile RAM before reset. Combine this with MPU stack guards, FPU enable checks, and post-mortem tooling, and the “random crash” becomes a traceable bug with a clear fix.
Key takeaways:
.noinit RAM, watchdog resetaddr2line or map grep finds the lineDIV_0_TRP, UNALIGN_TRP catch bugs earlyQuick Links
Legal Stuff





