Electronics Design AU
Firmware

How Do You Decode a Cortex-M HardFault Using the CFSR, HFSR, and MMFAR/BFAR Registers?

Last updated 9 July 2026 · 10 min read

Direct Answer

When a Cortex-M microcontroller HardFaults, four memory-mapped System Control Block registers record exactly what happened: HFSR (0xE000ED2C) says whether the fault escalated from another handler, CFSR (0xE000ED28) breaks down into MemManage, BusFault, and UsageFault status bytes identifying the specific violation, and MMFAR (0xE000ED34) / BFAR (0xE000ED38) hold the faulting memory address when the corresponding CFSR valid bit is set. Combined with the stacked register frame (R0–R3, R12, LR, PC, xPSR) pushed onto the stack at the moment of the fault, these registers usually pinpoint the exact faulting instruction and the bad address it tried to access — turning a generic crash into a specific line of code.

Detailed Explanation

A HardFault is the Cortex-M core's fallback exception — it fires when something goes wrong badly enough that no more specific handler is available, or when a more specific fault handler (MemManage, BusFault, UsageFault) is disabled and its fault escalates. Because HardFault runs at fixed priority −1 (below only Reset and NMI) and cannot be masked, it is the one exception handler every Cortex-M firmware project eventually has to actually debug — usually by disassembling a mystery crash rather than reading an obvious error message.

Before writing any code, it helps to understand the three-level fault model:

  1. A specific fault occurs — MemManage (memory protection violation), BusFault (bus/memory access error), or UsageFault (invalid instruction, invalid state, unaligned access, divide-by-zero if enabled).
  2. If that fault's own handler is enabled and higher priority than the current context, it runs. Most projects leave these disabled by default (they're optional on Cortex-M3/M4/M7; MemManage and BusFault don't exist at all on Cortex-M0/M0+), so step 3 is what actually happens in practice.
  3. The fault escalates to HardFault if its specific handler is disabled, or if the fault occurs while already inside a fault handler (a "fault within a fault"). The HFSR register records that this escalation happened.

The diagnostic information for all of this is preserved in four memory-mapped registers, part of the System Control Block, readable from any debugger or from firmware itself inside the fault handler.

The Fault Status Registers

RegisterAddressPurpose
HFSR (HardFault Status Register)0xE000ED2CSays why the HardFault handler ran — most commonly bit 30 (FORCED), meaning a lower-priority fault escalated. Bit 1 (VECTTBL) indicates a fault while reading the vector table itself — usually a corrupted or misconfigured vector table address.
CFSR (Configurable Fault Status Register)0xE000ED28A 32-bit register split into three 8/8/16-bit sub-registers. This is where the specific violation type lives.
MMFAR (MemManage Fault Address Register)0xE000ED34Holds the faulting address for a MemManage fault, but only when CFSR's MMARVALID bit is set.
BFAR (BusFault Address Register)0xE000ED38Holds the faulting address for a precise BusFault, but only when CFSR's BFARVALID bit is set.

CFSR is the most information-dense register and decodes into three byte/halfword fields:

  • MMFSR (bits 0–7, MemManage Fault Status) — IACCVIOL (instruction access violation), DACCVIOL (data access violation), MSTKERR/MUNSTKERR (fault during exception stacking/unstacking), MMARVALID (bit 7 — set if MMFAR holds a valid address).
  • BFSR (bits 8–15, BusFault Status) — IBUSERR (instruction bus error), PRECISERR (precise data bus error — BFAR is valid), IMPRECISERR (imprecise data bus error — BFAR is not valid because the CPU has already moved past the faulting instruction by the time the bus reports the error), UNSTKERR/STKERR (fault during exception stacking/unstacking), BFARVALID (bit 15 — set if BFAR holds a valid address).
  • UFSR (bits 16–31, UsageFault Status) — UNDEFINSTR (undefined instruction — often a corrupted function pointer or stack, causing execution to jump into data), INVSTATE (invalid EPSR state — usually a function pointer with the Thumb bit cleared), INVPC (invalid PC on exception return — a corrupted stack), NOCP (coprocessor access, e.g. FPU used without being enabled), UNALIGNED, DIVBYZERO.

An IMPRECISERR bus fault is a special case worth calling out separately: because it's imprecise, BFAR is invalid and the stacked PC does not point at the actual faulting instruction — the CPU had already retired several instructions before the bus reported the error. Diagnosing an imprecise bus fault usually requires narrowing it down by disabling write buffering (ACTLR.DISDEFWBUF on Cortex-M3/M4) during debugging to force faults to become precise, at a performance cost not suitable for production.

