/
Kernel Structure

Kernel Structure

This chapter describes some of the structural aspects of µC/OS-II. You will learn:

  • How µC/OS-II handles access to critical sections of code
  • What a task is, and how µC/OS-II knows about your tasks
  • How tasks are scheduled
  • How µC/OS-II determines the percent CPU your application is using
  • How to write Interrupt Service Routines (ISRs)
  • What a clock tick is and how µC/OS-II handles it
  • How to initialize µC/OS-II
  • How to start multitasking

Application Services

This chapter also describes the application services listed in table 3.1. The code for OSSchedLock() and OSSchedUnlock() can be disabled by setting OS_SCHED_LOCK_EN to 0 in OS_CFG.H as shown in table 3.1. You should note that the other services cannot be ‘compiled out’ because they are an integral part of the core services offered by µC/OS-II.

Table - Table 3.1 Core services configuration constants in OS_CFG.H
µC/OS-II Core ServiceEnabled when set to 1 in OS_CFG.H
OS_ENTER_CRITICAL()
OS_EXIT_CRITICAL()
OSInit()
OSStart()
OSIntEnter()
OSIntExit()
OSSchedLock()OS_SCHED_LOCK_EN
OSSchedUnlock()OS_SCHED_LOCK_EN
OSVersion()


Figure 3.1 shows the µC/OS-II architecture and its relationship with the hardware. When you use µC/OS-II in an application, you are responsible for providing the Application Software and the µC/OS-II Configuration sections. This book and CD contain all the source code for the Processor-Independent Code section as well as the Processor-Specific Code section for the Intel 80x86, real mode, large model. If you intend to use µC/OS-II on a different processor, you need to either obtain a copy of a port for the processor you intend to use or write one yourself if the desired processor port is not available. Check the official µC/OS-II Web site at www.micrium.com for a list of available ports.

Figure - Figure 3.1 µC/OS-II File Structure


Critical Sections, OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL()

µC/OS-II, like all real-time kernels, needs to disable interrupts in order to access critical sections of code and to re-enable interrupts when done. This allows µC/OS-II to protect critical code from being entered simultaneously from either multiple tasks or ISRs. The interrupt disable time is one of the most important specifications that a real-time kernel vendor can provide because it affects the responsiveness of your system to real-time events. µC/OS-II tries to keep the interrupt disable time to a minimum, but with µC/OS-II, interrupt disable time is largely dependent on the processor architecture and the quality of the code generated by the compiler.

Processors generally provide instructions to disable/enable interrupts, and your C compiler must have a mechanism to perform these operations directly from C. Some compilers allow you to insert in-line assembly language statements into your C source code. This makes it quite easy to insert processor instructions to enable and disable interrupts. Other compilers contain language extensions to enable and disable interrupts directly from C.

To hide the implementation method chosen by the compiler manufacturer, µC/OS-II defines two macros to disable and enable interrupts: OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL(), respectively. Because these macros are processor specific, they are found in a file called OS_CPU.H. Each processor port thus has its own OS_CPU.H file.

OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() are always used in pair to wrap critical sections of code as shown below:


{
    .
    .
    OS_ENTER_CRITICAL();
    /* µC/OS-II critical code section */
    OS_EXIT_CRITICAL();
    .
    .
}


Your application can also use OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() to protect your own critical sections of code. Be careful, however, because your application will crash (i.e. hang) if you disable interrupts before calling a service such as OSTimeDly() (see chapter 5). This happens because the task is suspended until time expires, but because interrupts are disabled, you would never service the tick interrupt! Obviously, all the PEND calls are also subject to this problem, so be careful. As a general rule, you should always call µC/OS-II services with interrupts enabled!

OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() can be implemented using three different methods. The actual method used by your port depends on the capabilities of the processor as well as the compiler used (see Chapter 13, Porting µC/OS-II). The method used is selected by the #define constant OS_CRITICAL_METHOD which is defined in OS_CPU.H of the port you will be using for your application (i.e. product).

OS_CRITICAL_METHOD == 1

The first and simplest way to implement these two macros is to invoke the processor instruction to disable interrupts for OS_ENTER_CRITICAL() and the enable interrupts instruction for OS_EXIT_CRITICAL(). However, there is a little problem with this scenario. If you call a µC/OS-II function with interrupts disabled, on return from a µC/OS-II service (i.e. function), interrupts would be enabled! If you had disabled interrupts prior to calling µC/OS-II, you may want them to be disabled on return from the µC/OS-II function. In this case, this implementation would not be adequate. However, with some processors/compilers, this is the only method you can use.

OS_CRITICAL_METHOD == 2

The second way to implement OS_ENTER_CRITICAL() is to save the interrupt disable status onto the stack and then disable interrupts. OS_EXIT_CRITICAL() is simply implemented by restoring the interrupt status from the stack. Using this scheme, if you call a µC/OS-II service with interrupts either enabled or disabled, the status is preserved across the call. In other words, interrupts would be enabled after the call if they were enabled before the call and, interrupts would be disabled after the call if they were disabled before the call. Be careful when you call a µC/OS-II service with interrupts disabled because you are extending the interrupt latency of your application. The pseudo code for these macros is shown below:


#define OS_ENTER_CRITICAL()   \
        asm(" PUSH   PSW")    \
        asm(" DI")
#define OS_EXIT_CRITICAL()    \
        asm(" POP    PSW")


Here, I’m assuming that your compiler will allow you to execute inline assembly language statements directly from your C code as shown above. You will need to consult your compiler documentation for this.

The PUSH PSW instruction pushes the ‘Processor Status Word’, PSW (also known as the condition code register or, processor flags) onto the stack. The DI instruction stands for ‘Disable Interrupts’. Finally, the POP PSW instruction is assumed to restore the original state of the interrupt flag from the stack. The instructions I used are only for illustration purposes and may not be actual processor instructions.

Some compilers do not optimize inline code real well and thus, this method may not work because the compiler may not be ‘smart’ enough to know that the stack pointer was changed (by the PUSH instruction). Specifically, the processor you are using may provide a ‘stack pointer relative’ addressing mode which the compiler can use to access local variables or function arguments using and offset from the stack pointer. Of course, if the stack pointer is changed by the OS_ENTER_CRITICAL() macro then all these stack offsets may be wrong and would most likely lead to incorrect behavior.

