
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, RETURN_ADDRESS, xPSR) when an exception occurs. This exception stack frame is fixed at 32 bytes. Your hard fault handler then executes in handler mode, using the main stack pointer (MSP) or process stack pointer (PSP) depending on your configuration.
When an exception occurs, the processor pushes the following 8 registers (32 bytes total):
The stack pointer used in handler mode depends on the CONTROL register bit 1:
CONTROL[1] = 0CONTROL[1] = 1 and in Thread modeMost bare-metal and RTOS configurations use MSP for all exception handlers. If your hard fault handler uses MSP and the main stack is nearly exhausted, you have a problem.
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] │├─────────────────────────┤ │ static struct info ││ Handler local vars │ vs │ (zero stack impact) ││ (DANGEROUS if large) │ └─────────────────────────┘└─────────────────────────┘
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) | 8-16 bytes (return addr + frame) |
| 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 = &_estack - sp;// 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)+ Handler locals (static = 0B, dynamic = X)+ Nested exception margin (2x frame = 64B)+ 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;void HardFault_Handler(void) {// Capture fault status registers immediatelyfault_cfsr = SCB->CFSR;fault_hfsr = SCB->HFSR;fault_dfsr = SCB->DFSR;fault_mmfar = SCB->MMFAR;fault_bfar = SCB->BFAR;__asm volatile (" tst lr, #4 \n"" ite eq \n"" mrseq r0, msp \n"" mrsne r0, psp \n"" ldr r1, [r0, #24] \n"" ldr r2, [r0, #20] \n"" ldr r3, [r0, #16] \n"" ldr r4, [r0, #12] \n"" ldr r5, [r0, #8] \n"" ldr r6, [r0, #4] \n"" ldr r7, [r0] \n"" str r0, [%[stacked_pc]] \n"" str r1, [%[stacked_r0]] \n"" str r2, [%[stacked_r1]] \n"" str r3, [%[stacked_r2]] \n"" str r4, [%[stacked_r3]] \n"" str r5, [%[stacked_r12]] \n"" str r6, [%[stacked_lr]] \n"" str r7, [%[stacked_psr]] \n":: [stacked_pc] "r" (&stacked_pc),[stacked_r0] "r" (&stacked_r0),[stacked_r1] "r" (&stacked_r1),[stacked_r2] "r" (&stacked_r2),[stacked_r3] "r" (&stacked_r3),[stacked_r12] "r" (&stacked_r12),[stacked_lr] "r" (&stacked_lr),[stacked_psr] "r" (&stacked_psr): "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7");// 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:
┌─────────────────────────────────────────────────────────────────┐│ 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 │├─────────────────────────────────────────────────────────────────┤│ ldr r1, [r0, #24] │ Load stacked R0 (offset 24) ││ ldr r2, [r0, #20] │ Load stacked R1 (offset 20) ││ ldr r3, [r0, #16] │ Load stacked R2 (offset 16) ││ ldr r4, [r0, #12] │ Load stacked R3 (offset 12) ││ ldr r5, [r0, #8] │ Load stacked R12 (offset 8) ││ ldr r6, [r0, #4] │ Load stacked LR (offset 4) ││ ldr r7, [r0] │ Load stacked PC (offset 0) │├─────────────────────────────────────────────────────────────────┤│ str r0-r7 to static │ Store to pre-allocated static variables │└─────────────────────────────────────────────────────────────────┘
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 for testingvoid test_hard_fault(void) {volatile uint32_t* null_ptr = (uint32_t*)0x0;*null_ptr = 0xDEADBEEF; // Null pointer dereference}
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 lockup (Cortex-M3/M4) or escalates to NMI (Cortex-M7/M33).
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-M3/M4, a fault during hard fault handling causes immediate lockup. On Cortex-M7/M33, it escalates to NMI. You can prepare:
// NMI Handler for Cortex-M7/M33 double fault escalationvoid NMI_Handler(void) {// Check if this is a double fault escalationif (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





