Over a million developers have joined DZone.

ARM Cortex-M Interrupts and FreeRTOS (Part 3)

In this mammoth post, see how FreeRTOS uses the ARM Cortex-M interrupt system so you can prioritize your interrupts to do what you need.

· IoT Zone

Access the survey results 'State of Industrial Internet Application Development' to learn about latest challenges, trends and opportunities with Industrial IoT, brought to you in partnership with GE Digital.

This is the third article of the ARM Cortex-M and how interrupts are used. In part 1, I discussed the Cortex-M interrupt system and in part 2, I showed nested interrupt examples. This part is about FreeRTOS and how it uses the Cortex-M interrupt system.

NXP KV58F ARM Cortex-M7

NXP KV58F ARM Cortex-M7

Outline

FreeRTOS and any other RTOS I’m aware of uses the microcontroller interrupt system. It is critical to understand the interrupt system of the microcontroller for the application. That’s why I wrote about this in the previous two parts. Using an RTOS like FreeRTOS means that I have to have an understanding of its usage of the interrupt system because otherwise, it can cause conflicts and wrong behavior of the application.

In this part, I describe how FreeRTOS uses the ARM Cortex-M (0/0+/3/4/7) interrupts:

  • Interrupts used by the RTOS.
  • Priorities of the RTOS interrupts.
  • Critical Section handling.
  • Partitioning of interrupt priorities/urgencies between the application and the RTOS.
  • Application startup and interrupts.

From the FreeRTOS perspective, Cortex-M0 and M0+ are the same, so I’m using M0 both for the M0+ and M0. The Cortex-M3/M4/M7 including their floating point variants are pretty much treated the same by FreeRTOS.

In this article, I’m using GNU assembly syntax to keep it simple. The McuOnEclipse FreeRTOS port covers GNU, IAR, and Keil too.

Interrupts Used by FreeRTOS

FreeRTOS on ARM Cortex-M uses the two or three interrupts, depending on the architecture and port used:

In FreeRTOS, a ‘port’ is the part of the Kernel which is microcontroller specific. This part deals with the low level hardware. Everything else in FreeRTOS is generic and written in C. The port part is written in a mix of C and assembly.

Interrupts Used by FreeRTOS

Interrupts Used by FreeRTOS (Source of tables: ARM Info Center)

Earlier ports for ARM Cortex-M0 did use SVCall too. Current port files provide that interrupt service routine for backward compatibility only.

  • SysTick: This one is used as the time base (timer interrupt) for the RTOS. Typical frequencies are 1 kHz or 100 Hz. In preemptive RTOS mode, that interrupt provides a way for the RTOS to preempt a running task and to pass control to another task.
  • PendSV (Pendable SerVice) is an interrupt request is used by the OS to force a context switch if no other interrupt is active.
  • SVCall (SuperVisor Call) is triggered by the SVC instruction and is used by the FreeRTOS to start the scheduler. This one is not used for M0.

SysTick is the default time base. Of course, any other timer interrupt can be used instead. For example, for low power applications, I’m using a special low power timer instead. And if enabling the FreeRTOS trace facility to measure task execution time, an extra timer might be needed.

When adding FreeRTOS to a ‘bare-metal’ (without RTOS) application, these three interrupts need to be routed to the FreeRTOS port.

Below is an example interrupt vector table for the NXP K20 (ARM Cortex-M4) with these three FreeRTOS interrupts highlighted (vPortSVCHandler, vPortPendSVHandler and vPortTickHandler):

