Suggested improvement: ISR stack checking (GCC M4)

dnadler wrote on Wednesday, August 21, 2019:

For Cortex M4 (and other MCUs) ISR’s run on a separate stack from FreeRTOS tasks (MSP).
To ensure bulletproof operation, its a good idea to verify all stacks have adequate space :wink:
I suggest the following additions:

  1. In configuration, new #defines:
// DRN ISR (MSP) stack initialization and checking
#define configISR_STACK_SIZE_WORDS (0x100) // DRN in WORDS, must be valid constant for assembler
#define configSUPPORT_ISR_STACK_CHECK  1   // DRN initialize and check ISR stack
  1. Additional code in port.c (this is for the M4 GCC port). I’ve tried to keep this tight but my ARM-GCC-assembler isn’t super-strong…
#define QUOTE(str) #str
#define EXPAND_AND_QUOTE(str) QUOTE(str)

#if defined(configSUPPORT_ISR_STACK_CHECK) && configSUPPORT_ISR_STACK_CHECK && !defined(configISR_STACK_SIZE_WORDS)
  #error "configISR_STACK_SIZE_WORDS must be defined for ISR stack checking (WORDS to reserve for MSP stack)"
#endif

static void prvPortStartFirstTask( void )
{
    /* Start the first task.  This also clears the bit that indicates the FPU is
    in use in case the FPU was used before the scheduler was started - which
    would otherwise result in the unnecessary leaving of space in the SVC stack
    for lazy saving of FPU registers. */
    __asm volatile(
      " ldr r0, =0xE000ED08  \n" /* Use the NVIC offset register to locate the initial stack. */
      " ldr r0, [r0]         \n"
      " ldr r0, [r0]         \n" /* r0 now has top of stack (actually, word above beginning of stack) */
      " msr msp, r0          \n" /* Set the MSP (*ISR* stack register) back to the start of the stack (top of RAM). */

    #if defined(configSUPPORT_ISR_STACK_CHECK) && configSUPPORT_ISR_STACK_CHECK
      // DRN code: Zero the MSP stack before use, to facilitate stack use check
      " mov r1, #0           \n" /* value to store into stack */
      " ldr r2, pxMSRstackLen\n" /* remaining words to clear in stack */
      "FillZeroMSPstack:     \n"
      " sub r0, r0, #4       \n" /* next word to zero */
      " str r1, [r0]         \n" /* store a zero */
      " sub r2, r2, #1       \n" /* decrement word count */
      " cmp r2, #0           \n" /* finished clearing stack? */
      " bne FillZeroMSPstack \n"
    #endif // #if defined(configSUPPORT_ISR_STACK_CHECK) && configSUPPORT_ISR_STACK_CHECK

      " mov r0, #0         \n" /* Clear the bit that indicates the FPU is in use, see comment above. */
      " msr control, r0    \n"
      " cpsie i            \n" /* Globally enable interrupts. */
      " cpsie f            \n"
      " dsb                \n"
      " isb                \n"
      " svc 0              \n" /* System call to start first task. */
      " nop                \n"
   );
  #if defined(configSUPPORT_ISR_STACK_CHECK) && configSUPPORT_ISR_STACK_CHECK
    __asm volatile (
      "    .align 4        \n"
      "pxMSRstackLen: .word " EXPAND_AND_QUOTE(configISR_STACK_SIZE_WORDS) "\n"
    );
  #endif // #if defined(configUSE_ISR_STACK_CHECK) && configUSE_ISR_STACK_CHECK
}
/*-----------------------------------------------------------*/
#if defined(configSUPPORT_ISR_STACK_CHECK) && configSUPPORT_ISR_STACK_CHECK
  BaseType_t xUnusedISRstackWords( void ) {
    register uint32_t *pStackOrigin;
    int unusedStackWords = 0;
    __asm volatile(
      " ldr r0, =0xE000ED08  \n" /* Use the NVIC offset register to locate the initial stack. */
      " ldr r0, [r0]         \n"
      " ldr r0, [r0]         \n" /* r0 now has top of stack (actually, word above beginning of stack) */
      " mov %[result], r0    \n" : [result] "=r" (pStackOrigin) :: "r0"
     );
    uint32_t *pStackUseEnd = pStackOrigin - configISR_STACK_SIZE_WORDS; // start testing at stack limit
    for(int lim=configISR_STACK_SIZE_WORDS; lim; lim--) {
      if(*pStackUseEnd++) break; // break at first used word
      unusedStackWords++;
    }
    return unusedStackWords;
  }
#endif // #if defined(configUSE_ISR_STACK_CHECK) && configUSE_ISR_STACK_CHECK
/*-----------------------------------------------------------*/

With this I’m able to verify ISR stack use in our application.
Thanks as always for your excellent work,
Best Regards, Dave

PS: I’ll update heap_useNewlib.c to match the above shortly, on http://www.nadler.com/embedded/newlibAndFreeRTOS.html

PS: I’ve created a pull-request for this change, hope its OK!

rtel wrote on Friday, August 23, 2019:

Hi Dave - thanks for your valuable input as always.

The PIC32 (MIPS core) port does actually check for overflows in the ISR
stack as the stack is allocated by the kernel itself, although I was
thinking of changing that so it works like the Cortex-M ports whereby
the stack is not allocated by the kernel, but instead the stack that is
allocated by the linker configuration is re-used as the ISR stack - and
there is the issue with doing this in a generic way as the linker
scripts are all different and some don’t even define a stack size at all
(preferring to just let the stack and heap grow toward each other in
scary and complex to predict way).

So in nearly all Cortex-M cases, where the linker script is created or
provided by the tools, or set up in the IDE (IAR and the like) the ISR
stack size is not actually know.

dnadler wrote on Friday, August 23, 2019:

We’re agreed about avoiding the scary and unpredictable bit :wink:
Because linker scripts and symbols are wildly different across toolchains,
and there is often no symbol for stacksize that’s relevant (as in STM case),
I took the unpleasant approach of defining the ISR stack size in FreeRTOSconfig.
If the default is no ISR stack checking, the user will have to fill in the value
on enabling the check, and hopefully will match their LD values.
Can’t easily modify the LD as the vendor “wizards” overwrite changes…

One thing at a time, but I’m happy to help with the other Cortex M parts.
I think I have eval test boards for the assorted Cortex parts.
I only have GCC toolchain at present.

Let me know how I can help,
Best Regards, Dave

rtel wrote on Friday, August 23, 2019:

…because of the wide variation of compilers that have different linker script formats, and the variation in ways in which linker scripts are written, I think the only clean solution that would not impact backward compatibility would be to copy what was done for the RISC-V port whereby if you define a constant in FreeRTOSConfig.h that sets the size of the interrupt stack then the stack is defined by the kernel (just a static array) and it is possible to monitor the stack for overflows, otherwise a linker variable is used to re-use the stack that was used by main() which saves RAM but prevents the stack being monitored: https://www.freertos.org/Using-FreeRTOS-on-RISC-V.html#RISC-V-INTERRUPT_STACK - except in the Cortex-M case you don’t need the linker variable because the start of stack is obtained from the vector table (as it is in the Cortex-M ports today).

dnadler wrote on Friday, August 23, 2019:

Agreed, I think that’s what I’ve done (except static array isn’t applicable in Cortex case), OK?
Or did I miss something??

Linking related resources: https://github.com/aws/amazon-freertos/pull/1132