OS_CRITICAL_METHOD == 3

Some compiler provides you with extensions that allow you to obtain the current value of the PSW (Processor Status Word) and save it into a local variable declared within a C function. The variable can then be used to restore the PSW back as shown in listing 3.1 Below.

Listing - Listing 3.1. Saving and restoring the PSW
void Some_uCOS_II_Service (arguments)
 
{   
    OS_CPU_SR  cpu_sr      (1)
    
    
    .
    cpu_sr = get_processor_psw(); (2)
    disable_interrupts();  (3)
    .
    /* Critical section of code */      (4)
    .
    set_processor_psw(cpu_sr);   (5)
    .
}

(1) OS_CPU_SR is a µC/OS-II data type that is declared in the processor specific file OS_CPU.H. When you select this critical section method, OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() always assume the presence of the cpu_sr variable. In other words, if you use this method to protect your own critical sections, you will need to declare a cpu_sr variable in your function.

(2) To enter a critical section, a function provided by the compiler vendor is called to obtain the current state of the PSW (condition code register, processor flags or whatever else this register is called for your processor). I called this function get_processor_psw() for sake of discussion but it will likely have a different name for your compiler.

(3) Another compiler provided function (disable_interrupt()) is called to, of course, disable interrupts.

(4) At this point, the critical code can be execute.

(5) Once the critical section has completed, interrupts can be re-enabled by calling another compiler specific extension that, for sake of discussion, I called set_processor_psw(). The function receives as an argument the previous state of the PSW. It’s assumed that this function will restore the processor PSW to this value.


Because I don’t know what the compiler functions are (there is no standard naming convention), the µC/OS-II macros are used to encapsulate the functionality as follows:

#define OS_ENTER_CRITICAL()    \
  cpu_sr = get_processor_psw(); \
  disable_interrupts();
#define OS_EXIT_CRITICAL()     \
  set_processor_psw(cpu_sr);

Tasks

A task is typically an infinite loop function as shown in Listing 3.2.

Listing - Listing 3.2 A task is an infinite loop
void YourTask (void *pdata)                                            (1)
{
   for (;;) {                                                          (2)
	  /* USER CODE */
	  Call one of uC/OS-II's services:
	  OSFlagPend();
	  OSMboxPend();
	  OSMutexPend();
	  OSQPend();
	  OSSemPend();
	  OSTaskDel(OS_PRIO_SELF);
	  OSTaskSuspend(OS_PRIO_SELF);
	  OSTimeDly();
	  OSTimeDlyHMSM();
	  /* USER CODE */
   }
}

(1) The return type must always be declared void. An argument is passed to your task code when the task first starts executing. Notice that the argument is a pointer to a void. This allows your application to pass just about any kind of data to your task. The pointer is a “universal” vehicle used to pass your task the address of a variable, a structure, or even the address of a function if necessary! It is possible (see Example 1 in Chapter 1) to create many identical tasks, all using the same function (or task body). For example, you could have four asynchronous serial ports that each are managed by their own task. However, the task code is actually identical. Instead of copying the code four times, you can create a task that receives a pointer to a data structure that defines the serial port’s parameters (baud rate, I/O port addresses, interrupt vector number, etc.) as an argument.

(2) You could also use a while (1) statement, if you prefer. A task looks just like any other C function containing a return type and an argument, but it never returns.


Alternatively, the task can delete itself upon completion as shown in Listing 3.3. Note that the task code is not actually deleted; µC/OS-II simply doesn’t know about the task anymore, so the task code will not run. Also, if the task calls OSTaskDel(), the task never returns.

Listing - Listing 3.3 A task that deletes itself when done
void YourTask (void *pdata)
{
   /* USER CODE */
   OSTaskDel(OS_PRIO_SELF);
}


µC/OS-II can manage up to 64 tasks; however, the current version of µC/OS-II uses two tasks for system use. I recommend that you don’t use priorities 0, 1, 2, 3, OS_LOWEST_PRIO-3, OS_LOWEST_PRIO-2, OS_LOWEST_PRIO-1, and OS_LOWEST_PRIO because I may use them in future versions µC/OS-II. However, if you need to keep your application as tight as possible then go ahead and use whatever priorities you need as long as you don’t use OS_LOWEST_PRIO. OS_LOWEST_PRIO is a #define constant defined in the file OS_CFG.H. Therefore, you can have up to 63 of your own application tasks unless you decide to not use the top and bottom four priorities as I recommend. In this case, you would have up to 56 of your own tasks.

Each task must be assigned a unique priority level from 0 to OS_LOWEST_PRIO–2, inclusively. The lower the priority number, the higher the priority of the task. µC/OS-II always executes the highest priority task ready to run. In the current version of µC/OS-II, the task priority number also serves as the task identifier. The priority number (i.e., task identifier) is used by some kernel services such as OSTaskChangePrio() and OSTaskDel().

In order for µC/OS-II to manage your task, you must “create” a task by passing its address along with other arguments to one of two functions: OSTaskCreate() or OSTaskCreateExt(). OSTaskCreateExt() is an extended version of OSTaskCreate() and provides additional features. These two functions are explained in Chapter 4, Task Management.

Task States

Figure 3.2 shows the state transition diagram for tasks under µC/OS-II. At any given time, a task can be in any one of five states.

The TASK DORMANT state corresponds to a task that resides in program space (ROM or RAM) but has not been made available to µC/OS-II. A task is made available to µC/OS-II by calling either OSTaskCreate() or OSTaskCreateExt(). These calls are simply used to tell µC/OS-II the starting address of your task, what priority you want to give to the task being ‘created’, how much stack space will your task use and so on. When a task is created, it is made ready to run and placed in the TASK READY state. Tasks may be created before multitasking starts or dynamically by a running task. If multitasking has started and a task created by another task has a higher priority than its creator, the created task is given control of the CPU immediately. A task can return itself or another task to the dormant state by calling OSTaskDel().

