Task Management Internals
Task States
From a µC/OS-III user point of view, a task can be in any one of five states as shown in the figure below. Internally, µC/OS-III does not need to keep track of the dormant state and the other states are tracked slightly differently. The actual µC/OS-III states will be discussed after a discussion on task states from the user’s point of view. The figure below also shows which µC/OS-III functions are used to move from one state to another. The diagram is actually simplified as state transitions are a bit more complicated than this.
(1) The Dormant state corresponds to a task that resides in memory but has not been made available to µC/OS-III.
A task is made available to µC/OS-III by calling a function to create the task, OSTaskCreate(). The task code actually resides in code space but µC/OS-III needs to be informed about it.
When it is no longer necessary for µC/OS-III to manage a task, your code can call the task delete function, OSTaskDel(). OSTaskDel() does not actually delete the code of the task, it is simply not eligible to access the CPU.
(2) A task is in the Ready state when it is ready-to-run. There can be any number of tasks ready and µC/OS-III keeps track of all ready tasks in a ready list (discussed later). This list is sorted by priority.
(3) The most important ready-to-run task is placed in the Running state. On a single CPU, only one task can be running at any given time.
The task selected to run on the CPU is switched in by µC/OS-III when the application code calls OSStart(), or when µC/OS-III calls either OSIntExit() or OS_TASK_SW().
As previously discussed, tasks must wait for an event to occur. A task waits for an event by calling one of the functions that brings the task to the pending state if the event has not occurred.
(4) Tasks in the Pending state are placed in a special list called a pend-list (or wait list) associated with the event the task is waiting for. When waiting for the event to occur, the task does not consume CPU time. When the event occurs, the task is placed back into the ready list and µC/OS-III decides whether the newly readied task is the most important ready-to-run task. If this is the case, the currently running task will be preempted (placed back in the ready list) and the newly readied task is given control of the CPU. In other words, the newly readied task will run immediately if it is the most important task.
Note that the OSTaskSuspend() function unconditionally blocks a task and this task will not actually wait for an event to occur but in fact, waits until another task calls OSTaskResume() to make the task ready-to-run.
(5) Assuming that CPU interrupts are enabled, an interrupting device will suspend execution of a task and execute an Interrupt Service Routine (ISR). ISRs are typically events that tasks wait for. Generally speaking, an ISR should simply notify a task that an event occurred and let the task process the event. ISRs should be as short as possible and most of the work of handling the interrupting devices should be done at the task level where it can be managed by µC/OS-III. ISRs are only allowed to make “Post” calls (i.e., OSFlagPost(), OSQPost(), OSSemPost(), OSTaskQPost() and OSTaskSemPost()). The only post call not allowed to be made from an ISR is OSMutexPost() since mutexes, as will be addressed later, are assumed to be services that are only accessible at the task level.
As the state diagram indicates, an interrupt can interrupt another interrupt. This is called interrupt nesting and most processors allow this. However, interrupt nesting easily leads to stack overflow if not managed properly.
Internally, µC/OS-III keeps track of task states using the state machine shown in the figure below. The task state is actually maintained in a variable that is part of a data structure associated with each task, the task’s TCB. The task state diagram was referenced throughout the design of µC/OS-III when implementing most of µC/OS-III’s services. The number in parentheses is the state number of the task and thus, a task can be in any one of eight (8) states (see os.h, OS_TASK_STATE_??? ).
Note that the diagram does not keep track of a dormant task, as a dormant task is not known to µC/OS-III. Also, interrupts and interrupt nesting is tracked differently as will be explained further in the text.
This state diagram should be quite useful to understand how to use several functions and their impact on the state of tasks. In fact, I’d highly recommend that the reader bookmark the page of the diagram.
(1) A task is in State 0 when a task is ready-to-run. Every task “wants” to be ready-to-run as that is the only way it gets to perform their duties.
(2) A task can decide to wait for time to expire by calling either OSTimeDly() or OSTimeDlyHMSM(). When the time expires or the delay is cancelled (by calling OSTimeDlyResume()), the task returns to the ready state.
(3) A task can wait for an event to occur by calling one of the pend (i.e., wait) functions (OSFlagPend(), OSMutexPend(), OSQPend(), OSSemPend(), OSTaskQPend(), or OSTaskSemPend()), and specify to wait forever for the event to occur. The pend terminates when the event occurs (i.e., a task or an ISR performs a “post”), the awaited object is deleted or, another task decides to abort the pend.
(4) A task can wait for an event to occur as indicated, but specify that it is willing to wait a certain amount of time for the event to occur. If the event is not posted within that time, the task is readied, then the task is notified that a timeout occurred. Again, the pend terminates when the event occurs (i.e., a task or an ISR performs a “post”), the object awaited is deleted or, another task decides to abort the pend.
(5) A task can suspend itself or another task by calling OSTaskSuspend(). The only way the task is allowed to resume execution is by calling OSTaskResume(). Suspending a task means that a task will not be able to run on the CPU until it is resumed. If a task suspends itself then it must be resumed by another task.
(6) A delayed task can also be suspended by another task. In this case, the effect is additive. In other words, the delay must complete (or be resumed by OSTimeDlyResume()) and the suspension must be removed (by another task which would call OSTaskResume()) in order for the task to be able to run.
(7) A task waiting on an event to occur may be suspended by another task. Again, the effect is additive. The event must occur and the suspension removed (by another task) in order for the task to be able to run. Of course, if the object that the task is pending on is deleted or, the pend is aborted by another task, then one of the above two condition is removed. The suspension, however, must be explicitly removed.
(8) A task can wait for an event, but only for a certain amount of time, and the task could also be suspended by another task. As one might expect, the suspension must be removed by another task (or the same task that suspended it in the first place), and the event needs to either occur or timeout while waiting for the event.
Task Control Blocks
A task control block (TCB) is a data structure used by kernels to maintain information about a task. Each task requires its own TCB and, for µC/OS-III, the user assigns the TCB in user memory space (RAM). The address of the task’s TCB is provided to µC/OS-III when calling task-related services (i.e., OSTask???() functions). The task control block data structure is declared in os.h as shown in the listing below. Note that the fields are actually commented in os.h, and some of the fields are conditionally compiled based on whether or not certain features are desired. Both are not shown here for clarity.
Also, it is important to note that even when the user understands what the different fields of the OS_TCB do, the application code must never directly access these (especially change them). In other words, OS_TCB fields must only be accessed by µC/OS-III and not the code.
Listing - OS_TCB Data Structure
struct os_tcb {
CPU_STK *StkPtr;
void *ExtPtr;
CPU_CHAR *NamePtr;
CPU_STK *StkLimitPtr;
OS_TCB *NextPtr;
OS_TCB *PrevPtr;
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_LIST *TickListPtr;
CPU_STK *StkBasePtr;
OS_TLS TLS_Tbl[OS_CFG_TLS_TBL_SIZE]
OS_TASK_PTR TaskEntryAddr;
void *TaskEntryArg;
OS_TCB *PendNextPtr;
OS_TCB *PendPrevPtr;
OS_PEND_OBJ *PendObjPtr;
OS_STATE PendOn;
OS_STATUS PendStatus;
OS_STATE TaskState;
OS_PRIO Prio;
OS_PRIO BasePrio;
OS_MUTEX *MutexGrpHeadPtr;
CPU_STK_SIZE StkSize;
OS_OPT Opt;
CPU_TS TS;
CPU_INT08U SemID;
OS_SEM_CTR SemCtr;
OS_TICK TickRemain;
OS_TICK TickCtrPrev;
OS_TICK TimeQuanta;
OS_TICK TimeQuantaCtr;
void *MsgPtr;
OS_MSG_SIZE MsgSize;
OS_MSG_Q MsgQ;
CPU_TS MsgQPendTime;
CPU_TS MsgQPendTimeMax;
OS_REG RegTbl[OS_TASK_REG_TBL_SIZE];
OS_FLAGS FlagsPend;
OS_FLAGS FlagsRdy;
OS_OPT FlagsOpt;
OS_MON_DATA MonData;
OS_NESTING_CTR SuspendCtr;
OS_CPU_USAGE CPUUsage;
OS_CPU_USAGE CPUUsageMax;
OS_CTX_SW_CTR CtxSwCtr;
CPU_TS CyclesDelta;
CPU_TS CyclesStart;
OS_CYCLES CyclesTotal;
OS_CYCLES CyclesTotalPrev;
CPU_TS SemPendTime;
CPU_TS SemPendTimeMax;
CPU_STK_SIZE StkUsed;
CPU_STK_SIZE StkFree;
CPU_TS IntDisTimeMax;
CPU SchedLockTimeMax;
OS_TCB DbgPrevPtr;
OS_TCB DbgNextPtr;
CPU_CHAR DbgNamePtr;
CPU_INT08U TaskID;
};
.StkPtr
This field contains a pointer to the current top-of-stack for the task. µC/OS-III allows each task to have its own stack and each stack can be any size. .StkPtr should be the only field in the OS_TCB data structure accessed from assembly language code (for the context-switching code). This field is therefore placed as the first entry in the structure making access easier from assembly language code (it will be at offset zero in the data structure).
.ExtPtr
This field contains a pointer to a user-definable data area used to extend the TCB as needed. This pointer is provided as an argument passed in OSTaskCreate(). This pointer is easily accessible from assembly language since it always follows the .StkPtr. .ExtPtr can be used to add storage for saving the context of a FPU (Floating-Point Unit) if the processor you are using has a FPU.
.NamePtr
This pointer allows a name (an ASCII string) to be assigned to each task. Having a name is useful when debugging, since it is user friendly compared to displaying the address of the OS_TCB. Storage for the ASCII string is assumed to be in user space, either in code memory (ASCII string declared as a const) or in RAM.
.StkLimitPtr
The field contains a pointer to a location in the task’s stack to set a watermark limit for stack growth and is determined from the value of the “stk_limit” argument passed to OSTaskCreate(). Some processors have special registers that automatically check the value of the stack pointer at run-time to ensure that the stack does not overflow. .StkLimitPtr may be used to set this register during a context switch. Alternatively, if the processor does not have such a register, this can be “simulated” in software. However, this is not as reliable as a hardware solution. If this feature is not used then you can set the value of “stk_limit” can be set to 0 when calling OSTaskCreate(). See also Detecting Task Stack Overflows).
.NextPtr and .PrevPtr
These pointers are used to doubly link OS_TCBs in the ready list. A doubly linked list allows OS_TCBs to be quickly inserted and removed from the list.
.TickNextPtr and .TickPrevPtr
These pointers are used to doubly link OS_TCBs in the list of tasks waiting for time to expire or to timeout from pend calls. Again, a doubly linked list allows OS_TCBs to be quickly inserted and removed from the list.
.TickListPtr
This pointer points to one of two tick lists: either the list of tasks delayed waiting for time to expire (OSTickListDly) or tasks pending on an object with a timeout (OSTickListTimeout). This field is used to easily remove the OS_TCB from the list.
.StkBasePtr
This field points to the base address of the task’s stack. The stack base is typically the lowest address in memory where the stack for the task resides. A task stack is declared as follows:
CPU_STK MyTaskStk[???];
CPU_STK is the data type you must use to declare task stacks and ??? is the size of the stack associated with the task. The base address is always &MyTaskStk[0].
.TLS_Tbl[]
This field typically contains an array of pointers and is used by compilers for thread safety as described in Chapter 20, “Thread Safety of the Compiler’s Run-Time Library”).
.TaskEntryAddr
This field contains the entry address of the task. As previously mentioned, a task is declared as shown below and this field contains the address of MyTask.
void MyTask (void *p_arg);
.TaskEntryArg
This field contains the value of the argument that is passed to the task when the task starts. As previously mentioned, a task is declared as shown below and this field contains the value of p_arg.
void MyTask (void *p_arg);
.PendNextPtr and .PendPrevPtr
These two pointers are used to doubly link the OS_TCB in a wait list for a kernel object such as a semaphore, event flag group, mutex or message queue. This is further described in Chapter 10, “Pend Lists”.
.PendObjPtr
Is a pointer to the kernel object pended on. This is further described in Chapter 10, “Pend Lists”.