The DATAFLOW Runtime for ARM Cortex-M provides a preemptive scheduler. It has been designed to execute active parts in the same way as a prioritized interrupt controller using a single stack. The Implementation is used for Cortex-M0, M3 and M7.
The ARM Cortex-M architecture is designed primarily for the traditional real-time kernels that use multiple per-thread stacks. Therefore, implementation of the non-blocking, single-stack scheduler of the DATAFLOW Runtime is a bit more involved on Cortex-M than other MCUs and works as follows:
- The ARM Cortex-M processor executes the application code (active parts) in the Privileged Thread mode, which is exactly the mode entered out of reset (default mode). The exceptions (including all interrupts) are always processed in the Privileged Handler mode.
- DATAFLOW Runtime uses only the Main Stack Pointer (DATAFLOW is a single stack runtime). The Process Stack Pointer is not used and is not initialized.
- ARM Cortex-M enters interrupt context without disabling interrupts (without setting the
PRIMASKbit or the
BASEPRIregister). Generally, you should not disable interrupts inside your ISRs. In particular, any runtime functions shall be called with interrupts enabled to avoid nested critical sections.
- The DATAFLOW Runtime uses the
PendSVexception (number 14) and the
NMIexception (number 2) to perform asynchronous preemption and return to the preempted active part, respectively. The startup code will initialize the Interrupt Vector Table with the addresses of
PendSVinterrupt is set to the lowest priority (0xFF) by the runtime.
- It is strongly recommended to not use the lowest priority (0xFF0 for any other interrupt in your application.
- The first line of code in each Interrupt Service Routine (ISR) must call the
RuntimeInterrupts::applicationIsrEntrymethod. The last line must call the
When the DATAFLOW Code Generator is used, this is already the case in the generated
ApplicationIsrExitwill post the
PendSVinterrupt. This interrupt is always executed when all other (nested) interrupts have been completely handled (tail chaining).
- In ARM Cortex-M the whole prioritization of interrupts, including the
PendSVexception, is performed entirely by the NVIC. Because the
PendSVhas the lowest priority in the system, the NVIC tail-chains to the
PendSVexception only after exiting the last nested interrupt.
- The pushing of the 8 registers comprising the ARM Cortex-M interrupt stack frame upon entry to
NMIexception is wasteful in a single-stack kernel, but is necessary to perform full interrupt return to the preempted context through the
In order to ensure that a high priority active part is not blocked by a long running low priority one, the low priority active part is preempted whenever a message is sent to an active part with higher priority. This can happen in synchronous way (sending a message from application code) and asynchronous way (sending a message from an ISR).
The "synchronous preemption" occurs when one (low-priority) active part is preempted by another (high-priority) active part. DATAFLOW Runtime handles this case as a regular function call. This function call happens inside the
The "asynchronous preemption" occurs when an interrupt sends a message to an active part with a higher priority as the current one. In ARM Cortex-M, this preemption is hanlded in the
PendSV exception handler.
- The timeline begins with the DATAFLOW Runtime executing the idle loop.
- At some point an interrupt occurs and the CPU immediately suspends the idle loop, pushes the interrupt stack frame to the Main Stack and starts executing the ISR.
- The ISR performs its work. At the end, applicationIsrExit() must be called, which sets the pending flag for the
PendSVexception in the NVIC. The priority of the
PendSVexception is configured to be the lowest of all exceptions (0xFF), so the ISR continues executing while
PendSVexception remains pending. At the ISR return, the ARM Cortex-M CPU performs tail-chaining to the pending
PendSVexception is entered via tail-chaining.
- The job of the
PendSVexception is to run the
runtimeSchedule()method, which calls the execute method for each pending message for each active part in order of descending priority.
runtimeSchedule()method must run in thread context, while
PendSVexecutes in the exception context. The change of the context is achieved by returning from the
PendSVexception context. A custom stack frame is created with the return address set to the
runtimeSchedule()method enables interrupts and calls the execute method of the Low-priority active part.
- Some time later a low-priority interrupt occurs. The Low-priority active part is suspended and the CPU pushes the interrupt stack frame to the Main Stack and starts executing the ISR.
- Before the Low-priority ISR completes, it too gets preempted by a High-priority ISR. The CPU pushes another interrupt stack frame and starts executing the High-priority ISR.
- The High-priority ISR sets the pending flag for the
PendSVexception by means of the
applicationIsrExit()method. When the High-priority ISR returns, the NVIC does not tail-chain to the
PendSVexception, because a higher-priority ISR than
PendSVis still active.
- The NVIC performs an exception return to the preempted Low-priority interrupt, which finally completes.
- Upon the exit from the Low-priority ISR, it too sets the pending flag for the
PendSVis already pended from the High-priority interrupt, so pending is again is redundant, but it is not an error.
- The NVIC performs tail-chaining to the
PendSVexception returns to the DATAFLOW scheduler as previously described. The scheduler detects that the High-priority active part has a pending message and calls its execute method. The High-priority active part runs to completion and returns to the scheduler.
- The scheduler does not find any more higher-priority active parts to execute and needs to return to the preempted active part. The only way to restore the interrupted context in ARM Cortex-M is through the interrupt return, but the scheduler is executing outside of the interrupt context (in fact, it is executing in the Privileged Thread mode). The scheduler enters the Handler mode by pending the
NMIexception is pended while interrupts are still disabled. This is not a problem, because
NMIcannot be masked by disabling interrupts, so it runs without any problems.
- The only job of the
NMIexception is to discard its own interrupt stack frame, re-enable interrupts, and return using the interrupt stack frame that has been on the stack from the moment of preemption.
- The Low-priority active part, which has been preempted all that time, resumes and finally runs to completion and returns to the scheduler. The scheduler does not find any more active parts to execute and causes the
NMIexception to return to the preempted active part.
NMIexception discards its own interrupt stack frame and returns using the interrupt stack frame from the preempted thread context