Multitasking is started by calling OSStart(). OSStart() MUST only be called once during startup and starts the highest priority task that has been created during your initialization code. The highest priority task is thus placed in the TASK RUNNING state. Only one task can be running at any given time. A ready task will not run until all higher priority tasks are either placed in the wait state or are deleted.

Figure - Figure 3.2 Task states


The running task may delay itself for a certain amount of time by calling either OSTimeDly() or OSTimeDlyHMSM(). This task would be placed in the TASK WAITING state until the time specified in the call expires. Both of these functions force an immediate context switch to the next highest priority task that is ready to run. The delayed task is made ready to run by OSTimeTick() when the desired time delay expires (see section 3.??, Clock Tick). OSTimeTick() is an internal function to µC/OS-II and thus, you don’t have to actually call this function from your code.

The running task may also need to wait until an event occurs by calling either OSFlagPend(), OSSemPend(), OSMutexPend(), OSMboxPend(), or OSQPend(). If the event did not already occur, the task that calls one of these functions is placed in the TASK WAITING state until the occurrence of the event. When a task pends on an event, the next highest priority task is immediately given control of the CPU. The task is made ready when the event occurs or, when a timeout expires. The occurrence of an event may be signaled by either another task or an ISR.

A running task can always be interrupted, unless the task or µC/OS-II disables interrupts as we have seen. The task thus enters the ISR RUNNING state. When an interrupt occurs, execution of the task is suspended and the ISR takes control of the CPU. The ISR may make one or more tasks ready to run by signaling one or more events. In this case, before returning from the ISR, µC/OS-II determines if the interrupted task is still the highest priority task ready to run. If a higher priority task is made ready to run by the ISR, the new highest priority task is resumed; otherwise, the interrupted task is resumed.

When all tasks are waiting either for events or for time to expire, µC/OS-II executes an internal task called the idle task, OS_TaskIdle().

Task Control Blocks (OS_TCBs)

When a task is created, it is assigned a Task Control Block, OS_TCB (Listing 3.??). A task control block is a data structure that is used by µC/OS-II to maintain the state of a task when it is preempted. When the task regains control of the CPU, the task control block allows the task to resume execution exactly where it left off. All OS_TCBs reside in RAM. You will notice that I organized its fields to allow for data structure packing while maintaining a logical grouping of members.

Listing - Listing 3.4 The µC/OS-II task control block
typedef struct os_tcb {
     OS_STK        *OSTCBStkPtr; 
 
 #if OS_TASK_CREATE_EXT_EN > 0
     void          *OSTCBExtPtr; 
     OS_STK        *OSTCBStkBottom; 
     INT32U         OSTCBStkSize;  
     INT16U         OSTCBOpt;      
     INT16U         OSTCBId;       
 #endif
 
     struct os_tcb *OSTCBNext;     
     struct os_tcb *OSTCBPrev;     
 
 #if OS_TASK_CREATE_EXT_EN > 0u
 #if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
     OS_TLS           OSTCBTLSTbl[OS_TLS_TBL_SIZE];
 #endif
 #endif

 #if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u) || (OS_SEM_EN > 0u) || (OS_MUTEX_EN > 0u)
     OS_EVENT      *OSTCBEventPtr; 
 #endif
 
 #if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
     OS_EVENT     **OSTCBEventMultiPtr;
 #endif
 
 #if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)
     void          *OSTCBMsg;   
 #endif
 
 #if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
 #if OS_TASK_DEL_EN > 0
     OS_FLAG_NODE  *OSTCBFlagNode;  
 #endif    
     OS_FLAGS       OSTCBFlagsRdy;  
 #endif
 
     INT16U         OSTCBDly;       
     INT8U          OSTCBStat;      
     INT8U          OSTCBStatPend;
     INT8U          OSTCBPrio;      
 
     INT8U          OSTCBX;         
     INT8U          OSTCBY;   
     INT8U          OSTCBBitX;      
     INT8U          OSTCBBitY;      
 
 #if OS_TASK_DEL_EN > 0u
     BOOLEAN OSTCBDelReq;    
 #endif
 
#if OS_TASK_PROFILE_EN > 0u
    INT32U           OSTCBCtxSwCtr;         
    INT32U           OSTCBCyclesTot;        
    INT32U           OSTCBCyclesStart;      
    OS_STK          *OSTCBStkBase;          
    INT32U           OSTCBStkUsed;          
#endif
#if OS_TASK_NAME_EN > 0u
    INT8U           *OSTCBTaskName;
#endif

#if OS_TASK_REG_TBL_SIZE > 0u
    INT32U           OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif


 } OS_TCB;


.OSTCBStkPtr

contains a pointer to the current top-of-stack for the task. µC/OS-II allows each task to have its own stack, but just as important, each stack can be any size. Some commercial kernels assume that all stacks are the same size unless you write complex hooks. This limitation wastes RAM when all tasks have different stack requirements because the largest anticipated stack size has to be allocated for all tasks. .OSTCBStkPtr should be the only field in the OS_TCB data structure which is accessed from assembly language code (from the context-switching code). I decided to place .OSTCBStkPtr as the first entry in the structure to make accessing this field easier from assembly language code (it ought to be at offset zero).

.OSTCBExtPtr

is a pointer to a user-definable task control block extension. This allows you or the user of µC/OS-II to extend the task control block without having to change the source code for µC/OS-II. .OSTCBExtPtr is only used by OSTaskCreateExt(), so you need to set OS_TASK_CREATE_EXT_EN in OS_CFG.H to 1 to enable this field. Once enabled, you could use .OSTCBExtPtr to point to a data structure that contains the name of the task, keep track of the execution time of the task, or the number of times a task has been switched-in (see Example 3 in Chapter 1). Notice that I decided to place this pointer immediately after the stack pointer in case you need to access this field from assembly language. This makes calculating the offset from the beginning of the data structure easier.

.OSTCBStkBottom

is a pointer to the bottom of the task’s stack. If the processor’s stack grows from high-to-low memory locations, then .OSTCBStkBottom will point at the lowest valid memory location for the stack. Similarly, if the processor’s stack grows from low-to-high memory locations, then .OSTCBStkBottom will point at the highest valid stack address. .OSTCBStkBottom is used by OSTaskStkChk() to check the size of a task’s stack at run time. This allows you to determine the amount of free stack space available for each stack. Stack checking can only occur if you create a task with OSTaskCreateExt(), so you need to set OS_TASK_CREATE_EXT_EN in OS_CFG.H to 1 to enable this field.