If using CMSIS compliant interrupt names, then it would be SVC_Handler, PendSV_Handler and SysTick_Handler.

 __attribute__ ((section (".vectortable"))) const tVectorTable __vect_table = { /* Interrupt vector table */
    /* ISR name No. Address Pri Name Description */
    &__SP_INIT, /* 0x00 0x00000000 - ivINT_Initial_Stack_Pointer */
    {
        (tIsrFunc)&__thumb_startup, /* 0x01 0x00000004 - ivINT_Initial_Program_Counter */
        (tIsrFunc)&Cpu_INT_NMIInterrupt, /* 0x02 0x00000008 -2 ivINT_NMI */
        (tIsrFunc)&HF1_HardFaultHandler, /* 0x03 0x0000000C -1 ivINT_Hard_Fault */
        (tIsrFunc)&Cpu_ivINT_Mem_Manage_Fault, /* 0x04 0x00000010 - ivINT_Mem_Manage_Fault */
        (tIsrFunc)&Cpu_ivINT_Bus_Fault, /* 0x05 0x00000014 - ivINT_Bus_Fault */
        (tIsrFunc)&Cpu_ivINT_Usage_Fault, /* 0x06 0x00000018 - ivINT_Usage_Fault */
        (tIsrFunc)&Cpu_ivINT_Reserved7, /* 0x07 0x0000001C - ivINT_Reserved7 */
        (tIsrFunc)&Cpu_ivINT_Reserved8, /* 0x08 0x00000020 - ivINT_Reserved8 */
        (tIsrFunc)&Cpu_ivINT_Reserved9, /* 0x09 0x00000024 - ivINT_Reserved9 */
        (tIsrFunc)&Cpu_ivINT_Reserved10, /* 0x0A 0x00000028 - ivINT_Reserved10 */
        (tIsrFunc)&vPortSVCHandler, /* 0x0B 0x0000002C - ivINT_SVCall */
        (tIsrFunc)&Cpu_ivINT_DebugMonitor, /* 0x0C 0x00000030 - ivINT_DebugMonitor */
        (tIsrFunc)&Cpu_ivINT_Reserved13, /* 0x0D 0x00000034 - ivINT_Reserved13 */
        (tIsrFunc)&vPortPendSVHandler, /* 0x0E 0x00000038 - ivINT_PendableSrvReq */
        (tIsrFunc)&vPortTickHandler, /* 0x0F 0x0000003C - ivINT_SysTick */
        (tIsrFunc)&Cpu_ivINT_DMA0, /* 0x10 0x00000040 - ivINT_DMA0 */
        (tIsrFunc)&Cpu_ivINT_DMA1, /* 0x11 0x00000044 - ivINT_DMA1 */
        (tIsrFunc)&Cpu_ivINT_DMA2, /* 0x12 0x00000048 - ivINT_DMA2 */
        (tIsrFunc)&Cpu_ivINT_DMA3, /* 0x13 0x0000004C - ivINT_DMA3 */
        (tIsrFunc)&Cpu_ivINT_DMA_Error, /* 0x14 0x00000050 - ivINT_DMA_Error */
        (tIsrFunc)&Cpu_ivINT_Reserved21, /* 0x15 0x00000054 - ivINT_Reserved21 */
        (tIsrFunc)&Cpu_ivINT_FTFL, /* 0x16 0x00000058 - ivINT_FTFL */
        (tIsrFunc)&Cpu_ivINT_Read_Collision, /* 0x17 0x0000005C - ivINT_Read_Collision */
        (tIsrFunc)&Cpu_ivINT_LVD_LVW, /* 0x18 0x00000060 - ivINT_LVD_LVW */
        (tIsrFunc)&Cpu_ivINT_LLW, /* 0x19 0x00000064 - ivINT_LLW */
        (tIsrFunc)&Cpu_ivINT_Watchdog, /* 0x1A 0x00000068 - ivINT_Watchdog */
        (tIsrFunc)&Cpu_ivINT_I2C0, /* 0x1B 0x0000006C - ivINT_I2C0 */
        (tIsrFunc)&Cpu_ivINT_SPI0, /* 0x1C 0x00000070 - ivINT_SPI0 */
        (tIsrFunc)&Cpu_ivINT_I2S0_Tx, /* 0x1D 0x00000074 - ivINT_I2S0_Tx */
        (tIsrFunc)&Cpu_ivINT_I2S0_Rx, /* 0x1E 0x00000078 - ivINT_I2S0_Rx */
        (tIsrFunc)&Cpu_ivINT_UART0_LON, /* 0x1F 0x0000007C - ivINT_UART0_LON */
        (tIsrFunc)&ASerialLdd1_Interrupt, /* 0x20 0x00000080 8 ivINT_UART0_RX_TX */
        (tIsrFunc)&ASerialLdd1_Interrupt, /* 0x21 0x00000084 8 ivINT_UART0_ERR */
        (tIsrFunc)&Cpu_ivINT_UART1_RX_TX, /* 0x22 0x00000088 - ivINT_UART1_RX_TX */
        (tIsrFunc)&Cpu_ivINT_UART1_ERR, /* 0x23 0x0000008C - ivINT_UART1_ERR */
        (tIsrFunc)&Cpu_ivINT_UART2_RX_TX, /* 0x24 0x00000090 - ivINT_UART2_RX_TX */
        (tIsrFunc)&Cpu_ivINT_UART2_ERR, /* 0x25 0x00000094 - ivINT_UART2_ERR */
        (tIsrFunc)&Cpu_ivINT_ADC0, /* 0x26 0x00000098 - ivINT_ADC0 */
        (tIsrFunc)&Cpu_ivINT_CMP0, /* 0x27 0x0000009C - ivINT_CMP0 */
        (tIsrFunc)&Cpu_ivINT_CMP1, /* 0x28 0x000000A0 - ivINT_CMP1 */
        (tIsrFunc)&TU1_Interrupt, /* 0x29 0x000000A4 8 ivINT_FTM0 */
        (tIsrFunc)&Cpu_ivINT_FTM1, /* 0x2A 0x000000A8 - ivINT_FTM1 */
        (tIsrFunc)&Cpu_ivINT_CMT, /* 0x2B 0x000000AC - ivINT_CMT */
        (tIsrFunc)&Cpu_ivINT_RTC, /* 0x2C 0x000000B0 - ivINT_RTC */
        (tIsrFunc)&Cpu_ivINT_RTC_Seconds, /* 0x2D 0x000000B4 - ivINT_RTC_Seconds */
        (tIsrFunc)&Cpu_ivINT_PIT0, /* 0x2E 0x000000B8 - ivINT_PIT0 */
        (tIsrFunc)&Cpu_ivINT_PIT1, /* 0x2F 0x000000BC - ivINT_PIT1 */
        (tIsrFunc)&Cpu_ivINT_PIT2, /* 0x30 0x000000C0 - ivINT_PIT2 */
        (tIsrFunc)&Cpu_ivINT_PIT3, /* 0x31 0x000000C4 - ivINT_PIT3 */
        (tIsrFunc)&Cpu_ivINT_PDB0, /* 0x32 0x000000C8 - ivINT_PDB0 */
        (tIsrFunc)&USB_ISR, /* 0x33 0x000000CC 0 ivINT_USB0 */
        (tIsrFunc)&Cpu_ivINT_USBDCD, /* 0x34 0x000000D0 - ivINT_USBDCD */
        (tIsrFunc)&Cpu_ivINT_TSI0, /* 0x35 0x000000D4 - ivINT_TSI0 */
        (tIsrFunc)&Cpu_ivINT_MCG, /* 0x36 0x000000D8 - ivINT_MCG */
        (tIsrFunc)&Cpu_ivINT_LPTimer, /* 0x37 0x000000DC - ivINT_LPTimer */
        (tIsrFunc)&Cpu_ivINT_PORTA, /* 0x38 0x000000E0 - ivINT_PORTA */
        (tIsrFunc)&Cpu_ivINT_PORTB, /* 0x39 0x000000E4 - ivINT_PORTB */
        (tIsrFunc)&Cpu_ivINT_PORTC, /* 0x3A 0x000000E8 - ivINT_PORTC */
        (tIsrFunc)&Cpu_ivINT_PORTD, /* 0x3B 0x000000EC - ivINT_PORTD */
        (tIsrFunc)&Cpu_ivINT_PORTE, /* 0x3C 0x000000F0 - ivINT_PORTE */
        (tIsrFunc)&Cpu_ivINT_SWI /* 0x3D 0x000000F4 - ivINT_SWI */
    }
};


