Cortex-M Fault Handlers Explained
A fault is a system exception generated by the processor to signal an error condition. Cortex-M3/M4 defines four fault types: HardFault, UsageFault, BusFault, and MemManage. Understanding what triggers each fault, which status registers to read, and how to write a diagnostic handler is essential for debugging real embedded firmware.
What is a Fault?
A fault is a system exception generated by the processor itself — not by an external peripheral — to indicate that something in the program’s execution has violated a processor rule. Faults are distinct from peripheral interrupts: they are internal signals triggered by the CPU’s own detection logic.
What the processor does on a fault
- Updates internal fault status registers with the type and address of the fault
- Sets the pending bit for the corresponding fault exception
- Stacks registers and enters the fault handler (same entry mechanism as any IRQ)
- Calls the registered fault handler from the vector table
What you can do in a fault handler
- Read status registers to identify the exact fault cause
- Read fault address registers to find the offending instruction or memory address
- Log the diagnostic information over UART or SWO
- Attempt recovery (rare), or reset/halt the system
- In an RTOS: kill the offending task and continue
Why Faults Happen
The vast majority of faults in embedded firmware are caused by programmer errors — code that violates the rules the processor is designed to enforce:
Common fault triggers
Most faults escalate to HardFault if the specific fault exception (UsageFault, BusFault, MemManage) is not enabled in SHCSR.
System Exception Table
The ARM Architecture Reference Manual defines the system exceptions and their fixed vector table positions. Each exception has an exception number (used by IPSR and the vector table) and a separate IRQ number used by CMSIS:
ARM Cortex-M System Exceptions (complete)
CMSIS uses negative IRQ numbers for system exceptions so that code like NVIC_SetPriority(MemManage_IRQn, 5) works uniformly. The IPSR register always returns the Exception number (not the IRQ number).
Four Fault Exception Types
HardFault is always enabled and has a fixed priority of −1. It cannot be pre-empted by any configurable exception. It can only be disabled by setting the FAULTMASK register (which also disables all interrupts at once — only useful in very specific atomic operations).
UsageFault, BusFault, and MemManage are all disabled by default. When disabled, any fault condition they would have handled instead escalates to HardFault.
HardFault (Exception 3)
- Always enabled, priority = −1 (fixed)
- Catch-all: triggered when any other fault escalates (configurable fault disabled, or fault within a fault handler)
- Also triggered by vector table read errors during exception entry
- HFSR register records the cause
- Key HFSR bits: FORCED [30] = escalated from another fault; VECTTBL [1] = vector table read error
UsageFault (Exception 6)
- Disabled by default — enable via SHCSR.USGFAULTENA
- Triggered by: undefined instruction, invalid EPSR state, PC not halfword-aligned on exception return, divide-by-zero (if CCR.DIV_0_TRP=1), unaligned access (if CCR.UNALIGN_TRP=1), coprocessor access on M3
- CFSR bits [31:16] (UFSR) record the specific cause
- Synchronous — happens at the instruction that caused it
BusFault (Exception 5)
- Disabled by default — enable via SHCSR.BUSFAULTENA
- Triggered by: invalid memory address access, AHB/APB bus error response from a peripheral
- Can be precise (synchronous — BFAR holds the bad address) or imprecise (asynchronous — buffered write went wrong after the instruction completed)
- CFSR bits [15:8] (BFSR) record the cause; BFAR holds the address for precise faults
MemManage (Exception 4)
- Disabled by default — enable via SHCSR.MEMFAULTENA
- Requires MPU to be configured — without MPU, this fault never fires
- Triggered by: access to a region not covered by any MPU region, access violating MPU region permissions (e.g. writing to a read-only region), executing code from a no-execute region (XN bit)
- CFSR bits [7:0] (MMFSR) record the cause; MMFAR holds the offending address
- Synchronous (precise)
Fault Status Registers
The SCB contains a set of fault status registers that the fault handler reads to diagnose the cause. These registers are sticky — bits are set by hardware and must be cleared by software (write 1 to clear).
Configurable Fault Status Register (CFSR) — 0xE000ED28
Key UFSR bits (CFSR[31:16])
Key BFSR bits (CFSR[15:8])
Key MMFSR bits (CFSR[7:0])
CFSR register at 0xE000ED28. All bits are RC/W1C (read to check, write 1 to clear). HFSR is at 0xE000ED2C. MMFAR at 0xE000ED34. BFAR at 0xE000ED38.
Enabling Configurable Faults
UsageFault, BusFault, and MemManage are all disabled by default. Enable them early in main() so that specific diagnostic handlers fire instead of the generic HardFault:
#include "stm32f4xx.h" /* or cmsis_device.h */
void fault_init(void)
{
/* Enable all three configurable fault exceptions */
SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk /* bit 18 */
| SCB_SHCSR_BUSFAULTENA_Msk /* bit 17 */
| SCB_SHCSR_MEMFAULTENA_Msk; /* bit 16 */
/* Enable divide-by-zero trapping (UsageFault) */
SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk; /* bit 4 */
/* Enable unaligned access trapping (UsageFault) — optional,
* can be noisy with naive pointer arithmetic */
/* SCB->CCR |= SCB_CCR_UNALIGN_TRP_Msk; */
/* Set priorities — configurable faults must have lower priority
* value (higher urgency) than HardFault's effective −1.
* They cannot be set higher than HardFault (-1), so set them
* anywhere in the configurable range (e.g. 1). */
NVIC_SetPriority(MemoryManagement_IRQn, 1);
NVIC_SetPriority(BusFault_IRQn, 1);
NVIC_SetPriority(UsageFault_IRQn, 1);
}
Writing a Diagnostic Fault Handler
A useful HardFault handler reads the stacked PC (the address of the instruction that caused the fault) and the CFSR/HFSR registers, then logs everything before halting.
#include <stdio.h>
#include "stm32f4xx.h"
/* The stacked frame pushed by hardware on exception entry */
typedef struct {
uint32_t r0, r1, r2, r3;
uint32_t r12;
uint32_t lr; /* caller's LR before exception */
uint32_t pc; /* instruction that caused the fault */
uint32_t xpsr;
} ExceptionFrame_t;
/* Called by the naked trampoline below; frame points to the hardware frame */
void hard_fault_handler_c(ExceptionFrame_t *frame)
{
uint32_t cfsr = SCB->CFSR; /* Configurable Fault Status */
uint32_t hfsr = SCB->HFSR; /* HardFault Status */
uint32_t mmfar = SCB->MMFAR; /* MemManage Fault Address */
uint32_t bfar = SCB->BFAR; /* BusFault Address */
printf("=== HARD FAULT ===\r\n");
printf("PC = 0x%08lX (faulting instruction)\r\n", frame->pc);
printf("LR = 0x%08lX\r\n", frame->lr);
printf("R0 = 0x%08lX R1 = 0x%08lX\r\n", frame->r0, frame->r1);
printf("R2 = 0x%08lX R3 = 0x%08lX\r\n", frame->r2, frame->r3);
printf("xPSR= 0x%08lX\r\n", frame->xpsr);
printf("CFSR= 0x%08lX\r\n", cfsr);
printf("HFSR= 0x%08lX\r\n", hfsr);
if (cfsr & (1UL << 7)) printf(" MMFAR = 0x%08lX (MemManage address)\r\n", mmfar);
if (cfsr & (1UL << 15)) printf(" BFAR = 0x%08lX (BusFault address)\r\n", bfar);
if (hfsr & (1UL << 30)) printf(" FORCED: escalated from configurable fault\r\n");
if (hfsr & (1UL << 1)) printf(" VECTTBL: vector table read error\r\n");
/* Decode UFSR */
uint32_t ufsr = (cfsr >> 16) & 0xFFFFU;
if (ufsr & (1U << 9)) printf(" DIVBYZERO\r\n");
if (ufsr & (1U << 8)) printf(" UNALIGNED\r\n");
if (ufsr & (1U << 3)) printf(" NOCP (FPU not enabled)\r\n");
if (ufsr & (1U << 2)) printf(" INVPC (bad EXC_RETURN)\r\n");
if (ufsr & (1U << 1)) printf(" INVSTATE (bad EPSR)\r\n");
if (ufsr & (1U << 0)) printf(" UNDEFINSTR\r\n");
/* Halt — never return from HardFault in production */
__BKPT(0);
while (1) {}
}
/* Naked trampoline: extracts the correct stack frame pointer before calling C */
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile (
"TST LR, #4 \n\t" /* check EXC_RETURN bit[2] */
"ITE EQ \n\t"
"MRSEQ R0, MSP \n\t" /* frame on MSP */
"MRSNE R0, PSP \n\t" /* frame on PSP (RTOS task) */
"B hard_fault_handler_c\n\t"
);
}UsageFault Handler Example
void UsageFault_Handler(void)
{
uint32_t ufsr = (SCB->CFSR >> 16) & 0xFFFFU;
if (ufsr & (1U << 9)) {
/* Divide by zero */
printf("UsageFault: divide by zero\r\n");
/* Option 1: clear the flag and return (risky — operand still 0) */
SCB->CFSR = (1UL << 25); /* write 1 to clear DIVBYZERO */
/* Option 2: halt */
}
if (ufsr & (1U << 0)) {
printf("UsageFault: undefined instruction at PC=%lX\r\n",
/* get stacked PC same way as HardFault handler */ 0UL);
}
while (1) {}
}
Common Fault Causes in Practice
Fault cause → diagnosis checklist
Frequently Asked Questions
Suggested Diagrams for This Lecture
- Fault escalation diagram: UsageFault/BusFault/MemManage (disabled) → HardFault arrow
- CFSR register: colour-coded UFSR (blue), BFSR (yellow), MMFSR (red) regions with key bit labels
- Fault handler flow: peripheral asserts → NVIC → stacking → handler reads CFSR/HFSR → log → reset
- Naked trampoline flowchart: EXC_RETURN[2]=0 → MSP frame → C handler, EXC_RETURN[2]=1 → PSP frame

2 Comments