Task Management

In the previous section, I specified that a task is either an infinite loop function or a function that deletes itself when it is done executing. Note that the task code is not actually deleted — µC/OS-II simply doesn’t know about the task anymore, so that code will not run. A task looks just like any other C function, containing a return type and an argument, but it must never return. The return type of a task must always be declared void. The functions described in this chapter are found in the file OS_TASK.C. To review, a task must have one of the two structures:



This chapter describes the services that allow your application to create a task, delete a task, change a task’s priority, suspend and resume a task, and allow your application to obtain information about a task.

µC/OS-II can manage up to 64 tasks, although µC/OS-II reserves the four highest priority tasks and the four lowest priority tasks for its own use. However, at this time, only two priority levels are actually used by µC/OS-II: OSTaskCreate and OS_LOWEST_PRIO-1 (see OS_CFG.H). This leaves you with up to 56 application tasks. The lower the value of the priority, the higher the priority of the task. In the current version of µC/OS-II, the task priority number also serves as the task identifier.

Creating a Task, OSTaskCreate()

In order for µC/OS-II to manage your task, you must create it. You create a task by passing its address and other arguments to one of two functions: OSTaskCreate() or OSTaskCreateExt(). OSTaskCreate() is backward compatible with µC/OS, and OSTaskCreateExt() is an extended version of OSTaskCreate(), providing additional features. A task can be created using either function. A task can be created prior to the start of multitasking or by another task. You must create at least one task before you start multitasking [i.e., before you call OSStart()]. A task cannot be created by an ISR.

The code for OSTaskCreate() is shown in Listing 4.1. As can be seen, OSTaskCreate() requires four arguments. task is a pointer to the task code, pdata is a pointer to an argument that is passed to your task when it starts executing, ptos is a pointer to the top of the stack that is assigned to the task (see section 4.02, Task Stacks), and prio is the desired task priority.


Creating a Task, OSTaskCreateExt()

Creating a task using OSTaskCreateExt() offers more flexibility, but at the expense of additional overhead. The code for OSTaskCreateExt() is shown in Listing 4.2.

As can be seen, OSTaskCreateExt() requires nine arguments! The first four arguments (task, pdata, ptos, and prio) are exactly the same as in OSTaskCreate(), and they are located in the same order. I did this to make it easier to migrate your code to use OSTaskCreateExt().

id

establishes a unique identifier for the task being created. This argument has been added for future expansion and is otherwise unused by µC/OS-II. This identifier will allow me to extend µC/OS-II beyond its limit of 64 tasks. For now, simply set the task’s ID to the same value as the task’s priority.

pbos

is a pointer to the task’s bottom-of-stack and this argument is used to perform stack checking.

stk_size

specifies the size of the stack in number of elements. This means that if a stack entry is four bytes wide, then a stk_size of 1000 means that the stack will have 4,000 bytes. Again, this argument is used for stack checking.

pext

is a pointer to a user-supplied data area that can be used to extend the OS_TCB of the task. For example, you can add a name to a task (see Example 3 in Chapter 1), storage for the contents of floating-point registers (see Example 4 in Chapter 1) during a context switch, a port address to trigger an oscilloscope during a context switch, and more.

opt

specifies options to OSTaskCreateExt(), specifying whether stack checking is allowed, whether the stack will be cleared, whether floating-point operations are performed by the task, etc. uCOS_II.H contains a list of available options (OS_TASK_OPT_STK_CHK, OS_TASK_OPT_STK_CLR, and OS_TASK_OPT_SAVE_FP). Each option consists of a bit. The option is selected when the bit is set (simply OR the above OS_TASK_OPT_??? constants).

Task Stacks

Each task must have its own stack space. A stack must be declared as being of type OS_STK and must consist of contiguous memory locations. You can allocate stack space either statically (at compile time) or dynamically (at run time). A static stack declaration is shown in Listings 4.3 and 4.4. Either declaration is made outside a function


or


You can allocate stack space dynamically by using the C compiler’s malloc() function as shown in Listing 4.5. However, you must be careful with fragmentation. Specifically, if you create and delete tasks, your memory allocator may not be able to return a stack for your task(s) because the heap eventually becomes fragmented.


Because µC/OS-II supports processors with stacks that grow either from high to low memory or from low to high memory, you must know how the stack grows when you call either OSTaskCreate() or OSTaskCreateExt() because you need to pass the task’s top-of-stack to these functions. When OS_STK_GROWTH is set to 0 in OS_CPU.H , you need to pass the lowest memory location of the stack to the task create function as shown in Listing 4.6.


When OS_STK_GROWTH is set to 1 in OS_CPU.H, you need to pass the highest memory location of the stack to the task create function as shown in Listing 4.7.


This requirement affects code portability. If you need to port your code from a processor architecture that supports a downward-growing stack to one that supports an upward-growing stack, you may need to make your code handle both cases. Specifically, Listings 4.6 and 4.7 are rewritten as shown in Listing 4.8.


The size of the stack needed by your task is application specific. When sizing the stack, however, you must account for nesting of all the functions called by your task, the number of local variables that will be allocated by all functions called by your task, and the stack requirements for all nested interrupt service routines. In addition, your stack must be able to store all CPU registers.