If using the NXP Processor Expert system, then the above interrupts get automatically added to vector table by the FreeRTOS component. If using an example from FreeRTOS.org, then the vector table already has these interrupts added.

SVCall

The M4/M7 ports of FreeRTOS are using a SuperVisor to start the first task. The SVC (SuperVisor Call) instruction is designed by ARM to access OS Kernel functions and device drivers. The SVCall exception is raised by the SVC assembly instruction which takes an additional argument/number, e.g.

SVC <number>


For M4/M7, FreeRTOS uses the SVC in a single place: when it starts the scheduler to run the first task. The function vPortStartFirstTask() gets called at the end when you do a call to the FreeRTOS vTaskStartScheduler():

void vPortStartFirstTask(void) {
    #if configCPU_FAMILY_IS_ARM_M4_M7(configCPU_FAMILY) /* Cortex M4/M7 */
    __asm volatile (
    " ldr r0, =0xE000ED08 \n" /* Use the NVIC offset register to locate the stack. */
    " ldr r0, [r0] \n" /* load address of vector table */
    " ldr r0, [r0] \n" /* load first entry of vector table which is the reset stack pointer */
    " msr msp, r0 \n" /* Set the msp back to the start of the stack. */
    " cpsie i \n" /* Globally enable interrupts. */
    " svc 0 \n" /* System call to start first task. */
    " nop \n"
    );

    #elif configCPU_FAMILY_IS_ARM_M0(configCPU_FAMILY) /* Cortex M0+ */
    /* With the latest FreeRTOS, the port for M0+ does not use the SVC instruction
    * and does not need vPortSVCHandler() any more.
    */
    /* The MSP stack is not reset as, unlike on M3/4 parts, there is no vector
    table offset register that can be used to locate the initial stack value.
    Not all M0 parts have the application vector table at address 0. */
    __asm volatile(
    " ldr r2, pxCurrentTCBConst2 \n" /* Obtain location of pxCurrentTCB. */
    " ldr r3, [r2] \n"
    " ldr r0, [r3] \n" /* The first item in pxCurrentTCB is the task top of stack. */
    " add r0, #32 \n" /* Discard everything up to r0. */
    " msr psp, r0 \n" /* This is now the new top of stack to use in the task. */
    " movs r0, #2 \n" /* Switch to the psp stack. */
    " msr CONTROL, r0 \n"
    " isb \n"
    " pop {r0-r5} \n" /* Pop the registers that are saved automatically. */
    " mov lr, r5 \n" /* lr is now in r5. */
    " pop {r3} \n" /* Return address is now in r3. */
    " pop {r2} \n" /* Pop and discard XPSR. */
    " cpsie i \n" /* The first task has its context and interrupts can be enabled. */
    " bx r3 \n" /* Finally, jump to the user defined task code. */
    " \n"
    " .align 4 \n"
    "pxCurrentTCBConst2: .word pxCurrentTCB"
    );
    #endif
}


SVC uses an argument which could be checked in the SVCall interrupt handler. Because FreeRTOS only uses the zero, no handling is needed. Below is an implementation of the SVCHandler:

Unlike the ‘normal’ FreeRTOS ports, the McuOnEcliopse FreeRTOS port covers multiple ARM architectures in a single port. For this the configCPU_FAMILY configuration macro is used. This makes sense as most of the port is the same for M0/M4/M7.

__attribute__ ((naked)) void vPortSVCHandler(void) {
    #if configCPU_FAMILY_IS_ARM_M4_M7(configCPU_FAMILY)  /* Cortex M4 or M7 */
    __asm volatile (
        " ldr r3, pxCurrentTCBConst2 \n" /* Restore the context. */
        " ldr r1, [r3]               \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
        " ldr r0, [r1]               \n" /* The first item in pxCurrentTCB is the task top of stack. */
        /* pop the core registers */
    #if configCPU_FAMILY_IS_ARM_FPU(configCPU_FAMILY)
        " ldmia r0!, {r4-r11, r14}   \n"
    #else
        " ldmia r0!, {r4-r11}        \n"
    #endif
        " msr psp, r0                \n"
        " mov r0, #0                 \n"
        " msr basepri, r0            \n"
    #if configCPU_FAMILY_IS_ARM_FPU(configCPU_FAMILY)
    #else
        " orr r14, r14, #13          \n"
    #endif
        " bx r14                     \n"
        "                            \n"
        " .align 2                   \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB \n"
    );
    #elif configCPU_FAMILY_IS_ARM_M0(configCPU_FAMILY) /* Cortex M0+ */
        /* This function is no longer used, but retained for backward
        compatibility. */
    #endif
}


Don’t be worried about the (GNU) ARM assembly code above: Basically what it does is perform a context switch — jumping from the vPortSVCHandler() interrupt handler/context to the task described by the pxCurrentTCB.

pxCurrentTCB points to an RTOS structure which contains a descriptor of the task (TCB stands for Task Control Block).

MSP and PSP

You might notice that the above handler sets the PSP (Process Stack Pointer):

