When a hard fault occurs on Cortex-M0+, it is critical to be able to print out the call stack to debug what went wrong. This can be achieved by implementing a hard fault handler that leverages the stack frame to output the call stack. Here is a step-by-step guide on how to do this on Cortex-M0+.
Overview of Hard Faults
A hard fault on Cortex-M is essentially an unrecoverable runtime exception that happens when the processor detects an error condition that compromises normal execution. This could occur due to things like:
- Accessing restricted registers
- Executing invalid instructions
- Violating memory protections
- Encountering an unexpected exception
When a hard fault happens, the processor immediately jumps to the hard fault handler if it’s implemented. The hard fault handler’s job is to try to gracefully record as much context about the fault as possible. This usually involves saving the stack frame and registers so we can understand the call stack that led to the fault.
Setting Up the Hard Fault Handler
To enable hard fault handling, we need to set up an exception handler function and then register it to handle hard faults specifically. Here are the steps:
- Define a hard fault handler function with the following signature: void HardFault_Handler(void) { /* handler code */ } This follows the standard naming convention of ExceptionName_Handler().
- Inside the System Handler Control and State Register (SHCSR), enable the hard fault exception by setting the HF bit to 1.
- Set the interrupt vector table’s hard fault exception entry to point to our handler function.
Parsing the Stack Frame
When the hard fault handler runs, the processor would have already automatically stacked the context that led to the fault. This context is stored in the stack frame, which we need to parse to print the call stack.
Here’s how to parse the stack frame on Cortex-M0+ step-by-step:
- Get the stacked program counter value which contains the address of the instruction that caused the fault: uint32_t stacked_pc = __get_PSP();
- Get the current stack pointer: uint32_t current_sp = __get_MSP();
- Calculate stacked frame address by subtracting the size of stacked context from current stack pointer: uint32_t stacked_frame = current_sp – 0x20; 0x20 is the fixed frame size on Cortex-M0+.
- Typecast stacked frame address to a StackFrame struct: StackFrame* frame = (StackFrame*)stacked_frame; The StackFrame struct is defined as: struct StackFrame { uint32_t r0; uint32_t r1; uint32_t r2; uint32_t r3; uint32_t r12; uint32_t lr; uint32_t pc; uint32_t psr; }; This matches the register order on the stack frame.
- The lr register contains the return address of the last function call before the fault. So we can traverse backwards using lr to print the call stack. void print_stack(StackFrame* frame) { while(frame) { // Print current stack frame print(“%x\r\n”, frame->pc); // Go to next stack frame frame = (StackFrame*)frame->lr; } } This will continuously traverse the stack frames and print the program counter values which correspond to call sites.
By following these steps, we can parse the stacked context and print the call stack that led to the hard fault on Cortex-M0+.
Putting It All Together
Here is how the full hard fault handler code looks to print the call stack on Cortex-M0+: /* Stack frame struct */ struct StackFrame { uint32_t r0; uint32_t r1; uint32_t r2; uint32_t r3; uint32_t r12; uint32_t lr; uint32_t pc; uint32_t psr; }; void print_stack(StackFrame* frame) { while(frame) { // Print current stack frame print(“%x\r\n”, frame->pc); // Go to next stack frame frame = (StackFrame*)frame->lr; } } void HardFault_Handler(void) { __asm( “TST LR, #4” ); __asm( “ITE EQ” ); __asm( “MRSEQ R0, MSP” ); __asm( “MRSNE R0, PSP” ); __asm( “B print_stack” ); } int main() { // Enable hard fault SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk; // Set hard fault handler void (*vector_table[])(void) = { /* other code */ HardFault_Handler }; // Function calls foo(); bar(); return 0; }
This demonstrates how to setup a hard fault handler, parse the stack frame and print the call stack to debug hard faults on Cortex-M0+. The key steps are:
- Configure SHCSR to enable hard faults
- Set the hard fault vector table entry to the handler function
- In the handler, get the stacked PC and SP
- Calculate the stacked frame address
- Typecast frame address to StackFrame struct
- Traverse lr registers to print each stack frame’s pc
With this, we can get a full dump of the call stack on hard faults to help debug crashes on Cortex-M0+ systems.
Tips for Advanced Usage
Here are some additional tips for improving and building on the hard fault handler:
- Register dumping – Dump registers like R0-R3, R12 from stack frame
- Stack overflow detection – Check for stack overflow errors
- Global error state – Set global error variable on fault
- Fault logging – Log faults to external memory
- Fault diagnostics – Run diagnostics to identify fault reason
- Debug printing – Support debug printing over a serial port
These kind of enhancements can help make the fault handler more robust and useful in debugging crashes in the field.
Conclusion
Implementing a hard fault handler to print the call stack provides extremely valuable insights into crashes on Cortex-M0+ systems. By parsing the stacked context we can reconstruct the stack frames leading up to a fault. This guide covers the key steps involved in setting up such a handler using standard C code.
With the handler in place, hard faults can be debugged effectively even in deployed devices. The techniques can be enhanced further to dump more state and diagnose the root cause of faults. Overall, hard fault handlers are a critical tool for developing reliable Cortex-M0+ applications.