HomeAbout UsContact Us

Cortex-M Fault Handlers and Exception Handling in Embedded C

By Jithin Tom
Published in Embedded C/C++
July 02, 2026
4 min read
Cortex-M Fault Handlers and Exception Handling in Embedded C

Table Of Contents

01
The Cortex-M Exception Model
02
Exception Stack Frame: What Hardware Saves
03
Fault Status Registers: The Debug Rosetta Stone
04
Exception Priority and Preemption
05
A Production-Ready HardFault Handler
06
Common Fault Scenarios and Root Causes
07
Debugging Workflow: From Fault to Root Cause
08
Preventing Faults: Design Practices
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

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.


The Cortex-M Exception Model

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:

  • Tail-chaining: When one exception finishes and another is pending, the processor skips the full unstacking and restacking cycle, reducing the back-to-back handler transition to just 6 cycles (vs. 12 cycles for a standard entry from Thread mode).
  • Late-arriving: If a higher-priority exception occurs during the state-saving phase of a lower-priority exception, the processor switches to fetch the higher-priority handler while completing the state save, ensuring the critical fault/interrupt is handled without delay.
+------------------------------------------------------------------------------------+
| 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).


Exception Stack Frame: What Hardware Saves

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.


Fault Status Registers: The Debug Rosetta Stone

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.

RegisterBitNameMeaning
MMFSR0IACCERRInstruction access violation (MPU)
MMFSR1DACCERRData access violation (MPU)
MMFSR3MUNSTKERRUnstacking error on exception return
MMFSR4MSTKERRStacking error on exception entry
MMFSR5MLSPERRMemManage fault during FPU lazy state preservation
MMFSR7MMARVALIDMMFAR holds valid fault address
BFSR0IBUSERRInstruction bus error
BFSR1PRECISERRPrecise data bus error
BFSR2IMPRECISERRImprecise data bus error
BFSR3UNSTKERRBus error on unstacking
BFSR4STKERRBus error on stacking
BFSR5LSPERRBus fault during FPU lazy state preservation
BFSR7BFARVALIDBFAR holds valid fault address
UFSR0UNDEFINSTRUndefined instruction
UFSR1INVSTATEInvalid EPSR state (e.g., Thumb bit clear)
UFSR2INVPCInvalid PC load (EXC_RETURN corruption)
UFSR3NOCPNo coprocessor (FPU access when disabled)
UFSR8UNALIGNEDUnaligned access (when UNALIGN_TRP=1)
UFSR9DIVBYZEROInteger division by zero (when DIV_0_TRP=1)
HFSR1VECTTBLVector table read fault
HFSR30FORCEDFault escalated to HardFault
HFSR31DEBUGEVTDebug 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.


Exception Priority and Preemption

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 them
NVIC_SetPriority(MemoryManagement_IRQn, 0);
NVIC_SetPriority(BusFault_IRQn, 0);
NVIC_SetPriority(UsageFault_IRQn, 0);
// Enable the fault handlers in SCB->SHCSR
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk |
SCB_SHCSR_BUSFAULTENA_Msk |
SCB_SHCSR_USGFAULTENA_Msk;

