1

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.

Atilla Filiz
  • 2,383
  • 8
  • 29
  • 47

2 Answers2

1

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.

cooperised
  • 2,404
  • 1
  • 15
  • 18
0

I used the following code with GCC.

void McuHardFault_HandlerC(uint32_t *hardfault_args)
{
  static volatile unsigned long stacked_r0 __attribute__((unused));
  static volatile unsigned long stacked_r1 __attribute__((unused));
  static volatile unsigned long stacked_r2 __attribute__((unused));
  static volatile unsigned long stacked_r3 __attribute__((unused));
  static volatile unsigned long stacked_r12 __attribute__((unused));
  static volatile unsigned long stacked_lr __attribute__((unused));
  static volatile unsigned long stacked_pc __attribute__((unused));
  static volatile unsigned long stacked_psr __attribute__((unused));
  static volatile unsigned long _CFSR __attribute__((unused));
  static volatile unsigned long _HFSR __attribute__((unused));
  static volatile unsigned long _DFSR __attribute__((unused));
  static volatile unsigned long _AFSR __attribute__((unused));
  static volatile unsigned long _BFAR __attribute__((unused));
  static volatile unsigned long _MMAR __attribute__((unused));
  stacked_r0 = ((unsigned long)hardfault_args[0]);
  stacked_r1 = ((unsigned long)hardfault_args[1]);
  stacked_r2 = ((unsigned long)hardfault_args[2]);
  stacked_r3 = ((unsigned long)hardfault_args[3]);
  stacked_r12 = ((unsigned long)hardfault_args[4]);
  stacked_lr = ((unsigned long)hardfault_args[5]);
  stacked_pc = ((unsigned long)hardfault_args[6]);
  stacked_psr = ((unsigned long)hardfault_args[7]);

  /* Configurable Fault Status Register */
  /* Consists of MMSR, BFSR and UFSR */
  _CFSR = (*((volatile unsigned long *)(0xE000ED28)));

  /* Hard Fault Status Register */
  _HFSR = (*((volatile unsigned long *)(0xE000ED2C)));

  /* Debug Fault Status Register */
  _DFSR = (*((volatile unsigned long *)(0xE000ED30)));

  /* Auxiliary Fault Status Register */
  _AFSR = (*((volatile unsigned long *)(0xE000ED3C)));


  /* Read the Fault Address Registers. */
  /* These may not contain valid values. */
  /* Check BFARVALID/MMARVALID to see */
  /* if they are valid values */
  /* MemManage Fault Address Register */
  _MMAR = (*((volatile unsigned long *)(0xE000ED34)));
  /* Bus Fault Address Register */
  _BFAR = (*((volatile unsigned long *)(0xE000ED38)));

  __asm("BKPT #0\n") ; /* cause the debugger to stop */
}

void HardFault_Handler(void) __attribute__((naked));
void HardFault_Handler(void)
{
  __asm volatile (
    ".syntax unified              \n"  /* needed for the 'adds r1,#2' below */
    " movs r0,#4                  \n"  /* load bit mask into R0 */
    " mov r1, lr                  \n"  /* load link register into R1 */
    " tst r0, r1                  \n"  /* compare with bitmask */
    " beq _MSP                    \n"  /* if bitmask is set: stack pointer is in PSP. Otherwise in MSP */
    " mrs r0, psp                 \n"  /* otherwise: stack pointer is in PSP */
    " b _GetPC                    \n"  /* go to part which loads the PC */
  "_MSP:                          \n"  /* stack pointer is in MSP register */
    " mrs r0, msp                 \n"  /* load stack pointer into R0 */
  "_GetPC:                        \n"  /* find out where the hard fault happened */
    " ldr r1,[r0,#24]             \n"  /* load program counter into R1. R1 contains address of the next instruction where the hard fault happened */
    " b McuHardFault_HandlerC   \n"  /* decode more information. R0 contains pointer to stack frame */
  );
}
Atilla Filiz
  • 2,383
  • 8
  • 29
  • 47