QP-nano  6.9.0
Real-Time Embedded Framework
ARM Cortex-M

Preemptive QK Kernel

ARM7/ARM9

This section describes the QP-nano™ port to the ARM Cortex-M processor family (Cortex M0/M0+/M3/M4). Three main implementation options are covered: the cooperative, priority-based QV-nano kernel and the preemptive run-to-completion QK-nano kernel. Additionally, the use of the VFP (floating point coprocessor) in the M4F processor is explained as well. This document assumes QP-nano version 5.9.x or higher.

Note
To focus the discussion, this section references the GNU-ARM toolchain. However, the general implementation strategy applies equally to all toolchains for ARM Cortex-M, such as ARM-KEIL and IAR EWARM, which are all provided as well. The QP code downloads contain also examples for other boards, such as STM32 Nucleo, NXP mbed-1768, SilLabs Gecko and others.

Directories and Files

The QP ports to ARM Cortex-M are available in the standard QP distribution. Specifically, the ARM Cortex-M ports are placed in the following directories:

  • /pors/arm-cm/
    • qv/ — QV ports
      • arm/ — ARM-KEIL toolchain
      • gnu/ — GNU-ARM toolchain
        • qfn_port.h — QF-nano port header
        • qvn_port.c — QV-nano port implementation
      • iar/ — IAR-EWARM toolchain
    • qk/ — QK-nano ports
      • arm/ — ARM-KEIL toolchain
      • armclang/ — ARM-KEIL toolchain CLANG/LLVM compiler
      • gnu/ — GNU-ARM toolchain
      • iar/ — IAR-EWARM toolchain

Interrupts in the QP-nano Ports to ARM Cortex-M

The QP-nano framework, like any real-time kernel, needs to disable interrupts in order to access critical sections of code and re-enable interrupts when done. This section describes the general policy that in the ARM Cortex-M ports of all built-in real time kernels in QP, such as QV-nano and QK-nano.

"Kernel-Aware" and "Kernel-Unaware" Interrupts

The QP ports to ARM Cortex-M3/M4 never completely disables interrupts, even inside the critical sections. On Cortex-M3/M4 (ARMv7-M architecture), the QP port disables interrupts selectively using the BASEPRI register. This policy divides interrupts into "kernel-unaware" interrupts, which are never disabled, and "kernel-aware" interrupts, which are disabled in the QP critical sections.

Note
The BASEPRI register is not implemented in the ARMv6-M architecture (Cortex-M0/M0+), so Cortex-M0/M0+ need to use the PRIMASK register to disable interrupts globally. In other words, in Cortex-M0/M0+ ports, all interrupts are "kernel-aware".
Attention
Only "kernel-aware" interrupts are allowed to call QP services. "Kernel-unaware" interrupts are not allowed to call any QP services and they can communicate with QP only by triggering a "kernel-aware" interrupt (which can post or publish events).

As illustrated in the figures below, the number of interrupt priority bits actually available is implementation dependent, meaning that the various ARM Cortex-M silicon vendors can provide different number of priority bits, varying from just 3 bits (which is the minimum for ARMv7-M architecture) up to 8 bits. For example, the TI Tiva-C microcontrollers implement only 3 priority bits (see figure below).

Kernel-aware and Kernel-unaware interrupts with 3 priority bits

On the other hand, the STM32 MCUs implement 4 priority bits (see figure below). The CMSIS standard provides the macro __NVIC_PRIO_BITS, which specifies the number of NVIC priority bits defined in a given ARM Cortex-M implementation.

Kernel-aware and Kernel-unaware interrupts with 4 priority bits

Another important fact to note is that the ARM Cortex-M core stores the interrupt priority values in the most significant bits of its eight bit interrupt priority registers inside the NVIC (Nested Vectored Interrupt Controller). For example, if an implementation of a ARM Cortex-M microcontroller only implements three priority bits, then these three bits are shifted to occupy bits five, six and seven respectively. The unimplemented bits can be written as zero or one and always read as zero.

And finally, the NVIC uses an inverted priority numbering scheme for interrupts, in which priority zero (0) is the highest possible priority (highest urgency) and larger priority numbers denote actually lower-priority interrupts. So for example, interrupt of priority 2 can preempt an interrupt with priority 3, but interrupt of priority 3 cannot preempt interrupt of priority 3. The default value of priority of all interrupts out of reset is zero (0).

Note
Starting with QP 5.9.x, the QF_init() call sets interrupt priority of all IRQs to the "kernel aware" value QF_BASEPRI. Still, it is highly recommended to set the priority of all interrupts used by an application explicitly.
Attention
Some 3rd-party libraries (e.g., STM32Cube) change the interrupt priorities and sometimes priority grouping internally and unexpectedly, so care must be taken to change the priorities back to the appropriate values right before running the application.

The CMSIS provides the function NVIC_SetPriority() which you should use to set priority of every interrupt.

Note
The priority scheme passed to NVIC_SetPriority() is different again than the values stored in the NVIC registers, as shown in the figures above as "CMSIS priorities"

Assigning Interrupt Priorities

The example projects included in the QP distribution the recommended way of assigning interrupt priorities in your applications. The initialization consist of two steps: (1) you enumerate the "kernel-unaware" and "kernel-aware" interrupt priorities, and (2) you assign the priorities by calling the NVIC_SetPriority() CMSIS function. The following snippet of code illustrates these steps with the explanation section following immediately after the code.

