Reading an ARM Cortex-M Hard Fault

A hard fault feels like the firmware equivalent of a locked door: the system stops, the debugger drops you into an infinite loop in HardFault_Handler, and there is no obvious message. But the Cortex-M4 records exactly what went wrong. Reading a hard fault is a learnable skill, and it separates engineers who guess from engineers who diagnose.

What a hard fault is

The Cortex-M4 has several fault exceptions: MemManage, BusFault, UsageFault, and the catch-all HardFault. In a default configuration the specific faults are not enabled, so everything escalates into HardFault. The first move in real bring-up is often to enable the configurable faults so you get a precise category instead of the generic one.

The two sources of truth

When a fault fires, the answer is in two places.

  • The fault status registers. SCB->CFSR (Configurable Fault Status Register) has bit fields telling you the category: a bus fault on a data access, an undefined instruction, an unaligned access, a divide by zero. When the relevant "address valid" bit is set, SCB->BFAR or SCB->MMFAR holds the faulting address. Reading CFSR is the single highest-value habit in fault debugging.
  • The stacked exception frame. On exception entry the hardware pushes eight registers onto the stack that was active: R0 through R3, R12, LR, PC, and xPSR. The stacked PC is the address of the instruction that faulted. Recover it and you know exactly where you were.

Recovering the faulting instruction

The trick is that the frame is on either the main stack or the process stack, and you have to pick the right one. The EXC_RETURN value in LR on handler entry tells you which. A minimal handler grabs the correct stack pointer and reads the stacked PC from it:

void HardFault_Handler(void) {
    __asm volatile(
        "tst lr, #4        \n"  // EXC_RETURN bit 2: which stack?
        "ite eq            \n"
        "mrseq r0, msp     \n"  // main stack
        "mrsne r0, psp     \n"  // process stack
        "b hardfault_report\n"  // r0 = stacked frame
    );
}

In hardfault_report you cast r0 to a frame struct and read frame->pc. Match that address against your map file or let the debugger symbolize it, and you have the line that crashed.

The usual suspects

Most hard faults are one of a short list.

  • Null or wild pointer dereference. A read or write to address 0 or to unmapped memory. BusFault with a valid BFAR pointing at the bad address.
  • Stack overflow. The stack grows into other memory and corrupts it, then a return reads a garbage PC. The stacked PC looks nonsensical because it is.
  • Bad function pointer. A call through an uninitialized or corrupted pointer, often landing at an odd address that also trips the Thumb-state rules.
  • Unaligned access. Some instructions require aligned addresses. A misaligned access raises a UsageFault that escalates to HardFault unless you handled it.

Why this is the core embedded skill

You cannot printf your way out of every hard fault, because sometimes the fault is in the code that would do the printing. The reliable path is to read the machine's own record of the failure: the fault registers say what kind, the stacked frame says where. That is pure diagnosis under a boundary condition, and it is the exact competence OhmWork's debugging challenges are designed to measure.