HomeAbout UsContact Us

Debugging Hard Faults on ARM Cortex-M: A Systematic Approach

By Jithin Tom
July 15, 2026
3 min read
Debugging Hard Faults on ARM Cortex-M: A Systematic Approach

Table Of Contents

01
The Fault Escalation Hierarchy
02
Minimal HardFault Handler: Capture First, Analyze Later
03
Decoding the Stack Frame
04
CFSR Bitfield Reference (Quick Lookup)
05
Fault Escalation Flow
06
Common Fault Patterns & Root Causes
07
Post-Mortem Analysis Workflow
08
Advanced: Stack Unwinding for RTOS Tasks
09
Prevention Checklist (Code Review)
10
Summary
11
Related Reading
12
References
13
Frequently Asked Questions

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.


The Fault Escalation Hierarchy

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)
|
v
HardFault Handler (fixed priority -1)
|
v
Lockup (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 faults
  • MMFAR/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.


Minimal HardFault Handler: Capture First, Analyze Later

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 value
uint32_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 Status
uint32_t stack_frame[8]; // R0-R3, R12, LR, PC, xPSR
uint32_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;
#else
hardfault_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 here
while (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

Decoding the Stack Frame

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
0xFFFFFFF1Return to Handler mode, use MSP
0xFFFFFFF9Return to Thread mode, use MSP
0xFFFFFFFDReturn to Thread mode, use PSP
0xFFFFFFE1Return to Handler mode, use MSP (FPU context)
0xFFFFFFE9Return to Thread mode, use MSP (FPU)
0xFFFFFFEDReturn 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.


CFSR Bitfield Reference (Quick Lookup)

MMFSR (CFSR[7:0]) - MemManage Fault Status
Bit 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 error
Bit 7: MMARVALID - MMFAR holds valid fault address
BFSR (CFSR[15:8]) - BusFault Status
Bit 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 error
Bit 4: STKERR - Stacking bus error
Bit 5: BLSPERR - Lazy FPU bus error
Bit 7: BFARVALID - BFAR holds valid fault address
UFSR (CFSR[31:16]) - UsageFault Status
Bit 0: UNDEFINSTR - Undefined instruction
Bit 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:

  • Bit 1 (VECTTBL): Vector table read fault (hardfault on exception entry)
  • Bit 30 (FORCED): Configurable fault escalated to HardFault
  • Bit 31 (DEBUGEVT): Debug event (breakpoint, watchpoint) with debug disabled

Fault Escalation Flow

+==============================================================+
| 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) | |
| +---------------------+ |
| |
+==============================================================+

Common Fault Patterns & Root Causes

1. IBUSERR / PRECISERR — Flash/Peripheral Access Violation

Symptoms: CFSR = 0x00010001 (IBUSERR) or 0x00000200 (PRECISERR + BFARVALID) Causes:

  • Executing from invalid address (branch to NULL, corrupted function pointer)
  • Reading/writing unmapped peripheral register (clock not enabled)
  • Flash wait states misconfigured for clock speed
  • External memory (FSMC/QSPI) timing violation

Debug: Check BFAR/PC correlation. If PC points to 0x00000000 region → NULL function pointer. If PC in peripheral space → clock gating.

2. DACCVIOL / IACCVIOL — MPU Region Violation

Symptoms: CFSR = 0x00000002 (DACCVIOL) or 0x00000001 (IACCVIOL) Causes:

  • Stack overflow into guarded region
  • Task accessing memory outside its MPU region
  • Executing from non-executable region (stack, data)

Fix: Verify MPU region configuration. Enable stack guard region (Region 7, size 32 bytes, no access) at stack limit.

3. INVSTATE — EPSR.T Bit Cleared

Symptoms: CFSR = 0x00020000 Causes:

  • Branch to address with LSB=0 (interworking error)
  • Corrupted LR on exception return
  • Loading PC from stack with T-bit=0

Debug: Check PC alignment. All Thumb instructions require PC[0]=1. If PC is even, the T-bit was cleared on load.

4. NOCP — FPU Access with CP10/11 Disabled

Symptoms: CFSR = 0x00080000 Causes:

  • Floating-point code compiled (-mfloat-abi=hard) but CPACR not configured
  • Context switch saving FPU registers without enabling FPU

Fix: In SystemInit() or before first FPU use:

#if (__FPU_PRESENT == 1)
SCB->CPACR |= (0xF << 20); // Enable CP10/CP11 full access
#endif

5. DIVBYZERO / UNALIGNED — Trappable UsageFaults

Symptoms: CFSR = 0x02000000 (DIVBYZERO) or 0x01000000 (UNALIGNED) Causes:

  • Integer division by zero (enabled via CCR.DIV_0_TRP)
  • Unaligned 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.


Post-Mortem Analysis Workflow

1. Dump .noinit via SWD/JTAG

# OpenOCD: dump 256 bytes from known symbol address
openocd -c "init; reset halt; mdb <hardfault_ctx_addr> 256 hardfault_dump.bin; shutdown"

2. Parse with Python

# parse_hardfault.py
import struct
with open("hardfault_dump.bin", "rb") as f:
data = f.read()
# Verify magic
magic = 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 CFSR
cfsr_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")

3. Correlate with Map File

# Find function containing fault PC
arm-none-eabi-addr2line -e firmware.elf -f 0x0800XXXX
# Or grep map file
grep -E " 0x0800[0-9A-F]{4} " firmware.map

Advanced: Stack Unwinding for RTOS Tasks

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 Thumb
uint32_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+4
uint32_t saved_fp = fp[0]; // Previous FP at fp+0
fault_stack[depth++] = saved_lr;
fp = (uint32_t *)saved_fp;
}
}

