The scheduler – implementation
The key concepts of RTOS schedulers were covered in a previous article. Here we will look at the facilities offered by Nucleus RTOS and, in much more detail, those provided by Nucleus SE.
Scheduling in Nucleus RTOS
Since Nucleus RTOS is a fully-fledged, well-proven, commercial RTOS, it may be assumed (correctly!) that the scheduler has been carefully designed for the requirements of such a product. It is sophisticated and flexible, giving the developer a wide range of options to address almost any conceivable real-time programming challenge.
The scheduler can support an indefinite number of tasks (only limited really by available resources) and is priority driven. A task may be assigned a priority from 0 to 255, where 0 is the highest priority and 255 the lowest. A task’s priority is dynamic – it may be changed at run time by itself or by another task in the application. Multiple tasks may be assigned the same priority level. In an extreme case, all the tasks may be assigned the same priority, which enables the implementation of a round robin or time sliced scheduling scheme.
If there are multiple tasks at the same priority, they will be scheduled in a round robin fashion, in the order in which they became ready. A task would need to suspend itself or relinquish control to allow the next one to be run. Tasks may also be assigned time slices, which enables a more controlled division of available CPU time.
Task scheduling is 100% deterministic, as might be expected from such a kernel. Tasks may also be dynamically created and destroyed, the process of which is handled by the scheduler seamlessly.
Scheduling in Nucleus SE
I designed all aspects of Nucleus SE to be broadly compatible with Nucleus RTOS, while being simpler and very memory efficient. The scheduler is no exception. It provides many of the capabilities of the Nucleus RTOS scheduler, but is somewhat restricted. Flexibility is achieved by build-time configuration.
A Nucleus SE application may have a maximum of 16 tasks (and a minimum of one, obviously). The kernel does not reserve any tasks for its own use. Although this number could theoretically be increased, the efficiency of algorithms would be compromised; a number of data structures rely on holding a task index number (0 to 15) in a nibble (four bits) and these would need to be redesigned, along with their associated code.
To achieve a balance between flexibility and simplicity (and size), instead of having a single scheduler with multiple capabilities, Nucleus SE offers a choice of four types of scheduler: run to completion, round robin, time slice and priority. The scheduler is selected statically, at configuration (build) time. This selection is described, along with more details of each scheduler type, below in Scheduler Types.
Like all aspects of Nucleus SE, tasks are static objects. They are defined at configuration time and their priority (index) cannot be changed.
As outlined above, Nucleus SE offers a choice of four types of scheduler. In common with most aspects of Nucleus SE configuration, this choice is determined by an entry in nuse_config.h – the parameter NUSE_SCHEDULER_TYPE needs to be set appropriately, as illustrated in this extract from the configuration file:
Whichever scheduler is selected, its startup code is called immediately after system initialization. Full details of Nucleus SE initialization will be covered in a future article.
Run to Completion Scheduler
The Run to Completion (RTC) scheduler is the simplest option and should be the first choice if it meets the demands of the application. Each task must complete its work before returning and allowing the scheduler to run the next task.
There is no requirement for a separate stack for each task and all of the code is written in C – no assembly language is needed at all. Here is the entire code of the RTC scheduler.
The code is really just an infinite loop, which calls each task in turn. The array NUSE_Task_Start_Address contains pointers to each task’s outer function. The macro PF0 is simply a caste to convert the void pointer to be a pointer to a void function with no parameters. It is simply intended to aid code readability.
Conditional compilation is used to include support for optional functionality: NUSE_SUSPEND_ENABLE determines if tasks may be suspended; NUSE_SCHEDULE_COUNT_SUPPORT determines whether a count of each time a task is scheduled is required. Further details of these may be found in the next article.
Round Robin Scheduler
If a little more flexibility than that afforded by the RTC scheduler is required, the Round Robin (RR) scheduler may be a good choice. It provides the possibility for a task to relinquish control or suspend itself and later continue from the same point. The additional overhead, apart from code complexity and non-portability, is that each task requires its own stack.
The scheduler code is in two parts. The start-up component looks like this:
If initial task state support is enabled (by a setting of NUSE_INITIAL_TASK_STATE_SUPPORT – see Options in the next article) the first ready task is located; otherwise a task index of 0 is used. The context of this task is then loaded using NUSE_Context_Load(). For more detail on context save and restore, see Context Saving in the next article.
The second part of the scheduler is the “reschedule” component:
This code is called when a task relinquishes the CPU or is suspended.
The code selects the next task index to be run and places the value into NUSE_Task_Next, taking into account whether task suspend is enabled or not. The NUSE_CONTEXT_SWAP() macro is then used to invoke a context swap using a software interrupt. For more detail on context save and restore, see Context Saving in the next article.