"msr psp, r0 \n" /* Remember the new top of stack for the task. */

The Cortex-M has two different stack pointers:

  • MSP (Main Stack Pointer) is used during startup and during main(). Interrupts are using this stack.
  • PSP (Process Stack Pointer) is used by the process/tasks.
ARM Cortex MSP and PSP Registers

ARM Cortex MSP and PSP Registers

The good thing with this is that the task stacks only need to count for the task stack/variables, and not include the stack needed for the interrupts. On the other hand, it means that the MSP stack needs to hold all the (nested) interrupt stacks.

In the above port for Cortex-M4/M7, the MSP stack pointer is reset to the reset vector stack pointer: This gives some extra stack bytes for the interrupt service routines. Because not all Cortex-M0 have the vector table at address zero and there is no dedicated register to locate the initial stack pointer, this is not used for the Cortex-M0 port.

To my knowledge, all NXP Cortex-M0+ parts do have the default vector table at address 0x0000’0000. It is true that the NVIC for Cortex-M0 does not implement a VTOR (Vector Table Offset Register) at 0xE000ED08. However, all the NXP Cortex-M0+ parts I’m aware of have it implemented. Not as part of NVIC, but as part of the SystemControl. So for the NXP Cortex-M0+, the same approach as for M4/M7 with VTOR and SVC could be used.

VTOR on NXP Kinetis L Family (ARM Cortex-M0+)

VTOR on NXP Kinetis L Family (ARM Cortex-M0+)

PendSV

The Pendable Service interrupt is used by the RTOS to perform a context switch. Such a context switch can be originated by the RTOS or by the application with a ‘yield.’

The vPortPendSVHandler() It is similar to the vPortSVCHandler(). But it does not switch from the MSP to the PSP. It performs a task context switch between different PSP values. Additionally, it calls the FreeRTOS vTaskSwitchContext(), which selects the highest (RTOS!) priority ready task:

Don’t get confused by the RTOS priorities. FreeRTOS uses a system where numerically lower values mean lower task priorities. Zero is the lowest task priority in FreeRTOS. Don’t get this confused with the ARM Cortex-M interrupt priority levels where zero is the *highest* urgency!

__attribute__ ((naked)) void vPortPendSVHandler(void) {
    #if configCPU_FAMILY_IS_ARM_M4_M7(configCPU_FAMILY) /* Cortex M4 or M7*/
    __asm volatile (
    " mrs r0, psp \n"
    " ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */
    " ldr r2, [r3] \n"
    #if configCPU_FAMILY_IS_ARM_FPU(configCPU_FAMILY)
    " tst r14, #0x10 \n" /* Is the task using the FPU context? If so, push high vfp registers. */
    " it eq \n"
    " vstmdbeq r0!, {s16-s31} \n"

    " stmdb r0!, {r4-r11, r14} \n" /* save remaining core registers */
    #else
    " stmdb r0!, {r4-r11} \n" /* Save the core registers. */
    #endif
    " str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */
    " stmdb sp!, {r3, r14} \n"
    " mov r0, %0 \n"
    " msr basepri, r0 \n"
    " bl vTaskSwitchContext \n"
    " mov r0, #0 \n"
    " msr basepri, r0 \n"
    " ldmia sp!, {r3, r14} \n"
    " ldr r1, [r3] \n" /* The first item in pxCurrentTCB is the task top of stack. */
    " ldr r0, [r1] \n"
    #if configCPU_FAMILY_IS_ARM_FPU(configCPU_FAMILY)
    " ldmia r0!, {r4-r11, r14} \n" /* Pop the core registers */
    " tst r14, #0x10 \n" /* Is the task using the FPU context? If so, pop the high vfp registers too. */
    " it eq \n"
    " vldmiaeq r0!, {s16-s31} \n"
    #else
    " ldmia r0!, {r4-r11} \n" /* Pop the core registers. */
    #endif
    " msr psp, r0 \n"
    " bx r14 \n"
    " \n"
    " .align 2 \n"
    "pxCurrentTCBConst: .word pxCurrentTCB \n"
    ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
    );
    #else /* Cortex M0+ */
    __asm volatile (
    " mrs r0, psp \n"
    " \n"
    " ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */
       " ldr r2, [r3] \n"
    " \n"
    " sub r0, r0, #32 \n" /* Make space for the remaining low registers. */
    " str r0, [r2] \n" /* Save the new top of stack. */
    " stmia r0!, {r4-r7} \n" /* Store the low registers that are not saved automatically. */
    " mov r4, r8 \n" /* Store the high registers. */
    " mov r5, r9 \n"
    " mov r6, r10 \n"
    " mov r7, r11 \n"
    " stmia r0!, {r4-r7} \n"
    " \n"
    " push {r3, r14} \n"
    " cpsid i \n"
    " bl vTaskSwitchContext \n"
    " cpsie i \n"
    " pop {r2, r3} \n" /* lr goes in r3. r2 now holds tcb pointer. */
    " \n"
    " ldr r1, [r2] \n"
    " ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */
    " add r0, r0, #16 \n" /* Move to the high registers. */
    " ldmia r0!, {r4-r7} \n" /* Pop the high registers. */
    " mov r8, r4 \n"
    " mov r9, r5 \n"
    " mov r10, r6 \n"
    " mov r11, r7 \n"
    " \n"
    " msr psp, r0 \n" /* Remember the new top of stack for the task. */
    " \n"
    " sub r0, r0, #32 \n" /* Go back for the low registers that are not automatically restored. */
    " ldmia r0!, {r4-r7} \n" /* Pop low registers. */
    " \n"
    " bx r3 \n"
    " \n"
    ".align 2 \n"
    "pxCurrentTCBConst: .word pxCurrentTCB"
    );
   #endif
}

