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 be often broken into shorter pieces by means of the "Reminder" state pattern [Reminder])
/ports/arm-cm/qv/
.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 back to the point of preemption. To avoid race conditions between the main loop and the interrupts, QV briefly disables interrupts.
QF_init()
function calls the function QV_init()
to set the interrupt priority of all IRQs available in the MCU to the safe value of QF_BASEPRI (for ARM-v7 architecture).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.
qp_port.h
header file does not change the default settings for all the rest of various object sizes inside QF. Please refer to Chapter 8 of [PSiCC2] for discussion of all configurable QF parameters.[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).
__ARM_ARCH
macro is specific to the GNU-ARM compiler. Other compilers for ARM Cortex-M provide different macros to detect the CPU type.[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 interrupt priorities are disabled by the kernel.
[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]).
CLZ
instruction. Therefore the log-base-2 calculation cannot be accelerated in hardware, as it is for ARM Cortex-M3 and higher.[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 a hardware problem in ARMv7M or higher architectures core r0p1:
MSR BASEPRI,...
with the CPSID i
/CPSIE i
pair, which is implemented in the QF_INT_DISABLE() macro. This workaround works also for Cortex-M3/M4 cores.[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 architecture 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.
__builtin_cls()
intrinsic function is specific to the GNU-ARM compiler. Other compilers for ARM Cortex-M use different function names for this intrinsic function.[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 the CPU is woken up by an interrupt, 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 the CPU is woken up by an interrupt, 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 main 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 setup 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.