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
A complication, with implementing a context swap, is that access to the status register (SR for ColdFire) challenging on many processors. A commonly applicable solution is to simulate an interrupt – i.e. perform a software interrupt or trap – which results in the SR being pushed onto the stack along with the PC . This is how the ColdFire implementation of Nucleus SE works. A macro – NUSE_CONTEXT_SWAP() – is defined in nuse_types.h , which expands to:
asm(” trap #0″);
This vectors through address $80 to NUSE_Context_Swap .
Here is the initialization code (in NUSE_Init_Task() in nuse_init.c ) for the context blocks:
This only initializes the stack pointer, PC and the SR . The first two have values set by the user in nuse_config.c . The value of SR is defined as a symbol – NUSE_STATUS_REGISTER – in nuse_types.h . For ColdFire, this value is 0x40002000 .
The Nucleus SE scheduler itself requires very little data memory, but, of course, uses data structures associated with tasks which will be covered in detail in upcoming articles.
There is no ROM data associated with the scheduler itself and between two and five global RAM variables (all defined in nuse_globals.c ), depending on which scheduler is in use:
NUSE_Task_Active – this is a variable of type U8 , which contains the index of the currently running task
NUSE_Task_State – this is a variable of type U8 , which contains a value which indicates the “state” of the code currently executing, which may be a task, and interrupt service routine or start-up code; possible values are: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT and NUSE_MISR_CONTEXT .
NUSE_Task_Saved_State – this is a variable of type U8 , which is used to preserve the value of NUSE_Task_State in managed interrupts.
NUSE_Task_Next – this is a variable of type U8 , which contains the index of the next task to be scheduled for all but the RTC scheduler.
NUSE_Time_Slice_Ticks – this is a variable of type U16 , which contains the time slice tick counter; it is only used with the TS scheduler.
Scheduler Data Footprint
The Nucleus SE scheduler uses no ROM data. The exact amount of RAM data varies depending upon which scheduler is in use:
For RTC, it is 2 bytes (NUSE_Task_Active and NUSE_Task_State ).
For RR and Priority, it is 4 bytes (NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State and NUSE_Task_Next ).
For TS, it is 6 bytes (NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next and NUSE_Time_Slice_Ticks ).
Implementing Other Scheduling Schemes
Although Nucleus SE is supplied with a choice of just four scheduler types, which should cover most eventualities, the open architecture lends itself to the implementation of some other possibilities.
Time Slice with Background
As discussed in a previous article, a simple time slice scheduler has limitations, as it simply bounds the maximum time that a task can “hog” the CPU. A more sophisticated option would be to add support for a background task. This task would be scheduled in any slots allocated to tasks that are suspended and would also run if a task relinquished the remainder of a slot. This approach results in tasks being scheduled at strictly regular intervals and their receiving a predictable proportion of CPU time.
Priority and Round Robin
In many real-time kernels, the priority scheduler supports multiple tasks at each priority level, unlike Nucleus SE where each task has a unique level. I made this choice because it greatly simplified the data structures and, hence, the scheduler code. Multiple tables in both ROM and RAM would be needed to support the more sophisticated architecture.
Colin Walls has over thirty years experience in the electronics industry, largely dedicated to embedded software. A frequent presenter at conferences and seminars and author of numerous technical articles and two books on embedded software, Colin is an embedded software technologist with Mentor Embedded [the Mentor Graphics Embedded Software Division], and is based in the UK. His regular blog is located at: http://blogs.mentor.com/colinwalls. He may be reached by email at firstname.lastname@example.org