
When a Cortex-M processor encounters a hard fault, the last thing you want is for your fault handler to make things worse by corrupting the stack. Yet this is a common issue that turns a debuggable fault into a system lockup. Let’s look at why this happens and how to fix it.
A Cortex-M hard fault handler that uses too much stack space can corrupt the very exception stack frame it’s trying to examine. This creates a vicious cycle: the fault occurs, the handler starts executing, uses excessive stack, overflows into the faulted context’s stack frame, corrupts the return address or registers, and triggers another fault—often locking up the system completely.
The Cortex-M processor automatically stacks certain registers (R0-R3, R12, LR, PC, xPSR) when an exception occurs. This basic exception stack frame is fixed at 32 bytes (or 104 bytes if the floating-point unit (FPU) is active and extended stacking occurs). Your hard fault handler then executes in handler mode, always using the main stack pointer (MSP).
When an exception occurs, the processor pushes the following 8 registers (32 bytes total for a basic frame):
Exception handlers on Cortex-M always execute using the MSP (Main Stack Pointer). The CONTROL[1] (SPSEL) bit determines which stack pointer Thread mode uses:
When a fault occurs, the processor pushes the exception frame onto whichever stack the faulted code was using (MSP or PSP), then switches to MSP for the handler. The EXC_RETURN value in LR tells you which stack holds the frame. If your main stack is nearly exhausted, the handler’s own stack usage on MSP can overflow.
Stack corruption typically occurs in these scenarios:
printf, sprintf, or complex logging functionschar buffer[256] or struct FaultInfo info on stackThe fix follows a simple principle: keep your hard fault handler as minimal as possible. Every byte of stack you save in the handler reduces the risk of corruption.
Instead of:
void HardFault_Handler(void) {uint32_t registers[8]; // 32 bytes on stackchar buffer[128]; // 128 bytes on stackstruct FaultInfo info; // Another 32+ bytes// ... handler code}
Do this:
void HardFault_Handler(void) {static uint32_t registers[8]; // Static allocationstatic char buffer[128]; // Static allocationstatic struct FaultInfo info; // Static allocation// ... handler code using static variables}
Static variables live in .bss or .data sections, not on the stack. This moves your storage requirements off the stack entirely.
Stack Memory (grows down) .bss/.data Section (fixed)+-------------------------+ +-------------------------+| Exception Stack Frame | | static uint32_t regs[8] || (32 bytes, fixed) | | static char buf[128] |+-------------------------+ vs | static struct info || Handler local vars | | || (DANGEROUS if large) | | (zero stack impact) |+-------------------------+ +-------------------------+
Each function call uses stack space for return addresses, parameters, and local variables. In a hard fault handler, avoid calling complex functions.
Instead of:
void HardFault_Handler(void) {log_fault_details(); // Who knows how much stack this uses?send_alert_via_uart(); // Another unknownanalyze_fault_cause(); // And another}
Do this:
static void fault_handler_impl(void) {// Minimal implementation - no function calls// Direct register manipulation only}void HardFault_Handler(void) {fault_handler_impl(); // Single, predictable call}
Or better yet, inline the critical code directly in the handler.
| Call Type | Typical Stack Usage |
|---|---|
| Leaf function (no locals) | 0-8 bytes (may not push LR) |
| Function with 4 params | 16-32 bytes |
printf family | 100-500+ bytes |
memcpy/memset | 16-64 bytes |
| Complex logging | 200-1000+ bytes |
You can’t fix what you don’t measure. Add stack monitoring to your hard fault handler:
void HardFault_Handler(void) {// Get current stack pointeruint32_t* sp = (uint32_t*)__get_MSP();// Stack grows downward on Cortex-M// Check if we're getting too close to stack limitextern uint32_t _estack; // Defined in linker scriptuint32_t stack_used = (uint32_t)&_estack - (uint32_t)sp; // Calculate in bytes// If stack usage is critical, trigger minimal fault indicationif (stack_used > CRITICAL_STACK_THRESHOLD) {// Set GPIO pin, toggle LED, etc. - minimal actionGPIOB->ODR |= GPIO_ODR_ODR_5;}// Continue with normal fault handling...}
Use compiler flags to analyze stack usage:
# GCC/Clang: report stack usage per functionarm-none-eabi-gcc -fstack-usage -c hard_fault.c# Output: hard_fault.su with per-function stack usage
Ensure your linker script allocates sufficient stack for handler mode. The default stack size might be too small if you have nested exceptions or deep fault handling.
In your linker script:
/* Stack size for handler mode (exceptions) */_estack = 0x20008000; /* Example: 32KB stack top */_stack_size = 0x2000; /* 8KB for handler mode */_Min_Heap_Size = 0x200; /* 512B heap */_Min_Stack_Size = 0x400; /* 1KB minimum stack */
Minimum handler stack = Exception frame (32B, or 104B with FPU)+ Handler locals (static = 0B, dynamic = X)+ Nested exception margin (2x frame = 64B or 208B)+ Function call overhead (Y)+ Safety margin (256B-1KB)Typical safe allocation: 2KB-8KB for handler mode
Extract fault information without function calls:
void HardFault_Handler(void) {// Read fault status registers directlyuint32_t cfsr = SCB->CFSR; // Configurable Fault Status Registeruint32_t hfsr = SCB->HFSR; // Hard Fault Status Registeruint32_t dfsr = SCB->DFSR; // Debug Fault Status Registeruint32_t mmfar = SCB->MMFAR; // MemManage Fault Address Registeruint32_t bfar = SCB->BFAR; // Bus Fault Address Register// Store to static variables for later analysisstatic uint32_t fault_cfsr, fault_hfsr, fault_mmfar, fault_bfar;fault_cfsr = cfsr;fault_hfsr = hfsr;fault_mmfar = mmfar;fault_bfar = bfar;// Minimal indicationGPIOD->ODR ^= GPIO_ODR_ODR_12;while (1) { /* halt */ }}
Here’s a complete hard fault handler that minimizes stack usage:
#include "stm32f4xx.h"// Static storage for fault analysis - zero stack usagestatic uint32_t stacked_r0;static uint32_t stacked_r1;static uint32_t stacked_r2;static uint32_t stacked_r3;static uint32_t stacked_r12;static uint32_t stacked_lr;static uint32_t stacked_pc;static uint32_t stacked_psr;// Fault status registersstatic uint32_t fault_cfsr;static uint32_t fault_hfsr;static uint32_t fault_dfsr;static uint32_t fault_mmfar;static uint32_t fault_bfar;// Naked wrapper: no compiler prologue, so LR still holds EXC_RETURN__attribute__((naked)) void HardFault_Handler(void) {__asm volatile (" tst lr, #4 \n"" ite eq \n"" mrseq r0, msp \n"" mrsne r0, psp \n"" b HardFault_Handler_C \n");}// C handler receives the stack frame pointer in r0 (first argument)void HardFault_Handler_C(uint32_t *frame) {// Capture fault status registers immediatelyfault_cfsr = SCB->CFSR;fault_hfsr = SCB->HFSR;fault_dfsr = SCB->DFSR;fault_mmfar = SCB->MMFAR;fault_bfar = SCB->BFAR;// Extract stacked registers from the exception framestacked_r0 = frame[0]; // offset 0stacked_r1 = frame[1]; // offset 4stacked_r2 = frame[2]; // offset 8stacked_r3 = frame[3]; // offset 12stacked_r12 = frame[4]; // offset 16stacked_lr = frame[5]; // offset 20stacked_pc = frame[6]; // offset 24stacked_psr = frame[7]; // offset 28// Minimal fault indication - toggle LEDGPIOD->ODR ^= GPIO_ODR_ODR_12;// Optional: send minimal fault code via ITMif (ITM->TCR & ITM_TCR_ITMENA_Msk) {ITM->PORT[0].u8 = 0xDE; // Fault markerITM->PORT[0].u8 = stacked_r0 & 0xFF;ITM->PORT[0].u8 = stacked_r1 & 0xFF;}// Halt or reset as appropriate for your systemwhile (1) {// Spin forever or trigger watchdog reset}}
This handler:
naked assembly wrapper to capture EXC_RETURN before the compiler modifies LRr0)+-----------------------+---------------------------------------------------+| HardFault_Handler (naked): |+-----------------------+---------------------------------------------------+| tst lr, #4 | Check EXC_RETURN bit 2 (MSP vs PSP) || ite eq | If-then-else: equal -> MSP, not equal -> PSP || mrseq r0, msp | Move MSP to R0 if equal || mrsne r0, psp | Move PSP to R0 if not equal || b HardFault_Handler_C | Branch to C handler with frame ptr in R0 |+-----------------------+---------------------------------------------------+| HardFault_Handler_C(frame): |+-----------------------+---------------------------------------------------+| frame[0] = R0 | Stacked R0 (offset 0) || frame[1] = R1 | Stacked R1 (offset 4) || frame[2] = R2 | Stacked R2 (offset 8) || frame[3] = R3 | Stacked R3 (offset 12) || frame[4] = R12 | Stacked R12 (offset 16) || frame[5] = LR | Stacked LR (offset 20) || frame[6] = PC | Stacked PC (offset 24) || frame[7] = xPSR | Stacked xPSR (offset 28) |+-----------------------+---------------------------------------------------+
After implementing your minimal hard fault handler:
arm-none-eabi-gcc -Wl,-Map=output.map -o firmware.elf ...grep -A5 -B5 "HardFault_Handler" output.map
Verify your handler doesn’t show large stack consumption.
// Force a hard fault by branching to an invalid/non-thumb addressvoid test_hard_fault(void) {void (*bad_func)(void) = (void (*)(void))0x00000000;bad_func();}
The system should fault, execute your handler minimally, and either halt or reset cleanly.
Use a debugger to inspect the stacked register values and fault status registers:
(gdb) monitor reset halt(gdb) b HardFault_Handler(gdb) c# Trigger fault...(gdb) p/x fault_cfsr(gdb) p/x fault_hfsr(gdb) p/x stacked_pc
Ensure your handler doesn’t fault again when handling the initial fault. A second fault during hard fault handling causes the processor to enter the Lockup state on all Cortex-M variants.
The semi-hosting or UART functions can use hundreds of bytes of stack. Never call printf, sprintf, snprintf, or any variadic function.
Even simple string formatting can blow your stack. If you need logging, use static buffers and direct register writes.
That 256-byte buffer for fault analysis? It’s on the stack. Make it static.
Check your linker script - handler mode needs adequate space. A 1KB stack is often insufficient.
Creates infinite fault loops that lock up the system. Test thoroughly.
If your RTOS uses PSP for tasks, the faulted context might be on PSP, not MSP. Always check the EXC_RETURN value.
Some fault status bits are sticky. Clear them before re-enabling interrupts or continuing.
With a properly designed minimal hard fault handler:
| Metric | Before | After |
|---|---|---|
| Stack usage | 200+ bytes | < 32 bytes (just exception frame) |
| Fault recovery | Unreliable | System reliably enters handler mode |
| Debug visibility | Corrupted registers | Fault context registers intact |
| Deterministic behavior | Unpredictable | Predictable execution time |
| System stability | Lockups common | No lockups from handler corruption |
On Cortex-M processors, a fault occurring during hard fault handling causes an immediate Lockup state. Once in Lockup, the processor halts instruction execution. However, an NMI (Non-Maskable Interrupt, priority -2) can preempt the Lockup state, providing a final opportunity to safely log the error or trigger a reset:
// NMI Handler can preempt a Lockup statevoid NMI_Handler(void) {// If we were in Lockup, NMI will still execute// We can attempt emergency recovery or logging hereif (SCB->HFSR & SCB_HFSR_FORCED_Msk) {// Double fault occurred - minimal recovery attemptstatic uint32_t double_fault_count;double_fault_count++;// Emergency indicationGPIOE->ODR |= GPIO_ODR_ODR_5;// Log to backup registers (if available)// RTC backup registers survive reset on many STM32RTC->BKP0R = double_fault_count;RTC->BKP1R = fault_cfsr;RTC->BKP2R = stacked_pc;}while (1) { /* halt */ }}
Fixing Cortex-M hard fault handler stack corruption requires shifting your mindset from “diagnostic tool” to “minimal survival kit”. Your hard fault handler isn’t the place for comprehensive fault analysis—it’s the last line of defense before system failure. By keeping it minimal, using static storage, limiting function calls, and verifying adequate stack allocation, you ensure that when faults occur, your handler can actually do its job: preserve fault context, provide minimal indication, and allow for safe system recovery or reset.
The next time you’re debugging a hard fault, ask yourself: “Is my handler making the problem worse?” If it’s using significant stack space, the answer is likely yes. Apply these principles, and your hard fault handler will become a reliable diagnostic tool rather than a source of additional instability.
💡 Key Takeaway: In exception handlers, every byte of stack you save increases system reliability. Store data statically, minimize function calls, and keep your hard fault handler as small as possible.
Quick Links
Legal Stuff





