Search code examples
cinterrupt-handlingcortex-mthumb

How can I save the call stack from an exception handler in Cortex M4?


Here is what I want to achieve: Whenever I receive a hard fault or watchdog interrupt, I will save the previous instruction's address to some RAM location that will survive a reset.

Kinetis M64 watchdog gives me 256 CPU cycles before it issues the reset, which should be enough time to save a few things. The question is where do I find these addresses? When an IRQ happens, LR holds an exception value instead of the actual return address.

I want to do this without an SWD probe attached, so the device can self-report whenever something goes wrong.


Solution

  • The best you can do, I think, is to find the address of the instruction that the handler would return to if it was allowed to return. This will usually be the instruction after the one that caused the fault, though this is not guaranteed (for example if the fault was caused by a branch instruction).

    On entry to the handler the link register contains a code which tells you, among other things, which stack was in use when the exception occurred. See for example here for the Cortex-M4.

    Just before branching to the exception handler, the CPU will push r0-r3, r12, LR (r14), PC (r15) and xPSR to the active stack. If your device has a FPU and it's enabled, floating-point context may also be pushed, or space left for it. The ARM stack is full descending, and registers are stored in ascending order of register number in ascending memory addresses, so on entry to the exception handler whichever stack pointer was in use will be pointing to the stacked value of r0; it stands to reason therefore that 6 words (24 bytes) above that will be the stacked value of PC, which is the return address for the exception handler.

    So the process for finding the instruction after the one that caused the fault, assuming it wasn't caused by a branch, is:

    • Examine LR to find out which stack was in use
    • Load the appropriate stack pointer into a free register (r0-r3 are all available because they're pushed on entry to the handler)
    • Read the word 24 bytes above this stack pointer to find the return address for the handler

    Whether the instruction that caused the fault is located 2 or 4 bytes before this return address depends on the instruction of course, the Thumb-2 instruction set is mixed 16- and 32-bit. And of course it might be located somewhere else entirely!

    Bear in mind that if the MSP was in use prior to the fault, then the handler will be using the same stack, and all of this will work only if nothing has been pushed to the stack in the handler function's prologue. The easiest thing to do may be to write the handler in assembly language. It can always call a C function after it's finished messing with stacks to complete whatever termination process you have in mind.

    One last thing, it's probably worth also saving the stacked value of LR. If the stacked value of PC tells you nothing of use (for example because it's zero, the code having attempted to branch to an invalid address), then the stacked value of LR will at least tell you where the last BL instruction was encountered, and if you're lucky this will be the branch that caused the fault. Even if you're not that lucky it may help you to narrow down your search.

    Code

    Here's some (untested) code that might do what you want. It's written in ARMASM syntax, so you'll need to change the odd thing if you're using a different toolchain:

        IMPORT cHandler
    
        TST   lr, 0x4       ; Is bit 2 of LR clear?
        ITE   eq
        MRSEQ r3, MSP       ; If so, MSP was in use
        MRSNE r3, PSP       ; Otherwise, PSP was in use
        LDR   r0, [r3, #24] ; Load the stacked PC into r0
        LDR   r1, [r3, #20] ; Load the stacked LR into r1
        B     cHandler      ; Tail-call a C function to finish the job
    

    If the C function cHandler has the prototype

    void cHandler(void * PC, void * LR);
    

    then the last line of the assembly language handler above will call this function, passing the recovered stacked PC as the first argument and the recovered stacked LR as the second.