The scheduler - options and context save
In the last article, I looked at the various kinds of scheduling that an RTOS might support and detailed the relevant facilities in Nucleus SE. This time, I will look at some of the optional features pertaining to scheduling in Nucleus SE and take a detailed look at the context save and restore process.
I designed Nucleus SE so that as many features as possible could be optional, allowing memory and/or time to be saved, if a capability is not required. This is apparent in a number of facets of task scheduling.
As mentioned in Task States in the previous article, Nucleus SE supports various forms of task suspend, but this functionality is entirely optional and is enabled by the symbol NUSE_SUSPEND_ENABLE in nuse_config.h. If this symbol is set to TRUE, the data structure NUSE_Task_Status is defined. This has an entry for each task. The array is of type U8 and the two nibbles are used separately. The lower nibble contains the task status: NUSE_READY, NUSE_PURE_SUSPEND, NUSE_SLEEP_SUSPEND, NUSE_MAILBOX_SUSPEND, etc. If the task is suspended on an API call (e.g. NUSE_MAILBOX_SUSPEND), the upper nibble contains the index of the object on which it is suspended. This information is used when a resource becomes available and the API call needs ascertain which suspended task should be woken.
A pair of scheduler functions – NUSE_Suspend_Task() and NUSE_Wake_Task() – are used to effect task suspend operations.
NUSE_Suspend_Task() is coded thus:
This function stores the new task status (both nibbles), which it receives as a parameter: suspend_code. If blocking is enabled (see API Call Blocking below), the return code NUSE_SUCCESS is stored. Then NUSE_Reschedule() is called to pass control to the next qualifying task.
The code for NUSE_Wake_Task() is quite simple:
The status of the task is set to NUSE_READY. If the Priority scheduler is not in use, the current task continues to have control of the CPU until it is time to relinquish. If priority scheduling is configured, NUSE_Reschedule() is called, with the task index as a “hint”, as this task may be of a higher priority and should be scheduled straight away.
API Call Blocking
In Nucleus RTOS, there are numerous API calls where the programmer has the option of specifying that a task be suspended (blocked) if a resource is unavailable. The task is then resumed when that resource is free again. This facility is also implemented in Nucleus SE and applies to a number of kernel objects; a task may be blocked on a memory partition, an event group, a mailbox, a queue, a pipe or a semaphore. But, like many facilities in Nucleus SE, it is optional and determined by a symbol – NUSE_BLOCKING_ENABLE – in nuse_config.h. If this symbol is set to TRUE, the NUSE_Task_Blocking_Return array is defined, which carries the return code for each task; this may be NUSE_SUCCESS or codes of the form NUSE_MAILBOX_WAS_RESET that indicate an object was reset while the task was blocked. Also, if blocking is enabled, the appropriate code is included in API functions by conditional compilation.
Nucleus RTOS keeps a count of how many times a task has been scheduled since it was created or last reset. This facility is also implemented in Nucleus SE, but it is optional and determined by a symbol – NUSE_SCHEDULE_COUNT_SUPPORT – in nuse_config.h. If this symbol is set to TRUE, the U16 array NUSE_Task_Schedule_Count, which stores the count for each task in the application, is instantiated.
Initial Task State
When a task is created in Nucleus RTOS, its state – ready or suspended – may be selected. In Nucleus SE, by default, all tasks are ready at start-up. An option, selected using the symbol NUSE_INITIAL_TASK_STATE_SUPPORT in nuse_config.h, provides a means to select the start up state. An array, NUSE_Task_Initial_State, is defined in nuse_config.c and needs to be initialized to NUSE_READY or NUSE_PURE_SUSPEND for each task in the application.
The idea of task context saving – with any type of scheduler other than Run to Completion – was introduced in a previous article. As I mentioned there, it is possible to save context in a number of different ways. Given that Nucleus SE is not targeted at high-end 32-bit processors, I chose not to use the stack for context saving and adopted a table-based approach.
A 2-dimensional array of type ADDR, called NUSE_Task_Context, is used to hold contexts for all the tasks in an application. Its first dimension is NUSE_TASK_NUMBER (the number of tasks in the application); the second dimension is NUSE_REGISTERS, which is processor dependent and set up in nuse_types.h – this is the number of registers to be saved.
The context saving and restoring code is processor dependent (obviously!) and is really the only Nucleus SE code that is so tied a specific device (and toolkit). I chose to write the example contest save/restore code for the ColdFire processor. This may seem an odd choice, being a somewhat obsolete CPU, but I felt that the assembly language was so much easier to read than many newer devices, that it would be worthwhile. The code is reasonably straightforward to follow and to use as a model to write a context switch for other processors:
When a context switch is required, this code is called at the entry point NUSE_Context_Swap. Two global variables are used: NUSE_Task_Active is the index of the current task, whose context is to be saved; NUSE_Task_Next is the index of the task whose context is to be loaded. See Global Data later in this article.
The context save process works as follows:
registers A0 and D0 are saved temporarily on the stack
A0 is set to point to the context blocks array NUSE_Task_Context
D0 is loaded with NUSE_Task_Active and multiplied by 72 (ColdFire has 18 registers requiring 72 bytes of storage)
D0 is then added to A0, which now points to the context block for the current task
the registers are then saved into the context block; first A0 and D0 (from the stack), then D1-D7 and A1-A6, then the SR and PC (from the stack – we will look at how a context swap is actually initiated shortly), lastly the stack pointer is saved
The context load process is simply the same sequence in reverse:
A0 is set to point to the context blocks array NUSE_Task_Context
D0 is loaded with NUSE_Task_Active, incremented and multiplied by 72
D0 is then added to A0, which now points to the end of the context block for the new task (as the context load must be performed in the reverse sequence to the save – the stack pointer is needed first)
the registers are then recovered from the context block; first the stack pointer, the PC and SR are pushed onto the stack, then D1-D7 and A1-A6, lastly D0 and A0 are loaded