Task Yielding

As mentioned above, a task context switch can be requested by a task. For this taskYIELD() is used. This is actually a macro:

/**
 * task. h
 *
 * Macro for forcing a context switch.
 *
 * \defgroup taskYIELD taskYIELD
 * \ingroup SchedulerControl
 */
#define taskYIELD() portYIELD()

Which is yet another macro:

#define portYIELD() vPortYieldFromISR()

Which is implemented as:

void vPortYieldFromISR(void) {
    /* Set a PendSV to request a context switch. */
    *(portNVIC_INT_CTRL) = portNVIC_PENDSVSET_BIT;
    /* Barriers are normally not required but do ensure the code is completely
    within the specified behavior for the architecture. */
    __asm volatile("dsb");
    __asm volatile("isb");
}

It sets the portNVIC_PENDSVSET_BIT to trigger the PendSV interrupt in the first statement of the function.

The above implementation uses data and instruction barrier instructions. The Cortex-M has several special instructions to flush the internal pipelines/caches/bus registers. Check the ARM Infocenter for more details about the need for barrier instructions.

SysTick

The SysTick interrupt is a 24-bit timer designed to be used by the RTOS as time base. Typical time bases are 10ms or 1ms. Basically, the tick interrupt preempts any running task and passes control to the scheduler so it can increment its internal tick counter and perform a context switch as needed. Below is a (simplified) version of the tick handler interrupt routine:

void vPortTickHandler(void) {
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known. */
    portDISABLE_INTERRUPTS();   /* disable interrupts */
    if (xTaskIncrementTick()!=pdFALSE) { /* increment tick count */
        traceISR_EXIT_TO_SCHEDULER();
        taskYIELD();
    }
    portENABLE_INTERRUPTS(); /* re-enable interrupts */
}

As a side note: In FreeRTOS non-preemptive (cooperative, configUSE_PREEMPTION set to zero) mode the call to xTaskIncrementTick() will always return pdTRUE, so the tick interrupt will not cause a context switch.

Notice the pair of portDISABLE_INTERRUPTS() and portENABLE_INTERRUPTS(): some ports will not completely turn off interrupts, more about later.

configKERNEL_INTERRUPT_PRIORITY

You might have noticed that comment inside vPortTickHandler() from above:

/* The SysTick runs at the lowest interrupt priority, ... */

In FreeRTOS, the kernel and its interrupts are running with the lowest interrupt priority/urgency.

FreeRTOS uses the macro configKERNEL_INTERRUPT_PRIORITY in FreeRTOSConfig.h for the interrupts used by the kernel port layer. Below is an implementation for a system with 4 interrupt priority bits.

#define configPRIO_BITS 4 /* 4 bits/16 priority levels on ARM Cortex M4 (Kinetis K Family) */
/* The lowest interrupt priority that can be used in a call to a "set priority" function. */
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

Note that configKERNEL_INTERRUPT_PRIORITY is in the form the NVIC expects it: already *shifted* to the left.

As noted above, the important point to note is that configKERNEL_INTERRUPT_PRIORITY is set to the *lowest* urgency interrupt level in the system. So this means the interrupts of the kernel (SysTick and context switches) run with the lowest possible urgency. This makes sense as the kernel should not block or interrupt the other interrupts in the system. The other reason is that this approach keeps the kernel simple, as it does not have to deal with nested interrupts ‘below’ the urgency level of the kernel.

PendSV and SysTick interrupt priorities are configured by the FreeRTOS at scheduler start:

/* Interrupt priorities used by the kernel port layer itself. These are generic
 to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

#define portNVIC_SYSPRI3 ((volatile unsigned long*)0xe000ed20) /* system handler priority register 3 (SHPR3), used for SysTick and PendSV priority, http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0662b/CIAGECDD.html */
#define portNVIC_SYSTICK_PRI (((unsigned long)configKERNEL_INTERRUPT_PRIORITY)<<24) /* priority of SysTick interrupt (in portNVIC_SYSPRI3) */
#define portNVIC_PENDSV_PRI (((unsigned long)configKERNEL_INTERRUPT_PRIORITY)<<16) /* priority of PendableService interrupt (in portNVIC_SYSPRI3) */

*(portNVIC_SYSPRI3) |= portNVIC_PENDSV_PRI; /* set priority of PendSV interrupt */
*(portNVIC_SYSPRI3) |= portNVIC_SYSTICK_PRI; /* set priority of SysTick interrupt */

The SVCall priority (only used on M4/M7) is *not* touched. Out of reset, this priority is zero (highest urgency for the ARM core). The SVCall interrupt is only used once at the startup of the RTOS (see above).

The reason why the SVCall priority is still at the highest urgency (0 value) is when executing the SVC instruction, the SVCall interrupt shall not be masked (disabled). If SVCall would be at the same priority of the SysTick, then a SysTick might happen earlier, and then using SVC would cause a hard fault. Leaving it at 0 (highest interrupt urgency) ensures that it is not masked by a running PendSV or SysTick interrupt, and that it is not masked by the BASEPRI register.

Calling RTOS API Functions from ISR

It is not uncommon to call RTOS functions (e.g. to set/clear a semaphore) from an interrupt service routine (ISR). There is one very important rule with FreeRTOS:

Only RTOS API functions ending with “FromISR” are allowed to be called from an interrupt service routine.

The “FromISR” functions are ‘interrupt safe, for example xSemaphoreGiveFromISR().

xSemaphoreGiveFromISR
    (
    SemaphoreHandle_t xSemaphore,
    signed BaseType_t *pxHigherPriorityTaskWoken
    )