A Production-Ready HardFault Handler

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 writes
typedef struct {
uint32_t cfsr; // Configurable Fault Status Register
uint32_t hfsr; // HardFault Status Register
uint32_t dfsr; // Debug Fault Status Register
uint32_t afsr; // Auxiliary Fault Status Register
uint32_t mmfar; // MemManage Fault Address Register
uint32_t bfar; // BusFault Address Register
uint32_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, xPSR
fault_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
  • Capture registers before any function calls that might clobber them
  • __BKPT(0) guarded by CoreDebug->DHCSR check — only halts when a debugger is attached, preventing lockup in production

Common Fault Scenarios and Root Causes

SymptomLikely CFSR BitTypical Cause
HardFault on startupVECTTBL (HFSR)Vector table offset (VTOR) not set, or table in invalid memory
HardFault after enabling FPUNOCP (UFSR)CP10/CP11 not enabled in CPACR before FPU use
HardFault in ISRFORCED (HFSR) + any CFSR bitLower-priority fault escalated (handler disabled or priority too low)
Fault on printf/mallocIMPRECISERR (BFSR)Heap/stack collision, or accessing freed memory
Fault on unaligned struct accessUNALIGNED (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 divisionDIVBYZERO (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 callINVSTATE (UFSR)Corrupted function pointer (Thumb bit clear), or branch to non-executable region
Fault after context switchMSTKERR/MUNSTKERR (MMFSR)Stack overflow, PSP/MSP misconfigured

Debugging Workflow: From Fault to Root Cause

  1. Check HFSR first — if FORCED is set, look at CFSR for the original fault
  2. Read CFSR — decode MMFSR/BFSR/UFSR to classify the fault
  3. If MMARVALID/BFARVALID set — read MMFAR/BFAR for the exact faulting address
  4. Examine stacked PC — for synchronous faults (precise bus errors, MPU violations, usage faults), this points to the faulting instruction; for asynchronous faults (imprecise bus errors), it may point to a later instruction
  5. Check EXC_RETURN (LR on handler entry) — tells you the mode (Thread/Handler), stack (MSP/PSP), and FPU state of the interrupted context
  6. Map PC to source — use addr2line -e firmware.elf <PC> or your IDE’s call stack
  7. Inspect the faulting instruction — disassembly at PC-2/PC-4 reveals the operation
# Example: decode fault address from .elf
arm-none-eabi-addr2line -e build/firmware.elf 0x0800423C
# Output: src/tasks/comm_task.c:142

Preventing Faults: Design Practices

PracticePrevents
Enable all fault handlers at priority 0Silent escalation to HardFault
Use MPU for stack guards & null pointer detectionMemManage on stack overflow / NULL deref
Initialize VTOR early in Reset_HandlerVECTTBL HardFault
Enable FPU in SystemInit before any FP mathNOCP UsageFault
Validate all pointers before dereferenceBusFault, MemManage
Use static_assert for struct alignmentUNALIGNED UsageFault
Guard division with if (divisor != 0) and enable DIV_0_TRPDIVBYZERO 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

Summary

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:

  1. Enable the configurable fault handlers (MemManage, BusFault, UsageFault) to ensure every exception is handled at the appropriate priority level.
  2. Capture the stack frame and status registers securely before executing any C code, preserving the exact state of the processor.
  3. Read CFSR/HFSR/MMFAR/BFAR systematically to accurately diagnose the system state.
  4. Preserve the fault record in non-volatile or .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.


  • Memory Protection Unit in Embedded Systems — Configuring the MPU for stack guards and memory regions
  • Interrupt Handling and ISRs in Embedded Systems — NVIC priority, preemption, and ISR best practices

References

  1. ARM, Cortex-M4 Devices Generic User Guide, ARM DUI 0553A; Cortex-M3 Devices Generic User Guide, ARM DUI 0552A; Cortex-M7 Devices Generic User Guide, ARM DUI 0646B.
  2. ARM, ARMv7-M Architecture Reference Manual, ARM DDI 0403E — Complete register definitions for CFSR, HFSR, MMFAR, BFAR, SHCSR.
  3. Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, 3rd ed., Newnes (2013).
  4. STMicroelectronics, Programming Manual PM0214: STM32F3/F4/G4 Series Cortex-M4 programming manual; Programming Manual PM0253: STM32F7 Series Cortex-M7 programming manual.
  5. Memfault (Chris Coleman), Cortex-M Hard Fault Debugging, Interrupt Blog (2019).
  6. FreeRTOS, FreeRTOS Cortex-M Port Guide — PSP/MSP usage in Thread/Handler mode, context switching stack frames.

Frequently Asked Questions

What is the difference between HardFault and other fault types on Cortex-M?

HardFault is the catch-all exception with fixed priority -1 that handles all faults when their specific handlers (MemManage, BusFault, UsageFault) are disabled or when a fault occurs inside another fault handler. MemManage, BusFault, and UsageFault are configurable-priority exceptions that handle specific fault classes when enabled.

How do I decode the EXC_RETURN value in LR to know which stack was used?

Check LR bit 2 (SPSEL): if 1, the exception return uses the Process Stack Pointer (PSP); if 0, it uses the Main Stack Pointer (MSP). Common values: 0xFFFFFFF1 (return to Handler mode, MSP), 0xFFFFFFF9 (return to Thread mode, MSP), 0xFFFFFFFD (return to Thread mode, PSP).

Which registers should I read first when debugging a HardFault?

Check HFSR (0xE000ED2C) first — if the FORCED bit is set, the fault escalated from a configurable fault. Then read CFSR (0xE000ED28) to identify the specific fault type (MMFSR/BFSR/UFSR). If MMARVALID or BFARVALID bits are set, read MMFAR (0xE000ED34) or BFAR (0xE000ED38) for the faulting address.

Can I use printf or semihosting inside a fault handler?

Avoid printf/semihosting in fault handlers — they may require a working stack, heap, or debugger connection that the fault itself has corrupted. Use minimal register dumps via ITM/SWO, blink an LED pattern, or write to a reserved RAM buffer for post-mortem analysis.

Why does my HardFault handler never get called — the system just resets?

If the HardFault handler itself faults (e.g., stack overflow in the handler, or accessing invalid memory while debugging the fault), the processor escalates to a lockup state and typically triggers a system reset. Ensure your fault handler uses minimal stack, no function calls, and only safe register access.

Tags

cortex-mfault-handlerexception-handlinghardfaultmemmanagebusfaultusagefaultarm

Share


Previous Article
Linker Scripts and Memory Layout in Embedded C: A Practical Guide
Jithin Tom

Jithin Tom

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

Related Posts

Stack Usage Analysis and Optimization in Embedded C
Stack Usage Analysis and Optimization in Embedded C
July 12, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media