Stack Checking, OSTaskStkChk()

Sometimes it is necessary to determine how much stack space a task actually uses. This allows you to reduce the amount of RAM needed by your application code by not overallocating stack space. µC/OS-II provides OSTaskStkChk(), which provides you with this valuable information.

In order to use the µC/OS-II stack-checking facilities, you must do the following.

  • Set OS_TASK_CREATE_EXT to 1 in OS_CFG.H.
  • Create a task using OSTaskCreateExt() and give the task much more space than you think it really needs. You can call OSTaskStkChk() for any task, from any task.
  • Set the opt argument in OSTaskCreateExt() to OS_TASK_OPT_STK_CLR + OS_TASK_OPT_STK_CLR. Note that if your startup code clears all RAM and you never delete tasks once they are created, you don’t need to set the OS_TASK_OPT_STK_CLR option. This reduces the execution time of OSTaskCreateExt().
  • Call OSTaskStkChk() from a task by specifying the priority of the task you want to check. You can inquire about any task stack not just the running task.


You need to run the application long enough and under your worst case conditions to get proper numbers. Once OSTaskStkChk() provides you with the worst case stack requirement, you can go back and set the final size of your stack. You should accommodate system expansion, so make sure you allocate between 10 and 100 percent more stack than what OSTaskStkChk() reports. What you should get from stack checking is a ballpark figure; you are not looking for an exact stack usage.

The code for OSTaskStkChk() is shown in Listing 4.9. The data structure OS_STK_DATA (see uCOS_II.H) is used to hold information about the task stack. I decided to use a data structure for two reasons. First, I consider OSTaskStkChk() to be a query-type function, and I wanted to have all query functions work the same way — return data about the query in a data structure. Second, passing data in a data structure is efficient and allows me to add additional fields in the future without changing the API (Application Programming Interface) of OSTaskStkChk(). For now, OS_STK_DATA only contains two fields: OSFree and OSUsed. As you can see, you invoke OSTaskStkChk() by specifying the priority of the task you want to perform stack checking on.


Deleting a Task, OSTaskDel()

Sometimes it is necessary to delete a task. Deleting a task means that the task will be returned to the DORMANT state (see section 3.02, Task States) and does not mean that the code for the task will be deleted. The task code is simply no longer scheduled by µC/OS-II. You delete a task by calling OSTaskDel() (Listing 4.10).


Requesting to Delete a Task, OSTaskDelReq()

Sometimes, a task owns resources such as memory buffers or a semaphore. If another task attempts to delete this task, the resources are not freed and thus are lost. This would lead to memory leaks which is not acceptable for just about any embedded system. In this type of situation, you somehow need to tell the task that owns these resources to delete itself when it’s done with the resources. You can accomplish this with the OSTaskDelReq() function. Both the requestor and the task to be deleted need to call OSTaskDelReq(). The requestor code is shown in Listing 4.11.


The pseudocode for the task that needs to delete itself is shown in Listing 4.12. This task basically polls a flag that resides inside the task’s OS_TCB. The value of this flag is obtained by calling OSTaskDelReq(OS_PRIO_SELF).


The code for OSTaskDelReq() is shown in Listing 4.13. As usual, OSTaskDelReq() needs to check for boundary conditions.


Changing a Task’s Priority, OSTaskChangePrio()

When you create a task, you assign the task a priority. At run time, you can change the priority of any task by calling OSTaskChangePrio(). In other words, µC/OS-II allows you to change priorities dynamically. The code for OSTaskChangePrio() is shown in Listing 4.14.


Suspending a Task, OSTaskSuspend()

Sometimes it is useful to explicitly suspend the execution of a task. This is accomplished with the OSTaskSuspend() function call. A suspended task can only be resumed by calling the OSTaskResume() function call. Task suspension is additive. This means that if the task being suspended is also waiting for time to expire, the suspension needs to be removed and the time needs to expire in order for the task to be ready to run. A task can suspend either itself or another task.

The code for OSTaskSuspend() is shown in Listing 4.15.


Resuming a Task, OSTaskResume()

As mentioned in the previous section, a suspended task can only be resumed by calling OSTaskResume(). The code for OSTaskResume() is shown in Listing 4.16.


Getting Information about a Task, OSTaskQuery()

Your application can obtain information about itself or other application tasks by calling OSTaskQuery(). In fact, OSTaskQuery() obtains a copy of the contents of the desired task’s OS_TCB. The fields available to you in the OS_TCB depend on the configuration of your application (see OS_CFG.H). Indeed, because µC/OS-II is scalable, it only includes the features that your application requires.

To call OSTaskQuery(), your application must allocate storage for an OS_TCB, as shown in Listing 4.17. This OS_TCB is in a totally different data space from the OS_TCBs allocated by µC/OS-II. After calling OSTaskQuery(), this OS_TCB contains a snapshot of the OS_TCB for the desired task. You need to be careful with the links to other OS_TCBs (i.e., .OSTCBNext and .OSTCBPrev); you don’t want to change what these links are pointing to! In general, only use this function to see what a task is doing — a great tool for debugging.


The code for OSTaskQuery() is shown in Listing 4.18.