If pxHigherPriorityTaskWoken returns pdTRUE, then the API call caused a higher priority task to be unlocked. I that case the interrupt should call a yield at the end of the service routine to perform a context switch.

The following example implements a timer interrupt which gives a semaphore:

/* Timer ISR */
void vTimerISR(void) {
    signed BaseType_t xHigherPriorityTaskWoken;

    xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    /* If xHigherPriorityTaskWoken was set to true you we should yield. */
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

Kernel Critical Sections

FreeRTOS stores internally information, things like the list of tasks and other data used by the kernel. Because this data is accessed both by tasks through FreeRTOS API calls (e.g. accessing a semaphore) and by the kernel itself (trigged through a SysTick timer interrupt), access to this data needs to be reentrant and protected with a critical section.

Remember the simple critical section we saw earlier for the tick handler:

void vPortTickHandler(void) {
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known. */
    portDISABLE_INTERRUPTS();   /* disable interrupts */
    if (xTaskIncrementTick()!=pdFALSE) { /* increment tick count */
        traceISR_EXIT_TO_SCHEDULER();
        taskYIELD();
    }
    portENABLE_INTERRUPTS(); /* re-enable interrupts */
}

Inside xTaskIncrementTick() the kernel is running: checking if there is another task to schedule in preemptive mode. In cooperative mode it will return pdFALSE

So now you could think that the RTOS is disabling all interrupts during its critical sections. This is actually true for some FreeRTOS ports (e.g. for ARM Cortex-M0/M0+). Disabling interrupts for critical sections increases interrupt latency and should be avoided as much as possible, and such a critical section should be as small as possible. While FreeRTOS makes every effort to keep such critical sections as small and fast as possible, they are certainly longer than a few CPU instructions. The good news is that for the Cortex-M3/M4/M7 ports, not all interrupts are disabled: FreeRTOS is taking advantage of the BASEPRI register (see Part 1).

They are behind yet another macro as below:

#define portDISABLE_INTERRUPTS() portSET_INTERRUPT_MASK()
#define portENABLE_INTERRUPTS()  portCLEAR_INTERRUPT_MASK()

The implementation depends on the Cortex-M core used:

#if configCPU_FAMILY_IS_ARM_M4_M7(configCPU_FAMILY) /* Cortex M4/M7 */
    /*
    * Set basepri to portMAX_SYSCALL_INTERRUPT_PRIORITY without effecting other
    * registers. r0 is clobbered.
    */
    #define portSET_INTERRUPT_MASK() \
     __asm volatile \
     ( \
     " mov r0, %0 \n" \
     " msr basepri, r0 \n" \
     : /* no output operands */ \
     :"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) /* input */\
     :"r0" /* clobber */ \
     )
    /*
    * Set basepri back to 0 without effective other registers.
    * r0 is clobbered.
    */
    #define portCLEAR_INTERRUPT_MASK() \
     __asm volatile \
     ( \
     " mov r0, #0 \n" \
     " msr basepri, r0 \n" \
     : /* no output */ \
     : /* no input */ \
     :"r0" /* clobber */ \
     )
    #elif configCPU_FAMILY_IS_ARM_M0(configCPU_FAMILY) /* Cortex-M0+ */
    #define portSET_INTERRUPT_MASK()   __asm volatile("cpsid i")
    #define portCLEAR_INTERRUPT_MASK() __asm volatile("cpsie i")
#endif

You might wonder why in above vPortTickHandler() it enables all interrupts with portENABLE_INTERRUPTS(), and why it does not need to store the current BASEPRI level? Because the tick interrupt has the lowest interrupt value, it only can run if all interrupts are enabled. So BASEPRI must be zero, so it does not make sense to save and restore a known value.

So on Cortex-M0, it globally disables all interrupts, while on M4/M7 FreeRTOS only disables the interrupts up to the

configMAX_SYSCALL_INTERRUPT_PRIORITY

level (more about this FreeRTOS configuration setting later). So using an ARM Cortex-M3/M4/M7 not only makes sense from the performance point of view: with the BASEPRI NVIC hardware it is possible that some interrupts are *not* blocked by the Kernel and their interrupt latency is not impacted by the RTOS. However, the range of interrupt levels need to be carefully selected, more about this later as well.

Task Critical Sections

So far we had a look a the port critical section handling for the tick interrupt. Another aspect is how the Kernel implements critical sections inside the RTOS API.

For example below is an example how FreeRTOS implements a critical section inside:

void* xQueueGetMutexHolder( QueueHandle_t xSemaphore )
{
    void *pxReturn;

    /* This function is called by xSemaphoreGetMutexHolder(), and should not
    be called directly.  Note:  This is a good way of determining if the
    calling task is the mutex holder, but not a good way of determining the
    identity of the mutex holder, as the holder may change between the
    following critical section exiting and the function returning. */
    taskENTER_CRITICAL();
    {
        if( ( ( Queue_t * ) xSemaphore )->uxQueueType == queueQUEUE_IS_MUTEX )
        {
            pxReturn = ( void * ) ( ( Queue_t * ) xSemaphore )->pxMutexHolder;
        }
        else
        {
            pxReturn = NULL;
        }
    }
    taskEXIT_CRITICAL();

    return pxReturn;
} /*lint !e818 xSemaphore cannot be a pointer to const because it is a typedef. */

As shown above, FreeRTOS uses the following:

taskENTER_CRITICAL();
/* critical section here */
taskEXIT_CRITICAL();

They are macros, and the implementation is part of the port for a microcontroller:

#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()

Below is the implementation for ARM Cortex-M (simplified):

void vPortEnterCritical(void) {
    /*
    * Disable interrupts before incrementing the count of critical section nesting.
    * The nesting count is maintained so we know when interrupts should be
    * re-enabled. Once interrupts are disabled the nesting count can be accessed
    * directly. Each task maintains its own nesting count.
    */
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
}
/*-----------------------------------------------------------*/
void vPortExitCritical(void) {
    /* Interrupts are disabled so we can access the nesting count directly. If the
    * nesting is found to be 0 (no nesting) then we are leaving the critical
    * section and interrupts can be re-enabled.
    */
    uxCriticalNesting--;
    if (uxCriticalNesting == 0) {
        portENABLE_INTERRUPTS();
    }
}

The two functions are using a nesting counter to allow nesting of critical sections. They are using

portDISABLE_INTERRUPTS();

and

portENABLE_INTERRUPTS();

to disable/enable the interrupts we have seen above. So here again: FreeRTOS is masking all interrupts on Cortex-M0, but leaves some interrupt levels enabled based on the configMAX_SYSCALL_INTERRUPT_PRIORITY.

configMAX_SYSCALL_INTERRUPT_PRIORITY

We already saw earlier the configMAX_SYSCALL_INTERRUPT_PRIORITY setting of FreeRTOS in FreeRTOSConfig.h. On Cortex-M3/M4/M7 it is used to mask interrupts using the BASEPRI register.

In FreeRTOSConfig.h it is present as a macro:

/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

Because the MAX_SYSCALL can be confusing, there is an alias configMAX_API_CALL_INTERRUPT_PRIORITY, which much better reflects up to which interrupt level the FreeRTOS API can be called from an interrupt. configMAX_API_CALL_INTERRUPT_PRIORITY is a new name for configMAX_SYSCALL_INTERRUPT_PRIORITY that is used by newer ports only. The two are equivalent.

The macro configMAX_SYSCALL_INTERRUPT_PRIORITY uses configPRIO_BITS (the number of priority bits available, see Part 1), and configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

It is used in the port to mask interrupts as below:

#define portSET_INTERRUPT_MASK() \
__asm volatile \
( \
" mov r0, %0 \n" \
" msr basepri, r0 \n" \
/* no output operands */ \
:"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) /* input */\
:"r0" /* clobber */ \
)
/*
* Set basepri back to 0 without effective other registers.
* r0 is clobbered.
*/
#define portCLEAR_INTERRUPT_MASK() \
__asm volatile \
( \
" mov r0, #0 \n" \
" msr basepri, r0 \n" \
: /* no output */ \
: /* no input */ \
:"r0" /* clobber */ \
)

Because the kernel only masks out interrupts up and equal to the configMAX_SYSCALL_INTERRUPT_PRIORITY numerical value, FreeRTOS API calls from interrupts can only be called from interrupts *numerically equal or lower* than configMAX_SYSCALL_INTERRUPT_PRIORITY (remember: ARM NVIC uses zero as the highest urgency level).

Let’s assume a system with 2 priority bits on a Cortex-M3/M4/M7:

#define configPRIO_BITS 2 /* prios: 0, 1, 2, and 3 */

The lowest priority is 3

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   3

So the FreeRTOS Kernel (PendSV and Systick) will run on that level which is 0xC0:

#define configKERNEL_INTERRUPT_PRIORITY    (configLIBRARY_LOWEST_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

Let’s set the ‘Max API Call Interrupt level’ to 2:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY     2

That means the kernel critical section (BASEPRI) is set to 0x80:

#define configMAX_SYSCALL_INTERRUPT_PRIORITY      (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

I configure the rest of my interrupts and get the following NVIC priorities:

  • USB: 0 (0x00).
  • I2C: 1 (0x40).
  • SCI: 2  (0x80).
  • BASEPRI: 0x80.
  • SysTick: 3 (0xC0).
  • PendSV: 3 (0xC0).
FreeRTOS and Interrupts

FreeRTOS and Interrupts

The FreeRTOS kernel through SysTick and PendSV runs on the lowest interrupt urgency. Whenever the FreeRTOS is creating a critical section, it writes 0x80 to the BASEPRI mask register, effectively blocking any interrupts with NVIC value 0x80 or greater.

Kernel and ISRs calling RTOS API

Any interrupt (SCI in the above case) with a priority value equal or higher than BASEPRI can call FromISR() FreeRTOS API functions. The ability of an interrupt service routine to use FreeRTOS API functions comes with the price that the RTOS is blocking that interrupt in the kernel critical sections. All the interrupts with NVIC priority values lower (higher urgency) than the BASEPRI value are not blocked by the RTOS.

Extract from http://www.freertos.org/a00110.html#kernel_priority, which applies to the Cortex-M3/M4/M7:

“configKERNEL_INTERRUPT_PRIORITY sets the interrupt priority used by the RTOS kernel itself. configMAX_SYSCALL_INTERRUPT_PRIORITY sets the highest interrupt priority from which interrupt safe FreeRTOS API functions can be called.

A full interrupt nesting model is achieved by setting configMAX_SYSCALL_INTERRUPT_PRIORITY above (that is, at a higher priority level) than configKERNEL_INTERRUPT_PRIORITY. This means the FreeRTOS kernel does not completely disable interrupts, even inside critical sections. Further, this is achieved without the disadvantages of a segmented kernel architecture. Note however, certain microcontroller architectures will (in hardware) disable interrupts when a new interrupt is accepted – meaning interrupts are unavoidably disabled for the short period between the hardware accepting the interrupt, and the FreeRTOS code re-enabling interrupts.

Interrupts that do not call API functions can execute at priorities above configMAX_SYSCALL_INTERRUPT_PRIORITY and therefore never be delayed by the RTOS kernel execution.”

Of course not every interrupt service routine needs FreeRTOS API functions: They can use an NVIC priority numerically lower than the BASEPRI value and are not affected by the RTOS critical section. However, if an interrupt service routine *is* using RTOS API calls (e.g. dealing with a semaphore or using FreeRTOS queues or events), then the NVIC priority of it has to be numerically equal or higher (lower urgency) than the BASEPRI value.

It is highly recommended to keep the configASSERT() in FreeRTOS enabled, as these checks are catching many wrong settings. Failure to configure the Cortex-M priorities in alignment how the RTOS is using it can cause strange and sporadic failure of the system!

What about systems without BASEPRI NVIC hardware support as the Cortex-M0+?  http://www.freertos.org/a00110.html#kernel_priority says:

“For ports that only implement configKERNEL_INTERRUPT_PRIORITY
 configKERNEL_INTERRUPT_PRIORITY sets the interrupt priority used by the RTOS kernel itself. Interrupts that call API functions must also execute at this priority. Interrupts that do not call API functions can execute at higher priorities and therefore never have their execution delayed by the RTOS kernel activity (within the limits of the hardware itself).”

Here I think this is not correct for the Cortex-M0+ or maybe misleading: As the port for Cortex-M0 blocks all interrupts in the RTOS critical section, they are indeed delayed by the RTOS kernel activity. On the other side, as they are all blocked and affected, they can use any priority. But any ISR using FreeRTOS API calls has to use FromISR() API variants.

System Startup and Interrupts

In FreeRTOS, the scheduler gets started with a call to vTaskStartScheduler:

void vTaskStartScheduler(void);

By default on ARM Cortex-M, interrupts are disabled out of reset. Typically they get enabled by the startup code on some systems. In most system they get enabled in or after main().

With using FreeRTOS, I recommend that interrupts are disabled during system initialization. FreeRTOS will enable interrupts inside vTaskStartScheduler().

It is safe to call FreeRTOS API functions before the scheduler is started, see point 3 in http://www.freertos.org/FAQHelp.html:

“If a FreeRTOS API function is called before the scheduler has been started then interrupts will deliberately be left disabled and not re-enable again until the first task starts to execute. This is done to protect the system from crashes caused by interrupts attempting to use FreeRTOS API functions during system initialization, before the scheduler has been started, and while the scheduler may be in an inconsistent state.

Do not alter the microcontroller interrupt enable bits or priority flags using any method other than calls to taskENTER_CRITICAL() and taskEXIT_CRITICAL(). These macros keep a count of their call nesting depth to ensure interrupts become enabled again only when the call nesting has unwound completely to zero. Be aware that some library functions may themselves enable and disable interrupts.”

FreeRTOS and Subpriorities?

I have not (yet) used FreeRTOS in a system with subpriorites. http://www.freertos.org/RTOS-Cortex-M3-M4.html says:

“It is recommended to assign all the priority bits to be preempt priority bits, leaving no priority bits as subpriority bits. Any other configuration complicates the otherwise direct relationship between the configMAX_SYSCALL_INTERRUPT_PRIORITY setting and the priority assigned to individual peripheral interrupts. “

I have made some experiments, and with careful settings of priority values and macros with a few tweaks it seems to be possible to run FreeRTOS in a system with subpriorities. I have not made extensive tests, and currently, I don’t have the need to run it with subpriorities. For sure it is (not yet?) supported out of the box, but certainly doable. And I’m sure if there is demand for it, it can get added to FreeRTOS.

Summary

What I started writing on the topic of Cortex-M interrupts and FreeRTOS, I thought I could cover it in one article. Now it ended up in three, with part three much larger than I thought. While collecting all the information and reading many other articles, I learned a lot new things and even ended up tweaking the current McuOnEclipse Processor Expert port for Cortex-M0+ (I was still using SVCall, now I removed the need for it).

The key takeaways are:

  • The FreeRTOS kernel uses two-three interrupts, depending on the core: SysTick is used as the time base, PendSV for context switches, and SVCall on Cortex-M3/4/7 to start the scheduler.
  • SysTick and PendSV are configured for lowest urgency: The RTOS runs with the lowest urgency level.
  • FreeRTOS task priorities start with 0 as the lowest urgent RTOS task priority, while the ARM NVIC is using zero as the highest urgent interrupt priority.
  • The tasks are using the PSP (Process Stack Pointer), while the interrupts are using the MSP (Main Stack Pointer). On the Cortex-M3/4/7 port, the MSP is set back to the reset stack pointer at scheduler start.
  • The FreeRTOS kernel needs to use a critical section to protect its internal data structures.
  • On Cortex-M0, the critical section of the Kernel masks all interrupts.
  • On Cortex-M3/4/7, the critical section of the kernel does not mask all interrupts: only masks interrupts up and equal to configMAX_SYSCALL_INTERRUPT_PRIORITY using the NVIC BASEPRI. Interrupts with NVIC values lower than BASEPRI (higher urgency) are not affected by the Kernel.
  • Only interrupts with NVIC numerical values greater-equal than configMAX_SYSCALL_INTERRUPT_PRIORITY/BASEPRI are allowed to use the RTOS API.
  • Interrupt service routines calling the RTOS API have to use the interrupt safe FromISR() variants.
  • Subpriorities would be possible, but not supported  out-of-the-box in FreeRTOS V9.0.0.

Let me know if I have missed anything or if something needs further clarification. I hope with this it makes it easier for you to use FreeRTOS with the ARM Cortex-M interrupt system.

Happy FreeRTOSing!

The IoT Zone is brought to you in partnership with GE Digital.  Discover how IoT developers are using Predix to disrupt traditional industrial development models.

Topics:
functions ,freertos ,port ,service ,interrupt ,mcuoneclipse ,api ,vector ,table

Published at DZone with permission of Erich Styger, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}