The whole truth about the RTOS. Article # 10. Scheduler: additional features and context preservation
In the previous article, we looked at the different types of scheduling supported by the RTOS and the corresponding features in Nucleus SE. This article will look at the additional scheduling options in Nucleus SE and the process of saving and restoring context.
Previous articles in the series:
Article # 9. Scheduler: implementation
Article # 8. Nucleus SE: Internal Design and Deployment
Article # 7. Nucleus SE: Introduction
Article # 6. Other RTOS services
Article # 5. Interaction between tasks and synchronization
Article # 4. Tasks, Context Switching and Interrupts
Article # 3. Tasks and planning
Article # 2. RTOS: Structure and Real Time
Article # 1. RTOS: introduction.
When developing the Nucleus SE, I made the maximum number of functions optional, which saves on memory and / or time.
As mentioned earlier in the “Scheduler: Implementation” article , the Nucleus SE supports various options for suspending tasks, but this feature is optional and is enabled with the NUSE_SUSPEND_ENABLE symbol in nuse_config.h . If set to TRUE , the data structure is defined as NUSE_Task_Status  . This type of suspension applies to all tasks. The array is of type U8 , where 2 nibbles are used separately. The lower 4 bits contain the task status:
NUSE_READY, NUSE_PURE_SUSPEND , NUSE_SLEEP_SUSPEND , NUSE_MAILBOX_SUSPEND , etc. If the task is suspended by an API call (for example,NUSE_MAILBOX_SUSPEND ), the upper 4 bits contain the index of the object on which the task is suspended. This information is used when the resource becomes available and to call the API you need to find out which of the suspended tasks you need to resume.
To perform the suspension of tasks, a pair of scheduler functions is used: NUSE_Suspend_Task () and NUSE_Wake_Task () .
The NUSE_Suspend_Task () code is as follows:
The function saves the new task state (all 8 bits), received as the suspend_code parameter. When locking is enabled (see “API blocking calls” below), the return code NUSE_SUCCESS is preserved . Next, NUSE_Reschedule () is called .to transfer control to the next task.
The NUSE_Wake_Task () code is quite simple:
The task status is set to NUSE_READY . If the Priority Scheduler is not used, the current task continues to occupy the processor until it is time to release the resource. If the Priority Scheduler is used, NUSE_Reschedule () is called with the task index as the execution indication, since the task may have a higher priority and must be immediately put to execution.
API blocking calls
Nucleus RTOS supports a variety of API calls, with which the developer can pause (block) a task if resources are unavailable. The task will resume when resources are available again. This mechanism is implemented in Nucleus SE and applies to a number of kernel objects: a task can be locked in a memory section, in an event group, in a mailbox, a queue, a channel, or a semaphore. But, like most tools in the Nucleus SE, it is optional and is defined by the symbol NUSE_BLOCKING_ENABLE in nuse_config.h . If set to TRUE , then the array NUSE_Task_Blocking_Return  is defined , which contains the return code for each task; it can be NUSE_SUCCESS or codeNUSE_MAILBOX_WAS_RESET , indicating that the object was reset when the task was blocked. When locking is enabled, the corresponding code is included in the API functions using conditional compilation.
Nucleus RTOS calculates how many times a task has been scheduled since it was created and last reset. This feature is also implemented in the Nucleus SE, but is optional and is defined by the symbol NUSE_SCHEDULE_COUNT_SUPPORT in nuse_config.h . If set to TRUE , an array of NUSE_Task_Schedule_Count  of type U16 is created , which stores the counter of each task in the application.
Initial state of the task
When a task is created in the RTOS Nucleus, you can select its status: ready or suspended. In Nucleus SE, by default, all tasks are ready at startup. The option selected using the symbol NUSE_INITIAL_TASK_STATE_SUPPORT in nuse_config.h allows you to select the launch state. The NUSE_Task_Initial_State  array is defined in nuse_config.c and requires the initialization of NUSE_READY or NUSE_PURE_SUSPEND for each task in the application.
The idea of saving the task context with any type of scheduler other than RTC (Run to Completion) was presented in article # 3 “Tasks and Planning”. As already mentioned, there are several ways to keep context. Given that the Nucleus SE is not intended for 32-bit processors, I chose to use tables rather than a stack to save the context.
A two-dimensional array of the type ADDR NUSE_Task_Context   is used to save the context for all tasks in the application. The rows are NUSE_TASK_NUMBER (the number of tasks in the application), the columns are NUSE_REGISTERS (the number of registers to be saved; depends on the processor and is set to nuse_types.h) .
Of course, saving the context and restoring the code depends on the processor. And this is the only Nucleus SE code associated with a specific device (and development environment). I will give an example of a save / restore code for a ColdFire processor. Although this choice may seem strange due to an outdated processor, its assembler is easier to read than the assemblers of most modern processors. The code is fairly simple to use as the basis for creating a context switch for other processors:
When context switching is required, this code is invoked in NUSE_Context_Swap. Two variables are used: NUSE_Task_Active , the index of the current task, the context of which must be saved; NUSE_Task_Next, index of the task, the context of which must be loaded (see section “Global data”).
The context preservation process works as follows:
- Registers A0 and D0 are temporarily stored on the stack;
- A0 is configured to point to an array of context blocks NUSE_Task_Context   ;
- D0 is loaded using NUSE_Task_Active and multiplied by 72 (ColdFire has 18 registers requiring 72 bytes to store);
- then D0 is added to A0 , which now points to the context block for the current task;
- further registers are saved in the context block; first A0 and D0 (from the stack), then D1-D7 and A1-A6 , then SR and PC (from the stack, we will look at a quickly triggered context switch), and the stack pointer is saved at the end.
The process of loading the context is the same sequence of actions in the reverse order:
- A0 is configured to point to an array of context blocks NUSE_Task_Context   ;
- D0 is loaded using NUSE_Task_Active , incremented and multiplied by 72;
- then D0 is added to A0 , which now points to the context block for the new task (since the context must be loaded in the reverse process of saving the sequence, the stack pointer is required first);
- further registers are restored from the context block; first the stack pointer, then PC and SR are pushed onto the stack, then D1-D7 and A1-A6 are loaded , and at the end D0 and A0 .
The difficulty in implementing context switching is difficult access to the state register for many processors (for ColdFire, this is SR ). A common solution is to interrupt, i.e., a program interrupt or interrupt by conditional transition, which results in the SR being loaded onto the stack along with the PC . This is how Nucleus SE works on ColdFire. The macro NUSE_CONTEXT_SWAP () is set in nuse_types.h , which expands to:
asm ("trap # 0");
Below is the initialization code ( NUSE_Init_Task () in nuse_init.c ) for the context blocks:
This is the initialization of the stack pointer, PC and SR. The first two have the values set by the user in nuse_config.c . The SR value is defined as the NUSE_STATUS_REGISTER character in nuse_types.h . For ColdFire, this value is 0x40002000 .
The Nucleus SE scheduler requires very little memory to store data, but, of course, uses the data structures associated with the tasks, which will be discussed in detail in future articles.
The scheduler does not use the data located in the ROM, and the RAM contains from 2 to 5 global variables (all set in nuse_globals.c ), depending on which scheduler is used:
- NUSE_Task_Active - a variable of type U8 , containing the index of the current task;
- NUSE_Task_State - a variable of type U8 , containing a value indicating the status of the currently running code, which can be a task, an interrupt handler, or a launch code; possible values: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT and NUSE_MISR_CONTEXT ;
- NUSE_Task_Saved_State is a U8 variable used to protect the NUSE_Task_State value in a controlled interrupt;
- NUSE_Task_Next - a variable of type U8 , containing the index of the next task, which should be scheduled for all schedulers except RTC;
- NUSE_Time_Slice_Ticks is a variable of type U16 containing a count of time slices ; Used only with TS Scheduler.
Data Footprint Data Scheduler
The Nucleus SE Scheduler does not use ROM data. The exact amount of RAM data varies depending on the scheduler used:
- for RTC - 2 bytes ( NUSE_Task_Active and NUSE_Task_State );
- for RR and Priority - 4 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State and NUSE_Task_Next );
- for TS - 6 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next and NUSE_Time_Slice_Ticks ).
Implementation of other planning mechanisms
Although Nucleus SE offers a choice of 4 schedulers, covering most cases, the open architecture allows you to realize the possibilities for other cases.
Time slicing with background task
As discussed in article # 3, “Tasks and Planning,” a simple time slice scheduler has limitations, because it limits the maximum time that a task can occupy a processor. A more difficult option would be to add support for the background task. Such a task could be scheduled on any slot allocated for suspended tasks, and run when the slot was partially released. This approach allows you to schedule tasks at regular intervals and the predicted amount of time the processor core to perform.
Priority and Round Robin (RR)
In most real-time kernels, the priority scheduler supports several tasks at each priority level, unlike Nucleus SE, where each task has a unique level. I prefer the latter because it greatly simplifies the data structures and, therefore, the scheduler code. To support more complex architectures, numerous ROM and RAM tables would be required.
About the author: Colin Walls has been working in the electronics industry for more than thirty years, spending a significant amount of time on embedded software. He is now an embedded software engineer in Mentor Embedded (a division of Mentor Graphics). Colin Walls often speaks at conferences and seminars, author of numerous technical articles and two books on embedded software. Lives in the UK. ProfessionalColin's blog , e-mail: email@example.com.
On translation: this cycle of articles seemed interesting because, in spite of the outdated described approaches, the author introduces a little-prepared reader with real-time OS features in a very clear language. I myself belong to the team of creators of the Russian RTOS , which we intend to make free , and I hope that the cycle will be useful for novice developers.