The Stacked Register Frame

The second half of the diagnostic picture is the register frame the core automatically pushes onto the active stack the instant an exception is taken: R0, R1, R2, R3, R12, LR, PC, and xPSR, in that order, at consecutive addresses starting from the stack pointer value at fault time.

The PC in this frame is the return address — for a HardFault, this is typically the address of the instruction that caused the fault (or the instruction immediately after it, in the imprecise case above). Cross-referencing this address against your .map file or disassembly identifies the exact function and, usually, the exact source line.

The LR value tells you what called the faulting function, giving one level of call-stack context beyond the fault site itself — useful because a corrupted-pointer bug often means the debugger's automatic stack unwinding can't walk any further back than this.

Writing a HardFault Handler That Captures This Data

The default HardFault_Handler in most vendor startup code is just while(1) {} — it halts the CPU but extracts nothing. A minimal diagnostic handler needs to determine which stack pointer (MSP or PSP) was active at the time of the fault, then pass that stack pointer to a C function that reads the frame:

void HardFault_Handler(void) {
    __asm volatile (
        "tst lr, #4                \n"
        "ite eq                    \n"
        "mrseq r0, msp              \n"
        "mrsne r0, psp              \n"
        "b hard_fault_handler_c     \n"
    );
}

void hard_fault_handler_c(uint32_t *stacked_regs) {
    volatile uint32_t r0  = stacked_regs[0];
    volatile uint32_t r1  = stacked_regs[1];
    volatile uint32_t r2  = stacked_regs[2];
    volatile uint32_t r3  = stacked_regs[3];
    volatile uint32_t r12 = stacked_regs[4];
    volatile uint32_t lr  = stacked_regs[5];
    volatile uint32_t pc  = stacked_regs[6];
    volatile uint32_t psr = stacked_regs[7];

    volatile uint32_t cfsr  = *(volatile uint32_t *)0xE000ED28;
    volatile uint32_t hfsr  = *(volatile uint32_t *)0xE000ED2C;
    volatile uint32_t mmfar = *(volatile uint32_t *)0xE000ED34;
    volatile uint32_t bfar  = *(volatile uint32_t *)0xE000ED38;

    /* Break here in the debugger: pc is the faulting address,
       cfsr decodes the violation type per the tables above. */
    __asm volatile ("bkpt #0");

    (void)r0; (void)r1; (void)r2; (void)r3; (void)r12; (void)lr; (void)psr;
    while (1) { }
}

The tst lr, #4 / ite eq sequence checks bit 2 of the exception LR value to determine whether the Main Stack Pointer (MSP) or Process Stack Pointer (PSP) was active when the exception was taken — required because Cortex-M can fault from either stack depending on whether the code was using an RTOS's per-task stack (PSP) or the exception/kernel stack (MSP) at the time.

In production firmware, replace the bkpt breakpoint with code that writes cfsr, hfsr, pc, and lr to a battery-backed or no-init RAM region (a region the linker script excludes from .bss zero-initialisation) and then resets. On the next boot, check that region for a valid fault record and log or transmit it — this turns "device rebooted in the field for no reason" into an actual, diagnosable fault report. See why does my STM32 keep resetting for the reset-cause side of this same diagnostic pattern.

A Worked Example

Given cfsr = 0x00008200 and pc = 0x08002144 captured from a fault:

  • 0x00008200 in binary is 0000 0000 0000 0000 1000 0010 0000 0000.
  • Bit 9 (PRECISERR, within BFSR) is set → a precise bus fault.
  • Bit 15 (BFARVALID) is set → BFAR holds a valid faulting address.
  • Reading BFAR gives, say, 0xA0000000 — an address well outside any valid RAM, flash, or peripheral region on the target part.
  • Looking up pc = 0x08002144 in the .map file resolves to a specific function — commonly the site of a dereferenced null or dangling pointer, or an array index that ran past its bounds and produced an out-of-range address.

That combination — a precise BusFault at a known PC, dereferencing a clearly invalid address — is one of the most common real-world HardFault causes, and is fully diagnosable from the register data alone, without needing to reproduce the fault under a live debugger session.

