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:
- 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).
- 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.
- 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
| Register | Address | Purpose |
|---|---|---|
| HFSR (HardFault Status Register) | 0xE000ED2C | Says 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) | 0xE000ED28 | A 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) | 0xE000ED34 | Holds the faulting address for a MemManage fault, but only when CFSR's MMARVALID bit is set. |
| BFAR (BusFault Address Register) | 0xE000ED38 | Holds 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:
0x00008200in binary is0000 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 = 0x08002144in the.mapfile 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->SHCSRduring 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
MSTKERRorSTKERR(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
Why Does My STM32 Keep Resetting Unexpectedly?
STM32 unexpected resets are caused by watchdog timeout, brown-out, hard fault, or power decoupling issues. Use the RCC reset flags to identify the root cause.
How Do You Configure STM32 NVIC Interrupt Priorities?
Learn how to configure STM32 NVIC interrupt priorities using HAL, priority grouping, and the FreeRTOS configMAX_SYSCALL_INTERRUPT_PRIORITY constraint.
What Does Embedded Startup Code Do Before main()?
Covers what embedded startup code does between reset and main(): stack init, SystemInit, .data copy to RAM, .bss zero, and C++ constructors.
How Does the Memory Map Work in an Embedded Microcontroller?
The Cortex-M memory map assigns flash, RAM, and peripherals to fixed address regions. Covers STM32 layout, volatile keyword, and how linker scripts map to it.
What Is a Watchdog Timer and How Do You Use It?
A watchdog timer resets an MCU when firmware hangs. Covers IWDG vs WWDG on STM32, prescaler setup, kick strategy, and window mode for fault detection.
What Is a Linker Script and What Does It Do?
A linker script controls where firmware code and data land in flash and RAM. Covers MEMORY regions, SECTIONS, LMA/VMA, and the startup symbols it exports.