Listing: Assigning the interrupt priorities (see file bsp.c in the example projects)

[1] enum KernelUnawareISRs { /* see NOTE0 */
/* ... */
[2] MAX_KERNEL_UNAWARE_CMSIS_PRI /* keep always last */
};
/* "kernel-unaware" interrupts can't overlap "kernel-aware" interrupts */
[3] Q_ASSERT_COMPILE(MAX_KERNEL_UNAWARE_CMSIS_PRI <= QF_AWARE_ISR_CMSIS_PRI);
[4] enum KernelAwareISRs {
[5] GPIOPORTA_PRI = QF_AWARE_ISR_CMSIS_PRI, /* see NOTE00 */
SYSTICK_PRIO,
/* ... */
[6] MAX_KERNEL_AWARE_CMSIS_PRI /* keep always last */
};
/* "kernel-aware" interrupts should not overlap the PendSV priority */
[7] Q_ASSERT_COMPILE(MAX_KERNEL_AWARE_CMSIS_PRI <= (0xFF>>(8-__NVIC_PRIO_BITS)));
~ ~ ~
[8] void QF_onStartup(void) {
/* set up the SysTick timer to fire at BSP_TICKS_PER_SEC rate */
SysTick_Config(ROM_SysCtlClockGet() / BSP_TICKS_PER_SEC);
/* assing all priority bits for preemption-prio. and none to sub-prio. */
[9] NVIC_SetPriorityGrouping(0U);
/* set priorities of ALL ISRs used in the system, see NOTE00
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* Assign a priority to EVERY ISR explicitly by calling NVIC_SetPriority().
* DO NOT LEAVE THE ISR PRIORITIES AT THE DEFAULT VALUE!
*/
[10] NVIC_SetPriority(SysTick_IRQn, SYSTICK_PRIO);
[11] NVIC_SetPriority(GPIOPortA_IRQn, GPIOPORTA_PRIO);
~ ~ ~
/* enable IRQs... */
[12] NVIC_EnableIRQ(GPIOPortA_IRQn);
}
#define Q_ASSERT_COMPILE(test_)
Definition: qassert.h:339
void QF_onStartup(void)
  • 1 The enumeration KernelUnawareISRs lists the priority numbers for the "kernel-unaware" interrupts. These priorities start with zero (highest possible). The priorities are suitable as the argument for the NVC_SetPriority() CMSIS function.

    ‍NOTE: The NVIC allows you to assign the same priority level to multiple interrupts, so you can have more ISRs than priority levels running as "kernel-unaware" or "kernel-aware" interrupts.

  • 2 The last value in the enumeration MAX_KERNEL_UNAWARE_CMSIS_PRI keeps track of the maximum priority used for a "kernel-unaware" interrupt.
  • 3 The compile-time assertion ensures that the "kernel-unaware" interrupt priorities do not overlap the "kernel-aware" interrupts, which start at QF_AWARE_ISR_CMSIS_PRI.
  • 4 The enumeration KernelAwareISRs lists the priority numbers for the "kernel-aware" interrupts.
  • 5 The "kernel-aware" interrupt priorities start with the QF_AWARE_ISR_CMSIS_PRI offset, which is provided in the qf_port.h header file.
  • 6 The last value in the enumeration MAX_KERNEL_AWARE_CMSIS_PRI keeps track of the maximum priority used for a "kernel-aware" interrupt.
  • 7 The compile-time assertion ensures that the "kernel-aware" interrupt priorities do not overlap the lowest priority level reserved for the PendSV exception.
  • 8 The QF_onStartup() callback function is where you set up the interrupts.
  • 9 This call to the CMIS function NVIC_SetPriorityGrouping() assigns all the priority bits to be preempt priority bits, leaving no priority bits as subpriority bits to preserve the direct relationship between the interrupt priorities and the ISR preemption rules. This is the default configuration out of reset for the ARM Cortex-M3/M4 cores, but it can be changed by some vendor-supplied startup code. To avoid any surprises, the call to NVIC_SetPriorityGrouping(0U) is recommended.
  • 10-11 The interrupt priories fall all interrupts ("kernel-unaware" and "kernel-aware" alike) are set explicitly by calls to the CMSIS function NVIC_SetPriority().
  • 12 All used IRQ interrupts need to be explicitly enabled by calling the CMSIS function.

Interrupts and the FPU (Cortex-M4F)

The QP ports described in this section support also the ARM Cortex-M4F. Compared to all other members of the Cortex-M family, these cores includes the single precision variant of the ARMv7-M Floating-Point Unit (Fpv4-SP). The hardware FPU implementation adds an extra floating-point register bank consisting of S0-S31 and some other FPU registers. This FPU register set represents additional context that need to be preserved across interrupts and thread switching (e.g., in the preemptive QK-nano kernel).

The ARM VFP has a very interesting feature called lazy stacking [ARM-AN298]. This feature avoids an increase of interrupt latency by skipping the stacking of floating-point registers, if not required, that is:

  • if the interrupt handler does not use the FPU, or
  • if the interrupted program does not use the FPU.

If the interrupt handler has to use the FPU and the interrupted context has also previously used by the FPU, then the stacking of floating-point registers takes place at the point in the program where the interrupt handler first uses the FPU. The lazy stacking feature is programmable and by default it is turned ON.

Note
All built-in kernels in QP are designed to take advantage of the lazy stacking feature [ARM-AN298].

References

Cooperative QV-nano Kernel