Design Considerations

  • Implement a persistent fault handler before you need it, not after. A HardFault that only occurs after days of field operation is nearly impossible to reproduce under a debugger. A RAM-persisted fault record read back on the next boot is the only practical way to diagnose faults that happen outside the lab.
  • Enable the individual fault handlers (MemManage, BusFault, UsageFault) during development. They're disabled by default on most vendor startup code, which routes everything through the less-specific HardFault path. Enabling them via SCB->SHCSR during development gives more precise fault classification, at the cost of needing three additional handlers instead of one.
  • A stack overflow can itself corrupt the exception frame, making the stacked PC/LR values unreliable. If CFSR shows MSTKERR or STKERR (a fault during exception stacking itself), suspect stack exhaustion before trusting the other stacked register values — see what does embedded startup code do for how stack placement is configured, and consider enabling a stack-overflow guard region via the MPU if the part has one.
  • Treat a HardFault handler that just calls NVIC_SystemReset() as a red flag in a codebase — it silently converts every hard fault into an ordinary-looking reset, hiding the fault data from the reset-cause diagnostics covered in why does my STM32 keep resetting. Capture the fault registers first, then reset.

Common Mistakes

*Not checking the VALID bits before trusting MMFAR/BFAR

MMFAR and BFAR retain their last value indefinitely — they are not cleared automatically on the next fault. Reading BFAR without first checking CFSR.BFARVALID (bit 15) risks acting on a stale address left over from a previous, unrelated fault.

Assuming the stacked PC always points exactly at the faulting instruction

This is true for precise faults but not for IMPRECISERR bus faults, where the CPU has already executed one or more instructions past the actual fault by the time the bus reports the error asynchronously. Treat an imprecise-fault PC as an approximate location, not an exact one.

Debugging a HardFault by single-stepping from Reset_Handler, hoping to catch it live

For a fault that only manifests after minutes or hours of operation, this is impractical. Instrument the fault handler to capture and persist the diagnostic registers (as shown above) and reproduce the analysis from the captured data instead of trying to catch the fault in a live debug session.

Ignoring HFSR.VECTTBL

If bit 1 of HFSR is set, the fault occurred while the core was trying to read the vector table itself — this usually means SCB->VTOR is misconfigured (common after a bootloader jump that doesn't correctly relocate the vector table) or the vector table has been corrupted, and no amount of CFSR analysis will find a normal application bug because the failure is happening before the intended handler can even run.

Firmware that ships to the field needs a fault-capture and reporting strategy designed in from the start, not bolted on after the first unexplained field return. Zeus Design's embedded firmware team builds production-grade fault handling, diagnostics, and remote fault reporting into commercial embedded products across STM32, nRF, and other Cortex-M platforms.

Frequently Asked Questions

Does this apply to ESP32 or RISC-V microcontrollers?
No — CFSR, HFSR, MMFAR, and BFAR are specific to the ARM Cortex-M fault-handling architecture (used by STM32, nRF52/53/91, RP2040's Cortex-M0+ core, and most other Cortex-M0/M3/M4/M7/M33 MCUs). Classic ESP32 uses Xtensa cores with a different exception model (`Guru Meditation Error` panics with their own register dump format), and RISC-V cores (ESP32-C3/C6, some RP2350 configurations) use the RISC-V trap-cause CSRs instead. The general debugging approach — read the exception's saved registers, find the faulting PC, cross-reference the .map file — carries over conceptually, but the specific register names and addresses in this guide are Cortex-M only.
Why does the debugger just show 'HardFault_Handler' in the call stack with no useful information?
Because the default HardFault_Handler generated by most vendor startup code is just an infinite loop (`while(1) {}`) with no code to extract or display the fault registers. The debugger correctly shows you're stuck in that infinite loop, but it can't reconstruct the call stack that led to the fault without help — the stack frame at the point of the fault needs to be manually unwound using the stacked PC and LR values described in this guide, or captured by a custom handler before the loop runs.
Can a HardFault happen without any bug in application code?
Yes, in specific circumstances. Calling a FreeRTOS API function from an interrupt whose NVIC priority is numerically higher than configMAX_SYSCALL_INTERRUPT_PRIORITY is a well-known configuration error that reliably produces a hard fault even though the calling code itself may be correct in isolation — see the STM32 NVIC interrupt priority configuration guide for the FreeRTOS-specific priority rules. Divide-by-zero (if UsageFault's DIVBYZERO trap is enabled), unaligned access on a core that traps it, and a stack overflow that corrupts the exception frame during the fault itself can also produce faults that don't trace back to an obviously 'wrong' line of code without register-level analysis.

References

Related Questions

Related Forum Discussions