Runtime for ARM Cortex-M

by Marco Wuelser

 

This Guide Article has been written for Version 2.1 of the DATAFLOW Software. For Previous Releases use the version selection in the navigation bar at the top of this page.

 

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.

Synopsis

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:

  1. 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.
  2. 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.
  3. ARM Cortex-M enters interrupt context without disabling interrupts (without setting the PRIMASK bit or the BASEPRI register). 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.
  4. The DATAFLOW Runtime uses the PendSV exception (number 14) and the NMI exception (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 PendSV_Handler() and NMI_Handler() exception handlers.
  5. The PendSV interrupt is set to the lowest priority (0xFF) by the runtime.
  6. It is strongly recommended to not use the lowest priority (0xFF0 for any other interrupt in your application.
  7. The first line of code in each Interrupt Service Routine (ISR) must call the RuntimeInterrupts::applicationIsrEntry method. The last line must call the RuntimeInterrupts::applicationIsrExit method.

    NOTE:
    When the DATAFLOW Code Generator is used, this is already the case in the generated *ApIrq.cpp file.

  8. ApplicationIsrExit will post the PendSV interrupt. This interrupt is always executed when all other (nested) interrupts have been completely handled (tail chaining).
  9. In ARM Cortex-M the whole prioritization of interrupts, including the PendSV exception, is performed entirely by the NVIC. Because the PendSV has the lowest priority in the system, the NVIC tail-chains to the PendSV exception only after exiting the last nested interrupt.
  10. The pushing of the 8 registers comprising the ARM Cortex-M interrupt stack frame upon entry to NMI exception is wasteful in a single-stack kernel, but is necessary to perform full interrupt return to the preempted context through the NMI's return.

Preemption

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).

Synchronous Preemption

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 RuntimeCore::sendEvent() function.

Synchronous_Preemtion.png

Asynchronous Preemption

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.

Async_Preemtion_Cortex-M.png

  1. The timeline begins with the DATAFLOW Runtime executing the idle loop.
  2. 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.
  3. The ISR performs its work. At the end, applicationIsrExit() must be called, which sets the pending flag for the PendSV exception in the NVIC. The priority of the PendSV exception is configured to be the lowest of all exceptions (0xFF), so the ISR continues executing while PendSV exception remains pending. At the ISR return, the ARM Cortex-M CPU performs tail-chaining to the pending PendSV exception.
  4. The PendSV exception is entered via tail-chaining.
  5. The job of the PendSV exception is to run the runtimeSchedule() method, which calls the execute method for each pending message for each active part in order of descending priority.

    NOTE:
    The runtimeSchedule() method must run in thread context, while PendSV executes in the exception context. The change of the context is achieved by returning from the PendSV exception context. A custom stack frame is created with the return address set to the runtimeSchedule() method.

  6. The runtimeSchedule() method enables interrupts and calls the execute method of the Low-priority active part.
  7. 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.
  8. 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.
  9. The High-priority ISR sets the pending flag for the PendSV exception by means of the applicationIsrExit() method. When the High-priority ISR returns, the NVIC does not tail-chain to the PendSV exception, because a higher-priority ISR than PendSV is still active.
  10. The NVIC performs an exception return to the preempted Low-priority interrupt, which finally completes.
  11. Upon the exit from the Low-priority ISR, it too sets the pending flag for the PendSV exception. The PendSV is already pended from the High-priority interrupt, so pending is again is redundant, but it is not an error.
  12. The NVIC performs tail-chaining to the PendSV exception.
  13. The PendSV exception 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.
  14. 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 NMI exception.

    NOTE:
    The NMI exception is pended while interrupts are still disabled. This is not a problem, because NMI cannot be masked by disabling interrupts, so it runs without any problems.

  15. The only job of the NMI exception 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.
  16. 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 NMI exception to return to the preempted active part.
  17. The NMI exception discards its own interrupt stack frame and returns using the interrupt stack frame from the preempted thread context

 

Required Module: DATAFLOW Runtime

This Article has been written based on V2.1.1 of the DATAFLOW software. 
Latest update 2023-05-31 by WUM.

Go back