.OSTCBStkSize

holds the size of the stack in number of elements instead of bytes (OS_STK is declared in OS_CPU.H). This means that if a stack contains 1,000 entries and each entry is 32 bits wide, then the actual size of the stack is 4,000 bytes. Similarly, a stack where entries are 16 bits wide would contain 2,000 bytes for the same 1,000 entries. .OSTCBStkSize is used by OSTaskStkChk(). Again, this field is valid only if you set OS_TASK_CREATE_EXT_EN in OS_CFG.H to 1.

.OSTCBOpt

holds “options” that can be passed to OSTaskCreateExt(), so this field is valid only if you set OS_TASK_CREATE_EXT_EN in OS_CFG.H to 1. µC/OS-II currently defines only three options (see uCOS_II.H): OS_TASK_OPT_STK_CHK, OS_TASK_OPT_STK_CLR, and OS_TASK_OPT_SAVE_FP.

OS_TASK_OPT_STK_CHK is used to specify to OSTaskCreateExt() that stack checking is enabled for the task being created. Stack checking is not performed automatically by µC/OS-II because I didn’t want to use valuable of CPU time unless you actually want to do stack checking. Stack checking is performed by your application code by calling OSTaskStkChk() (see Chapter 4, Task Management).

OS_TASK_OPT_STK_CLR indicates that the stack needs to be cleared (i.e. µC/OS-II writes zeros in every location of the stack) when the task is created. The stack only needs to be cleared if you intend to do stack checking. If you do not specify OS_TASK_OPT_STK_CLR and you then create and delete tasks, stack checking will report incorrect stack usage. If you never delete a task once it’s created and your startup code clears all RAM, you can save valuable execution time by NOT specifying this option. Passing OS_TASK_OPT_STK_CLR increases the execution time of OSTaskCreateExt() because it clears the content of the stack. The larger your stack, the longer it takes. Again, stack checking is invoked by your application code and not automatically by µC/OS-II.

OS_TASK_OPT_SAVE_FP tells OSTaskCreateExt() that the task will be doing floating-point computations. If the processor provides hardware-assisted floating-point capability, the floating-point registers need to be saved for the task being created and during a context switch.

.OSTCBId

is used to hold an identifier for the task. This field is currently not used and has only been included for future expansion.

.OSTCBNext and .OSTCBPrev

are used to doubly link OS_TCBs. The forward link (pointed to by .OSTCBNext) chain of OS_TCBs is used by OSTimeTick() to update the .OSTCBDly field for each task. The OS_TCB for each task is linked (using both pointers) when the task is created, and the OS_TCB is removed from the list when the task is deleted. A doubly linked list permits an element in the chain to be quickly inserted or removed.

.OSTCBEventPtr

is a pointer to an event control block and is described later (see Chapter 6, Event Control Blocks).

.OSTCBMsg

is a pointer to a message that is sent to a task. The use of this field is described later (see Chapter 10 and 11).

.OSTCBFlagNode

is a pointer to an event flag node (see Chapter 9, Event Flag Management). This field is only used by OSTaskDel() when we are deleting a task that waits on an event flag group. This field is present in the OS_TCB only when OS_FLAG_EN in OS_CFG.H is set to 1.

.OSTCBFlagsRdy

contains the event flags that made the task ready to run when the task was waiting on an event flag group (see Chapter 9, Event Flag Management). This field is present in the OS_TCB only when OS_FLAG_EN in OS_CFG.H is set to 1.

.OSTCBDly

is used when a task needs to be delayed for a certain number of clock ticks or a task needs to pend for an event to occur with a timeout. In this case, this field contains the number of clock ticks the task is allowed to wait for the event to occur. When this variable is 0, the task is not delayed or has no timeout when waiting for an event.

.OSTCBStat

contains the state of the task. When .OSTCBStat is 0, the task is ready to run. Other values can be assigned to .OSTCBStat, and these values are described in uCOS_II.H (see OS_STAT_???).

.OSTCBPrio

contains the task priority. A high-priority task has a low .OSTCBPrio value (i.e., the lower the number, the higher the actual priority).

.OSTCBX, .OSTCBY, .OSTCBBitX, and .OSTCBBitY

are used to accelerate the process of making a task ready to run or to make a task wait for an event (to avoid computing these values at run time). The values for these fields are computed when the task is created or when the task’s priority is changed. The values are obtained as shown in Listing 3.5.

Listing - Listing 3.5 Calculating
OSTCBY      = priority >> 3;
OSTCBBitY   = OSMapTbl[priority >> 3];
OSTCBX      = priority & 0x07;
OSTCBBitX   = OSMapTbl[priority & 0x07];


.OSTCBDelReq

is a boolean used to indicate whether or not a task requested that the current task be deleted. The use of this field is described later (see Chapter 4, Task Management). This field is present in the OS_TCB only when OS_TASK_DEL_EN in OS_CFG.H is set to 1.

You probably noticed that some of the fields in the OS_TCB structured are wrapped with conditional compilation statements. This is done to allow you to reduce the amount of RAM needed by µC/OS-II if you don’t need all the features that µC/OS-II provides.

The maximum number of tasks (OS_MAX_TASKS) that an application can have is specified in OS_CFG.H and determines the number of OS_TCBs allocated for your application. You can reduce the amount of RAM needed by setting OS_MAX_TASKS to the actual number of tasks needed in your application. All OS_TCBs are placed in OSTCBTbl[]. Note that µC/OS-II allocates OS_N_SYS_TASKS (see uCOS_II.H) extra OS_TCBs for internal use. Currently, one is used for the idle task, and another is used for the statistic task (if OS_TASK_STAT_EN in OS_CFG.H is set to 1). When µC/OS-II is initialized, all OS_TCBs in the table are linked in a singly-linked list of free OS_TCBs, as shown in Figure 3.3. When a task is created, the OS_TCB pointed to by OSTCBFreeList is assigned to the task, and OSTCBFreeList is adjusted to point to the next OS_TCB in the chain. When a task is deleted, its OS_TCB is returned to the list of free OS_TCBs.

