The non-preemptive QV kernel executes active objects one at a time, with priority-based scheduling performed after run-to-completion (RTC) processing of each event. Due to naturally short duration of event processing in state machines, the simple QV kernel is often adequate for many real-time systems. (NOTE: Long RTC steps can frequently be broken into shorter pieces using the "Reminder" state pattern [Reminder])
Synopsis of the QV Port on ARM Cortex-M
The non-preemptive QV kernel works essentially as the traditional foreground-background system (a.k.a. "superloop") in that all active objects are executed in the main loop and interrupts always return to the point of preemption. To avoid race conditions between the main loop and the interrupts, QV briefly disables interrupts.
The qp_port.h Header File
The QF header file for the ARM Cortex-M port is located in /ports/arm-cm/qv/gnu/qp_port.h. This file specifies the interrupt disabling policy (QF critical section) as well as the configuration constants for QF (see Chapter 8 in [PSiCC2]).
The following listing shows the qp_port.h header file for ARM Cortex-M with the GNU-ARM toolchain. Other toolchains use slightly different conditional compilation macros to select the Cortex-M variants, but implement the same policies.
[1] The QF_MAX_ACTIVE specifies the maximum number of active object priorities in the application. You always need to provide this constant. Here, QF_MAX_ACTIVE is set to 32, but it can be increased up to the maximum limit of 63 active object priorities in the system.
[2] The macro QF_MAX_TICK_RATE specifies the maximum number of clock tick rates for QP/C++ time events. If you do not need to specify this limit, in which case the default of a single clock rate will be chosen.
[3] As described in the previous Section, the interrupt disabling policy for the ARMv6-M architecture (Cortex-M0/M0+) is different than the policy for the ARMv7-M. In GNU-ARM, the macro __ARM_ARCH is defined as 6 for the ARMv6-M architecture (Cortex-M0/M0+), and 7 for ARMv7-M (Cortex-M3/M4/M4F).
[4-5] For the ARMv6-M architecture, the interrupt disabling policy uses the PRIMASK register to disable interrupts globally. The QF_INT_DISABLE() macro resolves in this case to the inline assembly instruction "CPSD i", which sets the PRIMASK. The QF_INT_ENABLE() macro resolves to the inline assembly instruction "CPSE i", which clears the PRIMASK.
[6] The QF_CRIT_STAT is empty, meaning that the critical section uses the simple policy of "unconditional interrupt disabling".
[7] The QF_CRIT_ENRY() enters a critical section. Interrupts are disabled by setting the PRIMASK register.
[8] The QF_CRIT_EXIT() macro leaves the critical section. Interrupts are unconditionally re-enabled by clearing the PRIMASK register.
[9] For the ARMv6-M architecture, the QF_AWARE_ISR_CMSIS_PRI priority level is defined as zero, meaning that all interrupts are "kernel-aware", because all interrupts that the kernel disables have priorities.
[10] The QF_LOG2() macro is defined as a call to the function QF_qlog2() ("quick log-base-2 logarithm"). This function is coded in hand-optimized assembly, which always takes only 14 CPU cycles to execute (see also label [23]).
[11] For the ARMv7-M (Cortex-M3/M4/M4F) architecture...
[12] The QF_PRIMASK_DISABLE() macro resolves to the inline assembly instruction CPSD i, which sets the PRIMASK.
[13] The QF_PRIMASK_ENABLE() macro resolves to the inline assembly instruction CPSE i, which clears the PRIMASK.
[14] Interrupts are disabled by setting the BASEPRI register to the value defined in the QF_BASEPRI macro (see label [19]). This setting of the BASEPRI instruction msr BASEPRI,... is surrounded by setting and clearing the PRIMASK register, as a workaround for a hardware problem in ARMv7M or higher architectures, core r0p1.
[15] The QF_INT_ENABLE() macro sets the BASEPRI register to zero, which disables BASEPRI interrupt masking.
[16] The QF_CRIT_STAT is empty, meaning that the critical section uses the simple policy of "unconditional interrupt disabling".
[17] The QF_CRIT_ENTRY() enters a critical section. Interrupts are disabled with the macro QF_INT_DISABLE() defined at label [12].
[18] The QF_CRIT_EXIT() macro leaves the critical section. Interrupts are unconditionally re-enabled with the macro QF_INT_ENABLE() defined at label [13].
[19] The QF_BASEPRI value is defined such that it is the lowest priority for the minimum number of 3 priority-bits that the ARMv7M or higher architectures must provide. This partitions the interrupts as "kernel-unaware" and "kernel-aware" interrupts, as shown in section arm-cm_int-assign.
[20] For the ARMv7-M architecture, the QF_AWARE_ISR_CMSIS_PRI priority level suitable for the CMSIS function NVIC_SetPriority() is determined by the QF_BASEPRI value.
[21] The macro QF_LOG2() is defined to take advantage of the CLZ instruction (Count Leading Zeroes), which is available in the ARMv7-M architecture.
[22] The macro QF_CRIT_EXIT_NOP() provides the protection against merging two critical sections occurring back-to-back in the QP/C++ code.
[23] For ARMv6 architecture, the prototype of the quick, hand-optimized log-base-2 function is provided (see also label [10]).
The qv_port.h Header File
The QV header file for the ARM Cortex-M port is located in /ports/arm-cm/qv/gnu/qv_port.h. This file provides the macro QV_CPU_SLEEP(), which specifies how to enter the CPU sleep mode safely in the non-preemptive QV kernel (see also Section 4.7 and [Samek 07]).
[1] For the ARMv6-M architecture, the macro QV_CPU_SLEEP() stops the CPU with the WFI instruction (Wait For Interrupt). After an interrupt wakes up the CPU, interrupts are re-enabled with the PRIMASK.
[2] For the ARMv7-M architecture, the macro QV_CPU_SLEEP() first disables interrupts by setting the PRIMASK, then clears the BASEPRI to enable all "kernel-aware" interrupts and only then stops the CPU with the WFI instruction (Wait For Interrupt). After an interrupt wakes up the CPU, interrupts are re-enabled with the PRIMASK. This sequence is necessary because the ARMv7M or higher architectures cannot be woken up by any interrupt blocked by the BASEPRI register.
[3] The macro QV_INIT() is defined as a call to the QV_init() function, which means that this function will be called from QF_init(). The QV_init() function initializes all available IRQ priorities in the MCU to the safe value of QF_BASEPRI.
The qv_port.c Implementation File
The QV implementation file for the ARM Cortex-M port is located in /ports/arm-cm/qv/gnu/qf_port.c. This file defines the function QV_init(), which for the ARMv7-M architecture sets the interrupt priorities of all IRQs to the safe value QF_BASEPRI.
[1] For the ARMv7-M architecture (Cortex-M3/M4/M7)...
[2] The exception priorities for User-Fault, Bus-Fault, and Mem-Fault are set to the value QF_BASEPRI.
[3] The exception priority for SVCCall is set to the value QF_BASEPRI.
[4] The exception priority for SysTick, PendSV, and Debug is set to the value QF_BASEPRI.
[5] The number of implemented IRQs is read from the SCnSCB_ICTR register
[6] The interrupt priority of all implemented IRQs is set to the safe value QF_BASEPRI in a loop.
Writing ISRs for QV
The ARM Cortex-M CPU is designed to use regular C functions as exception and interrupt service routines (ISRs).
Typically, ISRs are application-specific (with the primary purpose to produce events for active objects). Therefore, ISRs are not part of the generic QP/C++ port, but rather part of the BSP (Board Support Package).
The following listing shows an example of the SysTick_Handler() ISR (from the DPP example application). This ISR calls the QF_TICK_X() macro to perform QF time-event management.
Using the FPU in the QV Port
If you use ARMv7M or higher CPU and your application uses the hardware FPU, it should be enabled because it is turned off out of reset. The CMSIS-compliant way of turning the FPU on looks as follows:
SCB->CPACR |= (0xFU << 20);
FPU->FPCCR |= (1U << FPU_FPCCR_ASPEN_Pos) | (1U << FPU_FPCCR_LSPEN_Pos);
FPU NOT used in the ISRs
If you use the FPU only at the thread-level (inside active objects) and none of your ISRs use the FPU, you can set up the FPU not to use the automatic state preservation and not to use the lazy stacking feature as follows:
FPU->FPCCR &= ~((1U << FPU_FPCCR_ASPEN_Pos) | (1U << FPU_FPCCR_LSPEN_Pos));
With this setting, the processor uses only the standard 8-register interrupt stack frame with R0-R3,R12,LR,PC,xPSR. This scheme is the fastest and incurs no additional CPU cycles to save and restore the FPU registers.
QV Idle Processing Customization in QV_onIdle()
When no events are available, the non-preemptive QV kernel invokes the platform-specific callback function QV_onIdle(), which you can use to save CPU power, or perform any other "idle" processing (such as Quantum Spy software trace output).
Because QV_onIdle() must enable interrupts internally, the signature of the function depends on the interrupt locking policy. In case of the simple "unconditional interrupt locking and unlocking" policy, which is used in this ARM Cortex-M port, the QV_onIdle() takes no parameters. Listing 6 shows an example implementation of QV_onIdle() for the TM4C MCU. Other ARM Cortex-M embedded microcontrollers (e.g., NXP's LPC1114/1343) handle the power-saving mode very similarly.
[1] The non-preemptive QV kernel calls the QV_onIdle() callback with interrupts disabled, to avoid race condition with interrupts that can post events to active objects and thus invalidate the idle condition.
[2] The sleep mode is used only in the non-debug configuration, because sleep mode stops CPU clock, which can interfere with debugging.
[3] The macro QV_CPU_SLEEP() is used to put the CPU to the low-power sleep mode safely. The macro QV_CPU_SLEEP() is defined in the qv_port.h header file for the QV kernel and depends on the interrupt disabling policy used.
[4] When a sleep mode is not used, the QV_onIdle() callback simply re-enables interrupts.