Compiler flags for reliable unwinding:

-fno-omit-frame-pointer

Prevention Checklist (Code Review)

CheckTool/Method
MPU regions cover all RAM/Flash, guard stack`arm-none-eabi-objdump -t firmware.elf | grep -E ‘stack
FPU enabled before any float usegrep -r "CPACR" src/
Null pointer checks on all function pointersStatic analysis (Cppcheck, MISRA)
Division by zero guardedassert(divisor != 0)
Stack watermarks monitoredFreeRTOS uxTaskGetStackHighWaterMark()
Assert handler captures contextCustom assert_failed() → HardFault-like capture
Watchdog enabled, windowedIWDG/WWDG config in SystemInit()

Summary

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:

  1. Capture first, analyze later — minimal handler, .noinit RAM, watchdog reset
  2. Read CFSR and HFSR before clearing — clearing CFSR loses root cause when FORCED=1
  3. Decode EXC_RETURN — tells you MSP vs PSP, FPU state, return mode
  4. Correlate PC with map fileaddr2line or map grep finds the line
  5. Enable traps in debugDIV_0_TRP, UNALIGN_TRP catch bugs early


References

  1. ARM, Cortex-M3/M4/M7 Technical Reference Manuals, ARM DDI 0403/0439/0489
  2. ARM, Cortex-M Programming Guide to Memory Barrier Instructions, ARM DAI 0321A
  3. Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, 3rd Ed., Newnes, 2014, ISBN 978-0124080829
  4. FreeRTOS, FreeRTOS Hard Fault Handler Guide, https://www.freertos.org/Documentation/02-Kernel/03-Supported-devices/04-Demos/Others/Debugging-Hard-Faults-On-Cortex-M-Microcontrollers
  5. Interrupt, How to debug a HardFault on an ARM Cortex-M MCU, https://interrupt.memfault.com/blog/cortex-m-hardfault-debug
  6. SEGGER, AN00016: Analyzing HardFaults on Cortex-M, https://www.segger.com/downloads/application-notes/AN00016

Frequently Asked Questions

What causes a HardFault on ARM Cortex-M?

HardFault occurs when a fault condition cannot be handled by a specific fault handler (MemManage, BusFault, UsageFault) either because the handler is disabled, priority is too high, or the fault escalates. Common causes include invalid memory access, unaligned access, division by zero, and stack overflow.

How do I extract the faulting instruction address from a HardFault?

In the HardFault handler, read the stacked PC from the stack frame. For MSP: `pc = ((uint32_t*)msp)[6]`; for PSP: `pc = ((uint32_t*)psp)[6]`. The PC points exactly to the faulting instruction for precise faults (like MemManage or UsageFault). For imprecise BusFaults, the PC points to an instruction executed after the fault occurred.

What is the difference between FORCED and precise HardFault escalation?

FORCED HardFault (HFSR.FORCED=1) means a configurable fault (MemManage/BusFault/UsageFault) escalated because its handler was disabled or priority was too high. Precise escalation occurs when the fault is inherently unrecoverable (e.g., vector table read error during exception entry).

Can I recover from a HardFault in production firmware?

Generally no — HardFault indicates unrecoverable system corruption. Best practice is to log fault context (PC, LR, CFSR, HFSR, stack) to non-volatile storage, trigger watchdog reset, and analyze post-mortem. Attempting recovery risks undefined behavior.

Why does my HardFault handler itself fault (double fault)?

Double fault occurs if the HardFault handler accesses invalid memory, uses exhausted stack, or enables interrupts that cause nested faults. Keep the handler minimal: capture registers, disable interrupts, log to reserved RAM, then reset. Never call complex functions or printf.

Tags

arm-cortex-mhardfaultdebuggingfault-handlersstack-unwindingcortex-m4cortex-m7

Share


Previous Article
FreeRTOS Heap Implementations Compared: Heap_1 to Heap_5
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Embedded Firmware Debugging Techniques: From Printf to JTAG
Embedded Firmware Debugging Techniques: From Printf to JTAG
July 01, 2026
4 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media