Figure - Figure 3.3 List of free OS_TCBs


An OS_TCB is initialized by the function OS_TCBInit() (see Listing 3.6) when a task is created. OS_TCBInit() is called by either OSTaskCreate() or OSTaskCreateExt() (see Chapter 4, Task Management). OS_TCBInit() receives seven arguments:

prio

is the task priority,

ptos

is a pointer to the top of stack once the stack frame has been built by OSTaskStkInit() (will be described in Chapter 13, Porting µC/OS-II) and is stored in the .OSTCBStkPtr field of the OS_TCB.

pbos

is a pointer to the stack bottom and is stored in the .OSTCBStkBottom field of the OS_TCB.

id

is the task identifier and is saved in the .OSTCBId field.

stk_size

is the total size of the stack and is saved in the .OSTCBStkSize field of the OS_TCB.

pext

is the value to place in the .OSTCBExtPtr field of the OS_TCB.

opt

is the OS_TCB options and is saved in the .OSTCBOpt field.

Listing - Listing 3.6
INT8U OS_TCBInit (INT8U    prio,     
                  OS_STK  *ptos,   
                  OS_STK  *pbos, 
                  INT16U   id,
                  INT32U   stk_size, 
                  void    *pext,   
                  INT16U   opt)
{
#if OS_CRITICAL_METHOD == 3         
    OS_CPU_SR  cpu_sr;
#endif    
    OS_TCB     *ptcb;
#if OS_TASK_REG_TBL_SIZE > 0u
    INT8U      i;
#endif
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
    INT8U      j;
#endif
#endif

 
 
    OS_ENTER_CRITICAL();
    ptcb = OSTCBFreeList;                                                  (1)
    if (ptcb != (OS_TCB *)0) {                                             (2)
        OSTCBFreeList        = ptcb->OSTCBNext;
        OS_EXIT_CRITICAL();
        ptcb->OSTCBStkPtr    = ptos;                                       (3)
        ptcb->OSTCBPrio      = (INT8U)prio;
        ptcb->OSTCBStat      = OS_STAT_RDY;
        ptcb->OSTCBStatPend  = OS_STAT_PEND_OK; 
        ptcb->OSTCBDly       = 0;
#if OS_TASK_CREATE_EXT_EN > 0u
        ptcb->OSTCBExtPtr    = pext;                                       (4)
        ptcb->OSTCBStkSize   = stk_size;
        ptcb->OSTCBStkBottom = pbos;
        ptcb->OSTCBOpt       = opt;
        ptcb->OSTCBId        = id;
#else
        pext                 = pext;
        stk_size             = stk_size;
        pbos                 = pbos;
        opt                  = opt;
        id                   = id;
#endif
 
#if OS_TASK_DEL_EN > 0u
        ptcb->OSTCBDelReq    = OS_ERR_NONE;                                 (5)
#endif
 
#if OS_LOWEST_PRIO <= 63u                                         
        ptcb->OSTCBY             = (INT8U)(prio >> 3u);                     (6)
        ptcb->OSTCBX             = (INT8U)(prio & 0x07u);
#else                                                             
        ptcb->OSTCBY             = (INT8U)((INT8U)(prio >> 4u) & 0xFFu);
        ptcb->OSTCBX             = (INT8U) (prio & 0x0Fu);
#endif
                                                                  
        ptcb->OSTCBBitY          = (OS_PRIO)(1uL << ptcb->OSTCBY);
        ptcb->OSTCBBitX          = (OS_PRIO)(1uL << ptcb->OSTCBX);

 
#if     OS_EVENT_EN > 0u
        ptcb->OSTCBEventPtr      = (OS_EVENT *)0;                           (7)
#if (OS_EVENT_MULTI_EN > 0u)
        ptcb->OSTCBEventMultiPtr = (OS_EVENT **)0;         
#endif

#endif
 
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u) && (OS_TASK_DEL_EN > 0u)
        ptcb->OSTCBFlagNode  = (OS_FLAG_NODE *)0;                           (8)
#endif
 
#if     OS_MBOX_EN || (OS_Q_EN && (OS_MAX_QS >= 2))
        ptcb->OSTCBMsg       = (void *)0;
#endif
 
#if OS_TASK_PROFILE_EN > 0u
        ptcb->OSTCBCtxSwCtr      = 0uL;                    
        ptcb->OSTCBCyclesStart   = 0uL;
        ptcb->OSTCBCyclesTot     = 0uL;
        ptcb->OSTCBStkBase       = (OS_STK *)0;
        ptcb->OSTCBStkUsed       = 0uL;
#endif
#if OS_TASK_NAME_EN > 0u
        ptcb->OSTCBTaskName      = (INT8U *)(void *)"?";
#endif
#if OS_TASK_REG_TBL_SIZE > 0u                              
        for (i = 0u; i < OS_TASK_REG_TBL_SIZE; i++) {
            ptcb->OSTCBRegTbl[i] = 0u;
        }

#endif
        OSTCBInitHook(ptcb);                                                 (9)
 
        OS_ENTER_CRITICAL();
        OSTCBPrioTbl[prio] = ptcb;                                          (10)
        OS_EXIT_CRITICAL();

        OSTaskCreateHook(ptcb);                                             (11)
 
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
        for (j = 0u; j < OS_TLS_TBL_SIZE; j++) {
            ptcb->OSTCBTLSTbl[j] = (OS_TLS)0;
        }
        OS_TLS_TaskCreate(ptcb);                           
#endif
#endif

        OS_ENTER_CRITICAL();                                                (12)
        ptcb->OSTCBNext      = OSTCBList;
        ptcb->OSTCBPrev      = (OS_TCB *)0;
        if (OSTCBList != (OS_TCB *)0) {
            OSTCBList->OSTCBPrev = ptcb;
        }
        OSTCBList               = ptcb;
        OSRdyGrp               |= ptcb->OSTCBBitY;                          (13)
        OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
        OSTaskCtr++;                                       
        OS_EXIT_CRITICAL();
        return (OS_ERR_NONE);                                               (14)
    }
    OS_EXIT_CRITICAL();
    return (OS_NO_MORE_TCB);
}

