Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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
Listing - Run-To-Completion task
Listing - Run-To-Completion task

Code Block
languagecpp
titleListing - Run-To-Completion task
linenumberstrue
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
Listing - Infinite Loop task
Listing - Infinite Loop task

Code Block
languagecpp
titleListing - Infinite Loop task
linenumberstrue
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
Listing - OSTaskCreate()
Listing - OSTaskCreate()

Code Block
languagecpp
titleListing - OSTaskCreate()
linenumberstrue
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
borderWidth0
titleFigure - OSTaskCreate() initializes the task’s TCB and stack

Image Added


Panel
bgColor#f0f0f0

(1) When calling OSTaskCreate(), you pass the base address of the stack (p_stk_base) that will be used by the task, the watermark limit for stack growth (stk_limit) which is expressed in number of CPU_STK entries before the stack is empty, and the size of that stack (stk_size), also in number of CPU_STK elements.

(2) When specifying OS_OPT_TASK_STK_CHK + OS_OPT_TASK_STK_CLR in the opt argument of OSTaskCreate(), µC/OS-III initializes the task’s stack with all zeros.

(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 top-of-stack.

(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
Figure - Tasks interact with resources
Figure - Tasks interact with resources

Panel
borderWidth0
titleFigure - Tasks interact with resources

Image Added


Panel
bgColor#f0f0f0

(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 ready-to-run task (highest priority).

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 OS_PRIO_MAX in os_cfg.h). Thus, µC/OS-III allows the user to determine the number of different priority levels the application is allowed to use. Also, µC/OS-III supports an unlimited number of tasks at the same priority. For example, µC/OS-III can be configured to have 64 different priority levels and one can assign dozens of tasks at each priority level.

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.

static CPU_STK MyTaskStk[???];

or,

CPU_STK MyTaskStk[???];

Note that “???” indicates that the size of the stack (and thus the array) depends on the task stack requirements. Stack space may be allocated dynamically by using the C compiler’s heap management function (i.e., malloc()) as shown below. However, care must be taken with fragmentation. If creating and deleting tasks, the process of allocating memory might not be able to provide a stack for the task(s) because the heap will eventually become fragmented. For this reason, allocating stack space dynamically in an embedded system is typically allowed but, once allocated, stacks should not be deallocated. Said another way, it’s fine to create a task’s stack from the heap as long as you don’t free the stack space back to the heap.

Anchor
Listing - Stack size
Listing - Stack size

Code Block
languagecpp
titleListing - Stack size
linenumberstrue
void SomeCode (void) 
{
    CPU_STK *p_stk;
    :
    :
    p_stk = (CPU_STK *)malloc(stk_size); if (p_stk != (CPU_STK *)0) {
        Create the task and pass it "p_stk" as the base address of the stack; }
    :
    : 
} 

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.