Interrupts in the Nucleus SE RTOS
All modern microprocessors and microcontrollers have interrupt facilities of some sort. This capability is essential to provide the responsiveness required for many applications. Of course, being responsive and predictable is also a key objective behind the use of a real-time operating system, so the two topics do potentially conflict slightly. Using interrupts might compromise the real-time integrity of the OS. This subject, and the resolution of this conflict, was covered in an earlier article. Here, we will look at the interrupt handling strategy employed with Nucleus SE.
In all cases, interrupts are not controlled by Nucleus SE – they are processed when they occur according to priority and vectoring in the usual way. Their execution time is simply “stolen” from that available to run the mainline application code and the scheduler. Clearly this implies that all interrupt service routines should be simple, short and fast.
Native and Managed Interrupts
Nucleus SE does offer two ways to handle interrupts: “Native” interrupt service routines are nothing special and have somewhat limited opportunity to interact with the OS (at least they do when the priority scheduler is selected); “Managed” interrupt service routines have a much wider range of API calls that may be made.
By means of some entry/exit macros, an interrupt service routine used with a Nucleus SE application may be designated to be Native or Managed.
Nucleus SE native interrupts are standard interrupt service routines – you can think of them as “unmanaged”. They would typically be used when am interrupt may occur with high frequency and requires servicing with very low overhead. The routine is most likely to be coded in C, as many modern embedded compilers support the writing of interrupt service routines by means of the interrupt keyword. The only context information saved is that which the compiler deems necessary. This leads to significant limitations in what operations a native interrupt routine can perform, as we shall see shortly.
To construct a Nucleus SE native interrupt service routine, simply write the ISR in the usual way, including a call to the macro NUSE_NISR_Enter() at the beginning and a call to NUSE_NISR_Exit() at the end. These macros are defined in nuse_types.h and simply set the global variable NUSE_Task_State to NUSE_NISR_CONTEXT.
If you need greater flexibility in what operations can be performed by an ISR, a Nucleus SE managed interrupt may be the solution. The key difference from a native interrupt is the context save. Instead of just allowing the compiler to stack a few registers, a managed interrupt saves the complete task context (in its context block) on entry. The current task’s context is then loaded from its context block at the end. This accommodates the possibility that the current task may be changed by the operation of the ISR code; this is entirely possible when the priority scheduler is in use. A complete description of Nucleus SE context save and restore was included in an earlier article.
Clearly the complete context save represents a higher overhead than the stacking of a few registers performed by a native interrupt. This is the price for extra flexibility and the reason why there is a choice of approaches to handling interrupts.
A managed interrupt is constructed using the macro NUSE_MANAGED_ISR(), which is defined in nuse_types.h. This macro constructs a function which contains the following sequence:
task context save
set NUSE_Task_State to NUSE_MISR_CONTEXT
call user-supplied ISR code function
restore NUSE_Task_State to previous setting
task context restore
The macro takes two parameters: a name for the interrupt, which is used as the function name for the constructed routine; the name of the function containing the user-supplied ISR logic.
The Nucleus SE real time clock ISR, which is described later in this article, serves as an example of a managed ISR.
API Calls from Interrupt Service Routines
The range of API functions that may be called from a native or managed ISR depends on which scheduler has been selected. Broadly, the use of the priority scheduler provides many opportunities for the scheduler to be invoked as a result of an API function call, which would be a problem in a native ISR.
API Calls from a Native ISR with Priority Scheduler
A limited range of API function calls are permitted from a native ISR with the priority scheduler. This limitation is the result of the flexibility of the Nucleus SE API – many calls can result in a task being made ready and it would not be possible for the scheduler to be called by a native ISR (as the task context is not saved). There is greater flexibility if task blocking is not enabled.
The following API calls are always permitted:
However, the only one that is really useful is NUSE_Signals_Send(), as this provides a good way to indicate to a task that some work is required.
If blocking is disabled, which means that a task may not be made ready by many API calls, a number of additional API functions are available:
Certain API calls are never allowed from native ISRs, as they inevitably require the scheduler to operate:
API Calls from a Managed ISR or Native ISR with Non-priority Scheduler
A much wider range of API functions may be called from an ISR when the run to completion, round robin or time sliced scheduler is in use. If the priority scheduler is used, a managed ISR facilitates a similar wide range. This is because calls that may result in a different task being scheduled are permitted. This capability is facilitated by code in NUSE_Reschedule() that detects that the context of the call is an ISR and suppresses the context switch (allowing it to occur at the end of the ISR). Full details of the scheduler operation were covered in an earlier article.
A key requirement is that API calls within an ISR must not result in suspension of the current task – waiting upon a resource, for example. In other words, such calls should be made with the suspend option set to NUSE_NO_SUSPEND.
Given this proviso, the following API calls may be used:
A few API calls are never permitted, as they appertain specifically to the current task:
Real Time Clock ISR
The real time clock (RTC) ISR is the only complete interrupt service routine provided with Nucleus SE. Apart from providing all the required timing functionality for Nucleus SE, it also serves as an example of how to code a managed interrupt.
RTC ISR Operations
The facilities provided by the RTC ISR were outlined in an earlier, which covered the broad topic of system time in Nucleus SE. All of the functionality is optional, depending on how the application is configured. Here is the complete code for the RTC ISR.
We will look at each of the four areas of functionality in the RTC ISR:
If any application timers are configured, the ISR loops around to service each one by decrementing its counter value. If a timer expires (i.e. the counter reaches zero), two actions are effected:
If timer expiration routines are configured and the timer has a valid (not NULL) pointer to a function (in the NUSE_Timer_Expiration_Routine_Address), the routine is executed, receiving a parameter from NUSE_Timer_Expiration_Routine_Parameter.
If the timer has a reschedule time (i.e. a non-zero value in NUSE_Timer_Reschedule_Time) the timer is reloaded with that value.
Application timers were described in more detail in a previous article.
If a system clock is configured, the value of NUSE_Tick_Clock is simply incremented. Further discussion on system time may be found in a previous article.
If task sleep is enabled (i.e. the API call NUSE_Task_Sleep() is configured), each task’s timeout counter (entry in NUSE_Task_Timeout_Counter) is checked and, if non-zero, decremented. If any counter reaches zero, the corresponding task is woken up.
Time Slice Scheduling
If the time slice scheduler is in use, the time slice counter (NUSE_Time_Slice_Ticks) is decremented. If it reaches zero, the scheduler is called. The call to NUSE_Reschedule() takes care of resetting the counter.
A Managed Interrupt
Some explanation of the reasons why the RTC ISR is a managed interrupt may be useful, as, under the right circumstances, the user may want to recode it as a native interrupt to reduce the overhead. For example, if only the system time facility is used (i.e. no application timers, no task sleep and not the time slice scheduler) a native interrupt would be fine. The needs for a managed interrupt are as follows:
If timers are used and expiration routines configured, these routines may make API calls (from the interrupt context) that will cause a reschedule. These are subject to the same limitations as API calls made from ISRs (see earlier in this chapter).
If the priority scheduler is in use, an expiration of task sleep may require the scheduling of a higher priority task.
If the time slice scheduler is in use, it will definitely be called from the RTC ISR, so a managed interrupt is mandatory.