Debugging HardFault on Cortex-M: CFSR, Stacked PC and Fault Handlers
Sooner or later every Cortex-M program stops in HardFault_Handler, an endless loop with no hint of what went wrong. The debugger's call stack shows only the handler, and the bug could be anywhere. Fortunately the core records a great deal of information when a fault happens: which kind of fault it was, the faulting data address in many cases, and the exact instruction that was running, saved on the stack. This article shows how to collect that information with a small handler and turn it into a file name and line number.
Fault Exceptions on Cortex-M
Cortex-M3, M4 and M7 have four fault exceptions. MemManage, BusFault and UsageFault are disabled after reset, so every fault escalates to HardFault until you enable them in the SHCSR register. Cortex-M0 and M0+ have only HardFault and no fault status registers, so on those cores the stacked PC is your main clue.
| Exception | Number | Typical causes |
|---|---|---|
| HardFault | 3 | Escalated faults, vector table read error, fault during another fault handler |
| MemManage | 4 | MPU violation, executing from a non-executable region |
| BusFault | 5 | Access to an unmapped address, peripheral with its clock disabled, stack overflow into invalid memory |
| UsageFault | 6 | Undefined instruction, Thumb bit cleared, divide by zero (if enabled), unaligned access (if enabled), FPU used before enabling |
The Fault Status Registers
The Configurable Fault Status Register CFSR at 0xE000ED28 combines three sub-registers: MMFSR in bits 7:0, BFSR in bits 15:8 and UFSR in bits 31:16. Bits stay set until you write 1 to clear them. The HardFault Status Register HFSR at 0xE000ED2C has the FORCED bit (bit 30), which means a configurable fault escalated, and VECTTBL (bit 1), which means the vector table read failed.
| CFSR bit | Name | Meaning |
|---|---|---|
| 0 | IACCVIOL | Instruction fetch from a forbidden region |
| 1 | DACCVIOL | Data access violation; address in MMFAR if bit 7 is set |
| 7 | MMARVALID | MMFAR (0xE000ED34) holds the faulting address |
| 8 | IBUSERR | Bus error on instruction fetch |
| 9 | PRECISERR | Precise data bus error; BFAR is valid if bit 15 is set |
| 10 | IMPRECISERR | Imprecise data bus error; stacked PC is after the real culprit |
| 12 | STKERR | Bus error while stacking on exception entry, often a stack overflow |
| 15 | BFARVALID | BFAR (0xE000ED38) holds the faulting address |
| 16 | UNDEFINSTR | Undefined instruction |
| 17 | INVSTATE | Attempt to execute in ARM state, usually a function pointer without the Thumb bit |
| 18 | INVPC | Invalid EXC_RETURN value, often a corrupted stack |
| 19 | NOCP | Coprocessor (FPU) instruction while the FPU is disabled |
| 24 | UNALIGNED | Unaligned access when UNALIGN_TRP is enabled |
| 25 | DIVBYZERO | Integer divide by zero when DIV_0_TRP is enabled |
Recovering the Faulting Instruction
On exception entry the core pushes eight words onto the active stack: R0, R1, R2, R3, R12, LR, PC and xPSR, in that order from the lowest address. The stacked PC is the address of the instruction that faulted (for precise faults), and the stacked LR is the return address of the function that was running. The handler must first find out which stack was used. Bit 2 of the EXC_RETURN value in LR tells it: 0 means MSP, 1 means PSP. Because this test must run before the compiler touches the stack, the entry is written as a naked assembly function.
#include <stdint.h>
#define SCB_SHCSR (*(volatile uint32_t *)0xE000ED24u)
#define SCB_CFSR (*(volatile uint32_t *)0xE000ED28u)
#define SCB_HFSR (*(volatile uint32_t *)0xE000ED2Cu)
#define SCB_MMFAR (*(volatile uint32_t *)0xE000ED34u)
#define SCB_BFAR (*(volatile uint32_t *)0xE000ED38u)
#define SCB_CCR (*(volatile uint32_t *)0xE000ED14u)
/* Copied here so the debugger can show them after the fault */
typedef struct {
uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr;
uint32_t cfsr, hfsr, mmfar, bfar;
} fault_info_t;
volatile fault_info_t fault_info;
void fault_report(uint32_t *frame)
{
fault_info.r0 = frame[0];
fault_info.r1 = frame[1];
fault_info.r2 = frame[2];
fault_info.r3 = frame[3];
fault_info.r12 = frame[4];
fault_info.lr = frame[5];
fault_info.pc = frame[6]; /* the faulting instruction */
fault_info.xpsr = frame[7];
fault_info.cfsr = SCB_CFSR;
fault_info.hfsr = SCB_HFSR;
fault_info.mmfar = SCB_MMFAR;
fault_info.bfar = SCB_BFAR;
__asm volatile ("bkpt #0"); /* stop here if a debugger is attached */
while (1)
;
}
/* Select MSP or PSP using EXC_RETURN bit 2, then call fault_report(frame) */
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile (
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b fault_report \n"
);
}
/* Route MemManage, BusFault and UsageFault to the same handler */
void MemManage_Handler(void) __attribute__((alias("HardFault_Handler")));
void BusFault_Handler(void) __attribute__((alias("HardFault_Handler")));
void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler")));
void fault_setup(void)
{
SCB_SHCSR |= (1u << 16) | (1u << 17) | (1u << 18); /* enable MEM, BUS, USG faults */
SCB_CCR |= (1u << 4); /* trap divide by zero */
}
/* ---- Deliberate faults for testing ---- */
int main(void)
{
volatile uint32_t zero = 0;
volatile uint32_t r;
fault_setup();
r = 100u / zero; /* UsageFault: CFSR bit 25 DIVBYZERO */
/* Other tests: uncomment one at a time
* r = *(volatile uint32_t *)0xFFFFFFF0u; BusFault, BFAR = 0xFFFFFFF0
* ((void (*)(void))0x20000000u)(); INVSTATE: Thumb bit clear
*/
(void)r;
while (1)
;
}
From Fault Data to Source Line
- Run the program under the debugger. It stops at the bkpt instruction inside fault_report.
- Add fault_info to the watch window. For the divide test, cfsr shows 0x02000000, which is bit 25, DIVBYZERO.
- Copy the pc value and run arm-none-eabi-addr2line -e app.elf -f -C 0x
. It prints the function name and the file and line of the faulting instruction. - If the fault is IMPRECISERR, the stacked PC is a few instructions after the real store. Temporarily disable write buffering (DISDEFWBUF in the ACTLR register on Cortex-M3/M4) to make bus faults precise, then repeat.
- If pc looks like garbage, check lr. A corrupted PC usually means a bad function pointer or a stack overwrite, and lr still points at the caller.
Most Common Real Causes
- Stack overflow from large local buffers, printf with floating point, or recursion.
- Accessing a peripheral before enabling its clock or power bit in the vendor's clock control register.
- Calling a NULL or uninitialised function pointer, giving INVSTATE or IACCVIOL.
- Enabling hardware float in the compiler but not setting CP10 and CP11 full access in CPACR, giving NOCP.
- Casting a byte buffer to a struct or uint32_t pointer and using LDM, LDRD or any word access on Cortex-M0.
- Writing past the end of an array and overwriting a saved return address.
Summary
Enable MemManage, BusFault and UsageFault in SHCSR so faults are reported specifically. In the handler, use EXC_RETURN bit 2 to choose MSP or PSP, copy the eight-word stack frame and the CFSR, HFSR, MMFAR and BFAR registers, then stop. The CFSR bits tell you the kind of fault, BFAR or MMFAR give the bad address, and addr2line turns the stacked PC into a source line.