...
A run-to-completion task must delete itself by calling OSTaskDel(). The task starts, performs its function, and terminates. There would typically not be too many such tasks in the embedded system because of the overhead associated with “creating” and “deleting” tasks at run-time. In the task body, you can call most of µC/OS-III’s functions to help perform the desired operation of the task.
| Anchor | ||||
|---|---|---|---|---|
|
| Code Block | ||||||
|---|---|---|---|---|---|---|
| ||||||
void MyTask (void *p_arg)
{
OS_ERR err;
/* Local variables */
/* Do something with 'p_arg' */
/* Task initialization */
/* Task body ... do work! */
OSTaskDel((OS_TCB *)0, &err);
} |
With µC/OS-III, you either can call a C or assembly language functions from a task. In fact, it is possible to call the same C function from different tasks as long as the functions are reentrant. A reentrant function is a function that does not use static or otherwise global variables unless they are protected (µC/OS-III provides mechanisms for this) from multiple access. If shared C functions only use local variables, they are generally reentrant (assuming that the compiler generates reentrant code). An example of a non-reentrant function is the famous strtok() provided by most C compilers as part of the standard library. This function is used to parse an ASCII string for “tokens.” The first time you call this function, you specify the ASCII string to parse and a list of token delimiters. As soon as the function finds the first token, it returns. The function “remembers” where it was last so when called again, it can extract additional tokens, which is clearly non-reentrant.
The use of an infinite loop is more common in embedded systems because of the repetitive work needed in such systems (reading inputs, updating displays, performing control operations, etc.). This is one aspect that makes a task different than a regular C function. Note that one could use a “while (1)” or “for (;;)” to implement the infinite loop, since both behave the same. The one used is simply a matter of personal preference. At Micrium, we like to use “while (DEF_ON)”. The infinite loop must call a µC/OS-III service (i.e., function) that will cause the task to wait for an event to occur. It is important that each task wait for an event to occur, otherwise the task would be a true infinite loop and there would be no easy way for other lower priority tasks to execute. This concept will become clear as more is understood regarding µC/OS-III.
| Anchor | ||||
|---|---|---|---|---|
|
| Code Block | ||||||
|---|---|---|---|---|---|---|
| ||||||
void MyTask (void *p_arg) { /* Local variables */ /* Do something with "p_arg" */ /* Task initialization */ while (DEF_ON) { /* Task body, as an infinite loop. */ : /* Task body ... do work! */ : /* Must call one of the following services: */ /* OSFlagPend() */ /* OSMutexPend() */ /* OSQPend() */ /* OSSemPend() */ /* OSTimeDly() */ /* OSTimeDlyHMSM() */ /* OSTaskQPend() */ /* OSTaskSemPend() */ /* OSTaskSuspend() (Suspend self) */ /* OSTaskDel() (Delete self) */ : /* Task body ... do work! */ : } } |
The event the task is waiting for may simply be the passage of time (when OSTimeDly() or OSTimeDlyHMSM() is called). For example, a design may need to scan a keyboard every 100 milliseconds. In this case, you would simply delay the task for 100 milliseconds then see if a key was pressed on the keyboard and, possibly perform some action based on which key was pressed. Typically, however, a keyboard scanning task should just buffer an “identifier” unique to the key pressed and use another task to decide what to do with the key(s) pressed.
...
Tasks must be created in order for µC/OS-III to know about tasks. You create a task by simply calling OSTaskCreate() as we’ve seen in the Getting Started section. The function prototype for OSTaskCreate() is shown below:
| Anchor | ||||
|---|---|---|---|---|
|
| Code Block | ||||||
|---|---|---|---|---|---|---|
| ||||||
void OSTaskCreate (OS_TCB *p_tcb,
OS_CHAR *p_name,
OS_TASK_PTR p_task,
void *p_arg,
OS_PRIO prio,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_limit,
CPU_STK_SIZE stk_size,
OS_MSG_QTY q_size,
OS_TICK time_slice,
void *p_ext,
OS_OPT opt,
OS_ERR *p_err) |
A complete description of OSTaskCreate() and its arguments is provided in uC-OS-III API Reference. However, it is important to understand that a task needs to be assigned a Task Control Block (i.e., TCB), a stack, a priority and a few other parameters which are initialized by OSTaskCreate(), as shown in the figure below.
Anchor Figure - OSTaskCreate() initializes the task’s TCB and stack Figure - OSTaskCreate() initializes the task’s TCB and stack
| Panel | ||||
|---|---|---|---|---|
| ||||
| Panel | ||
|---|---|---|
| ||
(1) When calling (2) When specifying (3) µC/OS-III then initializes the top of the task’s stack with a copy of the CPU registers in the same stacking order as if they were all saved at the beginning of an ISR. This makes it easy to perform context switches as we will see when discussing the context switching process. For illustration purposes, the assumption is that the stack grows from high memory to low memory, but the same concept applies for CPUs that use the stack in the reverse order. (4) The new value of the stack pointer (SP) is saved in the TCB. Note that this is also called the (5) The remaining fields of the TCB are initialized: task priority, task name, task state, internal message queue, internal semaphore, and many others. |
Next, a call is made to a function that is defined in the CPU port, OSTaskCreateHook() (see os_cpu_c.c). OSTaskCreateHook() is passed the pointer to the new TCB and this function allows you (or the port designer) to extend the functionality of OSTaskCreate(). For example, one could printout the contents of the fields of the newly created TCB onto a terminal for debugging purposes.
...
The figure below shows the resources with which a task typically interacts.
| Anchor | ||||
|---|---|---|---|---|
|
| Panel | ||||
|---|---|---|---|---|
| ||||
| Panel | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| |||||||||||||||
(1) An important aspect of a task is its code. As previously mentioned, the code looks like any other C function, except that it is typically implemented as an infinite loop and, a task is not allowed to return. (2) Each task is assigned a priority based on its importance in the application. µC/OS-III’s job is to decide which task will run on the CPU. The general rule is that µC/OS-III will run the most important With µC/OS-III, a low priority number indicates a high priority. In other words, a task at priority 1 is more important than a task at priority 10. µC/OS-III supports a compile-time user configurable number of different priorities (see See Assigning Task Priorities. (3) A task has its own set of CPU registers. As far as a task is concerned, the task thinks it actually has the CPU all to itself. (4) Because µC/OS-III is a preemptive kernel, each task must have its own stack area. The stack always resides in RAM and is used to keep track of local variables, function calls, and possibly ISR (Interrupt Service Routine) nesting. Stack space can be allocated either statically (at compile-time) or dynamically (at run-time). A static stack declaration is shown below. This declaration is made outside of a function.
or,
Note that “
See Determining the Size of a Stack. (5) A task can also have access to global variables. However, because µC/OS-III is a preemptive kernel care must be taken with code when accessing such variables as they may be shared between multiple tasks. Fortunately, µC/OS-III provides mechanisms to help with the management of such shared resources (semaphores, mutexes and more). (6) A task may also have access to one or more Input/Output (I/O) devices (also known as peripherals). In fact, it is common practice to assign tasks to manage I/O devices. |