(1) OS_TCBInit() first tries to obtain an OS_TCB from the OS_TCB pool.

(2) & (3) If the pool contains a free OS_TCB, it is initialized. Note that once an OS_TCB is allocated, OS_TCBInit() can re-enable interrupts because at this point the creator of the task owns the OS_TCB and it cannot be corrupted by another concurrent task creation. OS_TCBInit() can thus proceed to initialize some of the OS_TCB fields with interrupts enabled.

(4) If you enabled code generation for OSTaskCreateExt() (OS_TASK_CREATE_EXT_EN is set to 1 in OS_CFG.H) then additional fields in OS_TCB are filled-in.

(5) The presence of the flag .OSTCBDelReq in OS_TCB depends on whether OS_TASK_DEL_EN has been enabled (see OS_CFG.H). In other words, if you never intend to delete tasks, you can save yourself the storage area of a BOOLEAN in every single OS_TCB.

(6) In order to save a bit of processing time during scheduling, OS_TCBInit() precalculates some fields. I decided to exchange execution time in favor of data space storage.

(7) If you don’t intend to use any semaphores, mutexes, message mailboxes and message queues in your application then the field .OSTCBEventPtr in the OS_TCB would not be present.

(8) If you enabled event flags (i.e. you set OS_FLAGS_EN to 1 in OS_CFG.H) then the pointer to an event flag node is intitialized to point to nothing because the task is not waiting for an event flag, it’s only being created.

(9) In V2.04, I added a call to a function that can be defined in the processor’s port file – OSTCBInitHook(). This allows you to add extensions to the OS_TCB. For example, you could initialize and store the contents of floating-point registers, MMU registers, or anything else that can be associated with a task. However, you would typically store this additional information in memory that would be allocated by your application. Note that interrupts are enabled when OS_TCBInit() calls OSTCBInitHook().

(10) OS_TCBInit() disables interrupts when it needs to insert the OS_TCB into the doubly linked list of tasks that have been created. 

(11) OS_TCBInit() then calls OSTaskCreateHook(), which is a user-specified function that allows you to extend the functionality of OSTaskCreate() or OSTaskCreateExt()OSTaskCreateHook() can be declared either in OS_CPU_C.C (if OS_CPU_HOOKS_EN is set to 1) or elsewhere (if OS_CPU_HOOKS_EN is set to 0). Note that interrupts are enabled when OS_TCBInit() calls OSTaskCreateHook().

You should note that I could have called only one of the two hook functions: OSTCBInitHook() or OSTaskCreateHook(). The reason there are two functions is to allow you to group (i.e. encapsulate) items that are tied with the OS_TCB in OSTCBInitHook() and other task related initialization in OSTaskCreateHook().

(12) The list starts at OSTCBList, and the OS_TCB of a new task is always inserted at the beginning of the list.

(13) & (14) Finally, the task is made ready to run, and OS_TCBInit() returns to its caller [OSTaskCreate() or OSTaskCreateExt()] with a code indicating that an OS_TCB has been allocated and initialized.

Ready List

Each task is assigned a unique priority level between 0 and OS_LOWEST_PRIO, inclusive (see OS_CFG.H). Task priority OS_LOWEST_PRIO is always assigned to the idle task when µC/OS-II is initialized. Note that OS_MAX_TASKS and OS_LOWEST_PRIO are unrelated. You can have only 10 tasks in an application while still having 32 priority levels (if you set OS_LOWEST_PRIO to 31).

Each task that is ready to run is placed in a ready list consisting of two variables, OSRdyGrp and OSRdyTbl[]. Task priorities are grouped (eight tasks per group) in OSRdyGrp. Each bit in OSRdyGrp indicates when a task in a group is ready to run. When a task is ready to run, it also sets its corresponding bit in the ready table, OSRdyTbl[]. The size of OSRdyTbl[] depends on OS_LOWEST_PRIO (see uCOS_II.H). This allows you to reduce the amount of RAM (data space) needed by µC/OS-II when your application requires few task priorities.

To determine which priority (and thus which task) will run next, the scheduler in µC/OS-II determines the lowest priority number that has its bit set in OSRdyTbl[]. The relationship between OSRdyGrp and OSRdyTbl[] is shown in Figure 3.4 and is given by the following rules.

Bit 0 in OSRdyGrp is 1 when any bit in OSRdyTbl[0] is 1.

Bit 1 in OSRdyGrp is 1 when any bit in OSRdyTbl[1] is 1.

Bit 2 in OSRdyGrp is 1 when any bit in OSRdyTbl[2] is 1.

Bit 3 in OSRdyGrp is 1 when any bit in OSRdyTbl[3] is 1.

Bit 4 in OSRdyGrp is 1 when any bit in OSRdyTbl[4] is 1.

Bit 5 in OSRdyGrp is 1 when any bit in OSRdyTbl[5] is 1.

Bit 6 in OSRdyGrp is 1 when any bit in OSRdyTbl[6] is 1.

Bit 7 in OSRdyGrp is 1 when any bit in OSRdyTbl[7] is 1.

The code in Listing 3.7 is used to place a task in the ready list. prio is the task’s priority.

Listing - Listing 3.7 Making a task ready to run
OSRdyGrp            |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];


As you can see from Figure 3.4, the lower three bits of the task’s priority are used to determine the bit position in OSRdyTbl[] , and the next three most significant bits are used to determine the index into OSRdyTbl[] . Note that OSMapTbl[] (see OS_CORE.C ) is in ROM and is used to equate an index from 0 to 7 to a bit mask, as shown in Table 3.1.

Table 3.1 Contents of OSMapTbl[].

Index

Bit Mask (Binary)

0

00000001

1

00000010

2

00000100

3

00001000

4

00010000

5

00100000

6

01000000

7

10000000

Figure - Figure 3.4 The µC/OS-II ready list

A task is removed from the ready list by reversing the process using the code in Listing 3.8.

Listing - Listing 3.8 Removing a task from the ready list
if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
    OSRdyGrp &= ~OSMapTbl[prio >> 3];


This code clears the ready bit of the task in OSRdyTbl[] and clears the bit in OSRdyGrp only if all tasks in a group are not ready to run; that is, all bits in OSRdyTbl[prio >> 3] are 0. Another table lookup is performed, rather than scanning through the table starting with OSRdyTbl[0] , to find the highest priority task ready to run. OSUnMapTbl[256] is a priority resolution table (see OS_CORE.C ). Eight bits represent when tasks are ready in a group. The least significant bit has the highest priority. Using this byte to index OSUnMapTbl[] returns the bit position of the highest priority bit set — a number between 0 and 7. Determining the priority of the highest priority task ready to run is accomplished with the code in Listing 3.9.

Listing - Listing 3.9 Finding the highest priority task ready to run
y    = OSUnMapTbl[OSRdyGrp];    /* Determine Y position in OSRdyTbl[]  */
x    = OSUnMapTbl[OSRdyTbl[y]]; /* Determine X position in OSRdyTbl[Y] */
prio = (y << 3) + x;


For example, as shown in Figure 3.5, if OSRdyGrp contains 01101000 (binary) or 0x68, then the table lookup OSUnMapTbl[OSRdyGrp] yields a value of 3, which corresponds to bit 3 in OSRdyGrp . Note that bit positions are assumed to start on the right with bit 0 being the rightmost bit. Similarly, if OSRdyTbl[3] contains 11100100 (binary) or 0xE4, then OSUnMapTbl[OSRdyTbl[3]] results in a value of 2 (bit 2). The task priority (prio) is then 26 (i.e. 3 x 8 + 2). Getting a pointer to the OS_TCB for the corresponding task is done by indexing into OSTCBPrioTbl[] using the task’s priority.

Figure - Figure 3.5 Finding the highest priority task ready to run


Task Scheduling

µC/OS-II always executes the highest priority task ready to run. The determination of which task has the highest priority, and thus which task will be next to run, is determined by the scheduler. Task-level scheduling is performed by OS_Sched(). ISR-level scheduling is handled by another function [OSIntExit()] described later. The code for OS_Sched() is shown in Listing 3.10. µC/OS-II task-scheduling time is constant irrespective of the number of tasks created in an application.

Listing - Listing 3.10 Task scheduler
void  OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3
    OS_CPU_SR  cpu_sr;
#endif    
    INT8U      y;
 
 
    OS_ENTER_CRITICAL();
    if ((OSIntNesting == 0u) && (OSLockNesting == 0u)) {                   (1)
        y             = OSUnMapTbl[OSRdyGrp];                              (2)
        OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
        if (OSPrioHighRdy != OSPrioCur) {                                  (3)
            OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];                    (4)
#if OS_TASK_PROFILE_EN > 0u
            OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task      */
#endif
            OSCtxSwCtr++;                                                  (5)
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
            OS_TLS_TaskSw();
#endif
#endif
            OS_TASK_SW();                                                  (6)
        }
    }
    OS_EXIT_CRITICAL();
}

(1) OS_Sched() exits if called from an ISR (i.e., OSIntNesting > 0) or if scheduling has been disabled because your application called OSSchedLock() at least once (i.e., OSLockNesting > 0).

(2) If OS_Sched() is not called from an ISR and the scheduler is enabled, then OS_Sched() determines the priority of the highest priority task that is ready to run. A task that is ready to run has its corresponding bit set in OSRdyTbl[].

(3) Once the highest priority task has been found, OS_Sched() verifies that the highest priority task is not the current task. This is done to avoid an unnecessary context switch which would be time consuming. Note that µC/OS (V1.xx) used to obtain OSTCBHighRdy and compared it with OSTCBCur. On 8- and some 16-bit processors, this operation was relatively slow because a comparison was made of pointers instead of 8-bit integers as it is now done in µC/OS-II. Also, there is no point in looking up OSTCBHighRdy in OSTCBPrioTbl[] (see L3.10(4)) unless you actually need to do a context switch. The combination of comparing 8-bit values instead of pointers and looking up OSTCBHighRdy only when needed should make µC/OS-II faster than µC/OS on 8- and some 16-bit processors.

(4) To perform a context switch, OSTCBHighRdy must point to the OS_TCB of the highest priority task, which is done by indexing into OSTCBPrioTbl[] using OSPrioHighRdy.

(5) Next, the statistic counter OSCtxSwCtr (a 32-bit variable) is incremented to keep track of the number of context switches. This counter serves no other purpose except that it allows you to determine the number of context switches in one second. Of course, do to this, you’d have to save OSCtxSwCtr in another variable (ex. OSCtxSwCtrPerSec) every second and then clear OSCtxSwCtr.

(6) Finally, the macro OS_TASK_SW() is invoked to do the actual context switch.


A context switch simply consists of saving the processor registers on the stack of the task being suspended and restoring the registers of the higher priority task from its stack. In µC/OS-II, the stack frame for a ready task always looks as if an interrupt has just occurred and all processor registers were saved onto it. In other words, all that µC/OS-II has to do to run a ready task is restore all processor registers from the task’s stack and execute a return from interrupt. To switch context, you would implement OS_TASK_SW() so that you simulate an interrupt. Most processors provide either software interrupt or TRAP instructions to accomplish this. The interrupt service routine (ISR) or trap handler (also called the exception handler) must vector to the assembly language function OSCtxSw(). OSCtxSw() expects to have OSTCBHighRdy point to the OS_TCB of the task to be switched-in and OSTCBCur point to the OS_TCB of the task being suspended. Refer to Chapter 13, Porting µC/OS-II, for additional details on OSCtxSw(). For now, you only need to know that OS_TASK_SW() will suspends execution of the current task and allows the CPU to resume execution of the more important task.

All of the code in OS_Sched() is considered a critical section. Interrupts are disabled to prevent ISRs from setting the ready bit of one or more tasks during the process of finding the highest priority task ready to run. Note that OS_Sched() could be written entirely in assembly language to reduce scheduling time. OS_Sched() was written in C for readability and portability and to minimize assembly language.

Task Level Context Switch, OS_TASK_SW()

As we discussed in the previous section, once the scheduler has determined that a more important task needs to run, OS_TASK_SW() is called to perform a context switch. The context of a task is generally the contents of all of the CPU registers. The context switch code simply needs to save the register values of the task being preempted and load into the CPU the values of the registers for the task to resume.

