What are Cortex-M Fault Handlers Explained-Embedded Systems Training in Hyderabad

ARM Cortex-M Programming

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.

HardFaultUsageFaultBusFaultMemManageCFSRFault Debugging

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

Cause category
Examples
Fault type
Illegal instruction
Executing data as code, undefined opcode, Thumb→ARM switch on Cortex-M
UsageFault / HardFault
Memory access violation
NULL pointer dereference, writing to read-only flash, stack overflow into protected region
MemManage / HardFault
Unaligned access
Reading a 32-bit word from an odd address when CCR.UNALIGN_TRP is set
UsageFault
Divide by zero
Integer division by 0 when CCR.DIV_0_TRP is set (Cortex-M3/M4 only)
UsageFault
Bus error
Accessing an invalid peripheral address, DMA misconfiguration, AMBA AHB/APB error response
BusFault / HardFault
Stack corruption
Stack overflow overwrites stacked PC, causing unstacking to jump to an invalid address
HardFault

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)

Exc #
IRQ # (CMSIS)
Exception type
Priority
Vector offset
Activation
1
—
Reset
−3 (highest fixed)
0x00000004
Asynchronous
2
−14
NMI
−2 (fixed)
0x00000008
Asynchronous
3
−13
HardFault
−1 (fixed)
0x0000000C
—
4
−12
MemManage
Configurable
0x00000010
Synchronous
5
−11
BusFault
Configurable
0x00000014
Sync (precise) / Async (imprecise)
6
−10
UsageFault
Configurable
0x00000018
Synchronous
7–10
—
Reserved
—
—
—
11
−5
SVCall
Configurable
0x0000002C
Synchronous (SVC instruction)
12–13
—
Reserved
—
—
—
14
−2
PendSV
Configurable
0x00000038
Asynchronous (ICSR.PENDSVSET)
15
−1
SysTick
Configurable
0x0000003C
Asynchronous (counter reaches 0)
16+
0+
IRQ0, IRQ1…
Configurable
0x00000040 + n×4
Asynchronous (peripheral)

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

Default state on power-up

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

Bits [31:16]
Bits [15:8]
Bits [7:0]
UFSR — UsageFault Status
BFSR — BusFault Status
MMFSR — MemManage Status

Key UFSR bits (CFSR[31:16])

DIVBYZERO [25]
Divide by zero (CCR.DIV_0_TRP must be set)
UNALIGNED [24]
Unaligned access (CCR.UNALIGN_TRP must be set)
NOCP [19]
No coprocessor (FPU not enabled on M4F, or M3)
INVPC [18]
Invalid PC load: EXC_RETURN value was invalid
INVSTATE [17]
Invalid EPSR state (e.g. branching to even address with LSB clear)
UNDEFINSTR [16]
Undefined instruction executed

Key BFSR bits (CFSR[15:8])

BFARVALID [15]
BFAR address register holds a valid fault address
LSPERR [13]
FPU lazy stacking error (Cortex-M4F)
STKERR [12]
Exception stacking error (stack overflow during push of exception frame)
UNSTKERR [11]
Exception unstacking error (bad SP during pop)
IMPRECISERR [10]
Imprecise bus error (async — BFAR not valid)
PRECISERR [9]
Precise bus error — BFAR holds the bad address
IBUSERR [8]
Instruction bus error during prefetch

Key MMFSR bits (CFSR[7:0])

MMARVALID [7]
MMFAR holds a valid fault address
MLSPERR [5]
FPU lazy stacking MemManage error
MSTKERR [4]
MemManage error during exception stacking
MUNSTKERR [3]
MemManage error during exception unstacking
DACCVIOL [1]
Data access violation (MPU region permission)
IACCVIOL [0]
Instruction access violation (XN region executed)

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

Symptom
Likely CFSR/HFSR bits
Most probable root cause
HardFault, PC in vector table region
HFSR.VECTTBL
Corrupt vector table or handler not defined (defaults to 0x00000000)
HardFault, PC looks like valid code
HFSR.FORCED + CFSR
A configurable fault escalated — check UFSR/BFSR/MMFSR bits
PC = 0xDEADC0DE or similar pattern
UFSR.INVPC
Stack overflow corrupted stacked PC; check canary pattern
CFSR.PRECISERR set, BFARVALID set
BFSR.PRECISERR
Read/write to non-existent peripheral address — check BFAR
CFSR.DACCVIOL set
MMFSR.DACCVIOL
MPU region permission violation — NULL ptr or out-of-bounds write
CFSR.UNDEFINSTR set
UFSR.UNDEFINSTR
Executing data as code (PC jumped into data/BSS section)
CFSR.DIVBYZERO set
UFSR.DIVBYZERO
Integer divide by zero — check divisor calculation logic

Frequently Asked Questions

Why are UsageFault, BusFault, and MemManage disabled by default?

The ARM architecture keeps them disabled so that resource-constrained systems that do not need fine-grained fault diagnosis can operate without dedicating vector table entries or writing handlers for each type. All three escalate cleanly to HardFault when disabled, giving the system a single catch-all handler. Enable them only when you want separate handlers and specific CFSR diagnostic data.

Can I return from a HardFault handler?

In theory yes — the processor executes a normal exception exit via EXC_RETURN. But in practice, if the fault was caused by corrupted stack or an invalid instruction, returning will immediately re-enter the fault. In most production systems, a HardFault handler logs the diagnostic information, saves it to non-volatile storage or transmits it, then either triggers a system reset (NVIC_SystemReset()) or halts in an infinite loop.

Why does the naked trampoline check EXC_RETURN bit[2]?

The hardware exception frame (containing the faulting PC) is pushed onto whichever stack was active before the fault: MSP in bare-metal Thread mode, or PSP if the fault occurred in an RTOS task. The naked trampoline reads LR (which holds EXC_RETURN), checks bit[2], and passes the correct stack pointer to the C diagnostic function so it reads the right PC and register values.

What is an imprecise BusFault and why is it harder to debug?

A precise BusFault occurs on a load/store that fails synchronously — the processor immediately faults, the stacked PC points to the offending instruction, and BFAR holds the bad address. An imprecise BusFault occurs from a buffered write that was accepted by the AHB write buffer but later rejected — by the time the fault fires, the PC has moved on by several instructions. BFAR is not valid. Debugging requires correlating the stacked PC with surrounding memory accesses.

How do CFSR bits get cleared?

CFSR bits are sticky Read/Write-1-to-Clear (RC/W1C) fields. Write a 1 to each bit position you want to clear (writing 0 has no effect). In a diagnostic handler you typically clear all bits after reading: SCB->CFSR = SCB->CFSR; (write back what you read, setting all set bits). Similarly for HFSR: SCB->HFSR = SCB->HFSR;

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

EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *