
Mastering Cortex-M fault handling is a highly rewarding skill that empowers embedded engineers to ship exceptionally reliable firmware. When an unexpected fault occurs, having a well-designed fault handler ensures that diagnosing the issue is a straightforward and efficient process, giving you complete visibility into the system’s precise state.
This article walks through the Cortex-M exception model, the anatomy of a fault stack frame, the key status registers that tell you what went wrong, and a practical fault handler template you can drop into your project today.
Cortex-M processors use a unified exception model where everything — interrupts, faults, system calls, the SysTick timer — enters through the same hardware mechanism. The processor operates in two modes: Thread mode (used for normal application code) and Handler mode (used strictly for exception processing).
The Nested Vectored Interrupt Controller (NVIC) manages prioritization, preemption, and automatic state preservation. For safety-critical hard-real-time systems, the NVIC guarantees deterministic interrupt latency through hardware optimizations:
+------------------------------------------------------------------------------------+| Cortex-M Exception Flow |+====================================================================================+| || [ NORMAL EXECUTION ] || Thread Mode (Background execution) || Uses Main Stack (MSP) or Process Stack (PSP) || || | || v || | || || [ FAULT / EXCEPTION OCCURS ] || HardFault | MemManage | BusFault | UsageFault || SVCall | PendSV | SysTick | External IRQ || || | || v || | || || [ VECTOR TABLE LOOKUP ] || VTOR -> Exception Number -> Handler Address || || | || v || | || || [ FAULT HANDLER EXECUTION ] || HardFault_Handler() / MemManage_Handler() || BusFault_Handler() / UsageFault_Handler() || (Handler Mode, MSP) || || 1. Stack Frame Auto-Saved (xPSR, PC, LR, R0-R3, R12 + optional FP regs) || 2. LR = EXC_RETURN (e.g. 0xFFFFFFF1/9/D or 0xFFFFFFE1/9/D) indicates || return stack (MSP/PSP), mode (Thread/Handler), and FPU usage || 3. Read CFSR/HFSR/DFSR/AFSR for fault cause || 4. Read MMFAR/BFAR for faulting address || || | || v || | || || [ RECOVERY / ACTION ] || Fix cause -> Return (BX LR) || Log fault -> System reset (NVIC_SystemReset()) || Enter safe mode / blink LED / watchdog trigger || |+------------------------------------------------------------------------------------+
The processor maintains two stack pointers: MSP (Main Stack Pointer) used in Handler mode and at reset, and PSP (Process Stack Pointer) typically used in Thread mode when an RTOS is present. On exception entry, hardware automatically pushes a stack frame onto the active stack (8 words for basic context, or 26 words when the FPU is active).
When any exception occurs, the processor pushes a frame onto the active stack without software intervention. For processors without an FPU (or if the FPU was not actively used), this is a standard 8-word frame. If the FPU was actively used in the interrupted context, hardware automatically pushes an extended 26-word frame (including S0-S15 and FPSCR) to ensure deterministic floating-point state preservation.
+--------------------------------------------------------------------+| Exception Stack Frame (Auto-Saved by Hardware) |+====================================================================+| || HIGH ADDR +--------+ xPSR <- Program Status Register || | | || +--------+ PC <- Program Counter (return address) || | | || +--------+ LR <- Link Register (pre-exception value)|| | | || +--------+ R12 <- R12 (scratch register) || | | || +--------+ R3 <- R3 || | | || +--------+ R2 <- R2 || | | || +--------+ R1 <- R1 || | | || LOW ADDR +--------+ R0 <- R0 (first argument) || || EXC_RETURN (LR) decoding: || 0xFFFFFFF1 = Return to Handler mode, MSP, FPU inactive || 0xFFFFFFF9 = Return to Thread mode, MSP, FPU inactive || 0xFFFFFFFD = Return to Thread mode, PSP, FPU inactive || 0xFFFFFFE1 = Return to Handler mode, MSP, FPU active || 0xFFFFFFE9 = Return to Thread mode, MSP, FPU active || 0xFFFFFFED = Return to Thread mode, PSP, FPU active || || Bit 2 (SPSEL) = 1 -> PSP used, 0 -> MSP used || Bit 4 (FTYPE) = 0 -> FPU frame (26 words), 1 -> Std frame (8 wds) |+--------------------------------------------------------------------+
The LR value on entry (EXC_RETURN) tells you exactly where the exception came from and which stack to use on return. Bit 2 (SPSEL) identifies the stack: 1 = PSP (Thread mode), 0 = MSP (Handler/Thread mode). Bit 4 (FTYPE) is critical for safety-critical systems using floating-point math: 0 indicates a 26-word FPU frame was stacked. The basic registers (R0-R3, R12, LR, PC, xPSR) remain at the lowest stack address (where SP points) regardless of frame type — the FPU registers (S0-S15, FPSCR) are stacked at higher addresses above them. This means fault handler code that reads the basic frame does not need any offset adjustment for FPU frames.
The Configurable Fault Status Register (CFSR) at 0xE000ED28 is a 32-bit register composed of three sub-registers: a 16-bit UFSR (bits 31:16), an 8-bit BFSR (bits 15:8), and an 8-bit MMFSR (bits 7:0). Reading it as a 32-bit word gives you the complete picture.
+--------------------------------------------------------------------------------------------------------------------+| Key Fault Status Registers (Cortex-M3/M4/M7) |+====================================================================================================================+| || CFSR (0xE000ED28) Configurable Fault Status Register UFSR[31:16] | BFSR[15:8] | MMFSR[7:0] || MMFSR (0xE000ED28) MemManage Fault Status IACCERR, DACCERR, MUNSTKERR, MSTKERR || BFSR (0xE000ED29) BusFault Status IBUSERR, PRECISERR, IMPRECISERR, UNSTKERR || UFSR (0xE000ED2A) UsageFault Status UNDEFINSTR, INVSTATE, INVPC, UNALIGNED || HFSR (0xE000ED2C) HardFault Status Register VECTTBL, FORCED, DEBUGEVT || DFSR (0xE000ED30) Debug Fault Status Register HALTED, BKPT, DWTTRAP, VCATCH || MMFAR (0xE000ED34) MemManage Fault Address Reg Faulting data address || BFAR (0xE000ED38) BusFault Address Register Faulting bus address || AFSR (0xE000ED3C) Auxiliary Fault Status Register Vendor-specific (e.g., L2 cache fault) || |+--------------------------------------------------------------------------------------------------------------------+
Key bits to check first:
Note: The bit positions below are relative to each sub-register (MMFSR, BFSR, UFSR) when accessed individually. When reading the full 32-bit CFSR at 0xE000ED28, BFSR bits are offset by +8 and UFSR bits by +16. For example, UNDEFINSTR is bit 0 of UFSR but bit 16 of CFSR.
| Register | Bit | Name | Meaning |
|---|---|---|---|
| MMFSR | 0 | IACCERR | Instruction access violation (MPU) |
| MMFSR | 1 | DACCERR | Data access violation (MPU) |
| MMFSR | 3 | MUNSTKERR | Unstacking error on exception return |
| MMFSR | 4 | MSTKERR | Stacking error on exception entry |
| MMFSR | 5 | MLSPERR | MemManage fault during FPU lazy state preservation |
| MMFSR | 7 | MMARVALID | MMFAR holds valid fault address |
| BFSR | 0 | IBUSERR | Instruction bus error |
| BFSR | 1 | PRECISERR | Precise data bus error |
| BFSR | 2 | IMPRECISERR | Imprecise data bus error |
| BFSR | 3 | UNSTKERR | Bus error on unstacking |
| BFSR | 4 | STKERR | Bus error on stacking |
| BFSR | 5 | LSPERR | Bus fault during FPU lazy state preservation |
| BFSR | 7 | BFARVALID | BFAR holds valid fault address |
| UFSR | 0 | UNDEFINSTR | Undefined instruction |
| UFSR | 1 | INVSTATE | Invalid EPSR state (e.g., Thumb bit clear) |
| UFSR | 2 | INVPC | Invalid PC load (EXC_RETURN corruption) |
| UFSR | 3 | NOCP | No coprocessor (FPU access when disabled) |
| UFSR | 8 | UNALIGNED | Unaligned access (when UNALIGN_TRP=1) |
| UFSR | 9 | DIVBYZERO | Integer division by zero (when DIV_0_TRP=1) |
| HFSR | 1 | VECTTBL | Vector table read fault |
| HFSR | 30 | FORCED | Fault escalated to HardFault |
| HFSR | 31 | DEBUGEVT | Debug event caused HardFault |
The FORCED bit (HFSR bit 30) serves as the critical escalation flag: it indicates that a MemManage, BusFault, or UsageFault was triggered, but because its handler was disabled or blocked, the processor was forced to escalate it to a HardFault.
Cortex-M uses lower numerical value = higher priority. Fixed system exceptions have negative priorities that cannot be changed:
+--------------------------------------------------------------------+| Exception Priority & Preemption (Lower Value = Higher Priority) |+====================================================================+| || Exception Priority Type Description ||--------------------------------------------------------------------|| Reset -3 (Fixed) Highest System reset || NMI -2 (Fixed) Highest Non-maskable interrupt || HardFault -1 (Fixed) Highest All escalated faults || MemManage 0-255 Config MPU violation || BusFault 0-255 Config Bus error || UsageFault 0-255 Config Undefined instr, div/0 || SVCall 0-255 Config Supervisor call (SVC) || PendSV 0-255 Config Pendable service call || SysTick 0-255 Config System tick timer || External IRQs 0-255 Config Peripheral interrupts || || Preemption: Higher priority (lower num) interrupts lower priority || Priority grouping: PRIGROUP splits priority into group/subpriority |+--------------------------------------------------------------------+
Safety-Critical Tip: Set fault handlers (MemManage, BusFault, UsageFault) to priority 0 (highest configurable) so they preempt everything except NMI/HardFault. In safety-critical systems, ensure your PRIGROUP setting maps this exclusively to a preemption priority, not a sub-priority. Furthermore, if your application uses the BASEPRI register to mask interrupts (common in RTOS critical sections), setting a fault handler to priority 0 guarantees it will never be masked by BASEPRI (which cannot mask priority 0). This ensures faults are caught immediately rather than escalating.
// Enable fault handlers at highest configurable priority (0)// This guarantees BASEPRI masking will never block themNVIC_SetPriority(MemoryManagement_IRQn, 0);NVIC_SetPriority(BusFault_IRQn, 0);NVIC_SetPriority(UsageFault_IRQn, 0);// Enable the fault handlers in SCB->SHCSRSCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk |SCB_SHCSR_BUSFAULTENA_Msk |SCB_SHCSR_USGFAULTENA_Msk;
Here’s a minimal, dependency-free handler that captures everything you need for post-mortem analysis:
#include <stdint.h>#include "stm32f4xx.h" // or your CMSIS header// Persistent fault record in a noinit section (survives reset)// Marked volatile to ensure the compiler doesn't optimize away the writestypedef struct {uint32_t cfsr; // Configurable Fault Status Registeruint32_t hfsr; // HardFault Status Registeruint32_t dfsr; // Debug Fault Status Registeruint32_t afsr; // Auxiliary Fault Status Registeruint32_t mmfar; // MemManage Fault Address Registeruint32_t bfar; // BusFault Address Registeruint32_t stacked_r0;uint32_t stacked_r1;uint32_t stacked_r2;uint32_t stacked_r3;uint32_t stacked_r12;uint32_t stacked_lr;uint32_t stacked_pc;uint32_t stacked_xpsr;uint32_t exc_return; // LR value on entry (EXC_RETURN)} fault_record_t;volatile __attribute__((section(".noinit.fault_record"))) fault_record_t fault_record;__attribute__((naked)) void HardFault_Handler(void) {__asm volatile ("TST LR, #4 \n" // Test bit 2 (SPSEL) of EXC_RETURN"ITE EQ \n" // If bit 2 == 0 -> MSP, else PSP"MRSEQ R0, MSP \n" // R0 = MSP"MRSNE R0, PSP \n" // R0 = PSP"MOV R1, LR \n" // Pass EXC_RETURN as the second argument (R1)"B fault_handler_c \n" // Branch to C handler (R0 = stack frame ptr, R1 = EXC_RETURN));}void fault_handler_c(uint32_t *stack_frame, uint32_t exc_return) {// Capture fault status registers first (before they potentially change)fault_record.cfsr = SCB->CFSR;fault_record.hfsr = SCB->HFSR;fault_record.dfsr = SCB->DFSR;fault_record.afsr = SCB->AFSR;fault_record.mmfar = SCB->MMFAR;fault_record.bfar = SCB->BFAR;// Stack frame layout: R0, R1, R2, R3, R12, LR, PC, xPSRfault_record.stacked_r0 = stack_frame[0];fault_record.stacked_r1 = stack_frame[1];fault_record.stacked_r2 = stack_frame[2];fault_record.stacked_r3 = stack_frame[3];fault_record.stacked_r12 = stack_frame[4];fault_record.stacked_lr = stack_frame[5];fault_record.stacked_pc = stack_frame[6];fault_record.stacked_xpsr = stack_frame[7];fault_record.exc_return = exc_return;// Safety-critical: Only execute BKPT if a debugger is actively attached.// Executing BKPT without a debugger will cause a lockup/escalation.if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {__BKPT(0);}// In production: trigger watchdog, log to non-volatile memory, or reset// NVIC_SystemReset();}
Key design choices:
__attribute__((naked)) — no prologue/epilogue, we control the stack.noinit section — survives soft reset for post-mortem debugging__BKPT(0) guarded by CoreDebug->DHCSR check — only halts when a debugger is attached, preventing lockup in production| Symptom | Likely CFSR Bit | Typical Cause |
|---|---|---|
| HardFault on startup | VECTTBL (HFSR) | Vector table offset (VTOR) not set, or table in invalid memory |
| HardFault after enabling FPU | NOCP (UFSR) | CP10/CP11 not enabled in CPACR before FPU use |
| HardFault in ISR | FORCED (HFSR) + any CFSR bit | Lower-priority fault escalated (handler disabled or priority too low) |
Fault on printf/malloc | IMPRECISERR (BFSR) | Heap/stack collision, or accessing freed memory |
| Fault on unaligned struct access | UNALIGNED (UFSR) | Packed struct dereference or unaligned LDM/STM/LDRD/STRD instructions (Cortex-M3/M4/M7 support unaligned accesses unless UNALIGN_TRP in SCB->CCR is set to 1) |
| Fault on division | DIVBYZERO (UFSR) | Integer divide by zero (requires DIV_0_TRP enabled in SCB->CCR; disabled by default, where div-by-zero silently returns 0) |
| Fault on function pointer call | INVSTATE (UFSR) | Corrupted function pointer (Thumb bit clear), or branch to non-executable region |
| Fault after context switch | MSTKERR/MUNSTKERR (MMFSR) | Stack overflow, PSP/MSP misconfigured |
FORCED is set, look at CFSR for the original faultaddr2line -e firmware.elf <PC> or your IDE’s call stack# Example: decode fault address from .elfarm-none-eabi-addr2line -e build/firmware.elf 0x0800423C# Output: src/tasks/comm_task.c:142
| Practice | Prevents |
|---|---|
| Enable all fault handlers at priority 0 | Silent escalation to HardFault |
| Use MPU for stack guards & null pointer detection | MemManage on stack overflow / NULL deref |
Initialize VTOR early in Reset_Handler | VECTTBL HardFault |
Enable FPU in SystemInit before any FP math | NOCP UsageFault |
| Validate all pointers before dereference | BusFault, MemManage |
Use static_assert for struct alignment | UNALIGNED UsageFault |
Guard division with if (divisor != 0) and enable DIV_0_TRP | DIVBYZERO UsageFault |
| Reserve RAM for fault record (.noinit) | Post-mortem analysis after reset |
| Implement an Independent Watchdog (IWDG) | System hang if a fault escalates to a Lockup state |
Cortex-M fault handling is a highly deterministic and powerful hardware mechanism that provides precise insights into system execution, empowering you to build highly reliable firmware. By following these best practices, you can maximize your system’s observability and resilience:
.noinit RAM to enable seamless post-reset analysis and continuous improvement.The bare-metal assembly wrapper and C handler pattern presented here is highly efficient, adding only ~20 bytes of flash while providing a robust foundation for building resilient, safety-critical embedded systems.
Quick Links
Legal Stuff