OS_TASK_SW() is a macro that ‘normally’ invokes a microprocessor software interrupt because µC/OS-II assumes that context switching will be done by interrupt level code. What µC/OS-II thus needs is a processor instruction that behaves just like a hardware interrupt (thus the name software interrupt). A macro is used to make µC/OS-II portable across multiple platforms by encapsulating the actual processor specific software interrupt mechanism. You will learn more about how to implement OS_TASK_SW() in Chapter 13, Porting µC/OS-II.

Figure 3.6 shows the state of some µC/OS-II variables and data structures just prior to calling OS_TASK_SW(). For sake of discussion, I ‘created’ a fictituous CPU containing seven registers:

  • A Stack Pointer (SP)
  • A Program Counter (PC)
  • A Processor Status Word (PSW)
  • Four general purpose registers (R1, R2, R3 and R4)

Figure - Figure 3.6 µC/OS-II structures when OS_TASK_SW() is called

(1) OSTCBCur points to the OS_TCB of the task being suspended (the Low Priority Task).

(2) The CPU’s stack pointer (SP register) points to the current top-of-stack of the task being preempted.

(3) OSTCBHighRdy points to the OS_TCB of the task that will execute after completing the context switch.

(4) The .OSTCBStkPtr field in the OS_TCB points to the top-of-stack of the task to resume.

(5) The stack of the task to resume contains the desired register values to load into the CPU. These values could have been saved by a previous context switch as we will see shortly. For the time being, let’s simply assume that they have the desired values.


Figure 3.7 shows the state of the variables and data structures after calling OS_TASK_SW() and after saving the context of the task to suspend.

Figure - Figure 3.7 Saving the current task’s context

(1) Calling OS_TASK_SW() invokes the software interrupt instruction which forces the processor to save the current value of the PSW and the PC onto the current task’s stack. The processor then ‘vectors’ to the software interrupt handler which will be responsible to complete the remaining steps of the context switch.

(2) The software interrupt handler starts by saving the general purpose registers R1, R2, R3 and R4 in this order.

(3) The stack pointer register is then saved into the current task’s OS_TCB. At this point, both the CPU’s SP register and OSTCBCur->OSTCBStkPtr are pointing to the same location into the current task’s stack.


Figure 3.8 shows the state of the variables and data structures after executing the last part of the context switch code.

Figure - Figure 3.8 Resuming the current task

(1) Because the new ‘current’ task will now be the task being resumed, the context switch code copies OSTCBHighRdy to OSTCBCur.

(2) The stack pointer of the task to resume is extracted from the OS_TCB (from OSTCBHighRdy->OSTCBStkPtr) and loaded into the CPU’s SP register. At this point, the SP register point at the stack location containing the value of register R4.

(3) The general purpose registers are popped from the stack in the reverse order (R4, R3, R2 and R1).

(4) The PC and PSW registers are loaded back into the CPU by executing a return from interrupt instruction. Because the PC is changed, code execution resumes where the PC is pointing to, which happens to be in the new task’s code.


The pseudo code for the context switch is shown in Listing 3.11. OSCtxSw() is generally written in assembly language because most C compilers cannot manipulate CPU registers directly from C. In Chapter 14, 80x86 Large Model Port, we will see how OSCtxSw() as well as other µC/OS-II functions look on a real processor, the Intel 80x86.

Listing - Listing 3.11 Context Switch pseudo code
void  OSCtxSw (void)
{
    PUSH R1, R2, R3 and R4 onto the current stack;                 See F3.6(2)
    OSTCBCur->OSTCBStkPtr = SP;                                    See F3.6(3)
    OSTCBCur              = OSTCBHighRdy;                          See F3.7(1)
    SP                    = OSTCBHighRdy->OSTCBStkPtr;             See F3.7(2)
    POP R4, R3, R2 and R1 from the new stack;                      See F3.7(3)
    Execute a return from interrupt instruction;                   See F3.7(4)
}


Locking and Unlocking the Scheduler

The OSSchedLock() function (Listing 3.12) is used to prevent task rescheduling until its counterpart, OSSchedUnlock() (Listing 3.13), is called. The task that calls OSSchedLock() keeps control of the CPU even though other higher priority tasks are ready to run. Interrupts, however, are still recognized and serviced (assuming interrupts are enabled). OSSchedLock() and OSSchedUnlock() must be used in pairs. The variable OSLockNesting keeps track of the number of times OSSchedLock() has been called. This allows nested functions to contain critical code that other tasks cannot access. µC/OS-II allows nesting up to 255 levels deep. Scheduling is re-enabled when OSLockNesting is 0. OSSchedLock() and OSSchedUnlock() must be used with caution because they affect the normal management of tasks by µC/OS-II.

Listing - Listing 3.12 Locking the scheduler
void  OSSchedLock (void)
{
#if OS_CRITICAL_METHOD == 3                      
    OS_CPU_SR  cpu_sr;
#endif    
    
    
    if (OSRunning == OS_TRUE) {                                            (1)
        OS_ENTER_CRITICAL();
        if (OSIntNesting == 0u) {                
            if (OSLockNesting < 255u) {                                    (2)
                OSLockNesting++;                     
            }
        }
        OS_EXIT_CRITICAL();
    }
}

(1) It only makes sense to lock the scheduler if multitasking has started (i.e. OSStart() was called).

(2) Before incrementing OSLockNesting, we need to make sure that we have not exceeded the allowable number of nesting levels.


After calling OSSchedLock(), your application must not make any system calls that suspend execution of the current task; that is, your application cannot call OSFlagPend(), OSMboxPend(), OSMutexPend(), OSQPend(), OSSemPend(), OSTaskSuspend(OS_PRIO_SELF), OSTimeDly(), or OSTimeDlyHMSM() until OSLockNesting returns to 0 because OSSchedLock() prevents other tasks from running and thus your system will lockup.

You may want to disable the scheduler when a low-priority task needs to post messages to multiple mailboxes, queues, or semaphores (see Chapter 6, Intertask Communication & Synchronization) and you don’t want a higher priority task to take control until all mailboxes, queues, and semaphores have been posted to.