Versions Compared

Key

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

...

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 reenable 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.

...

{   
    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)
    .
}

L3.1(1) OS_CPU_SR is a Because I don’t know what the compiler functions are (there is no standard naming convention), the µ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 macros are used to encapsulate the functionality as follows:

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

...

L3.1(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.

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

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

L3.1(5) Once the critical section has completed, interrupts can be reenabled 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);

3.01. Tasks

L3.2(2) A task is typically an infinite loop function as shown in Listing 3.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.

L3.2(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.

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.

µ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.

3.02. 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 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().

3.03. 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.

.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 that 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

...

     \
  set_processor_psw(cpu_sr);

Tasks

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.

µ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 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.

.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 that 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. 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

.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 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 . µ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).

...

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.

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

L3.6(2)

L3.6(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.

L3.6(4) If you enabled code generation for OSTaskCreateExt() (OS_TASK_CREATE_EXT_EN is set 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 additional fields in OS_TCB are filled in.

L3.6(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.

L3.6(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.

L3.6(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.

L3.6(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.

L3.6(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().

L3.6(10) 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().

L3.6(11)

L3.6(12) OS_TCBInit() disables interrupts when it needs to insert the OS_TCB into the doubly linked list of tasks that have been created. The list starts at OSTCBList, and the OS_TCB of a new task is always inserted at the beginning of the list.

L3.6(13)

L3.6(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.

3.04. 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.

...

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() 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().

(11)

(12) OS_TCBInit() disables interrupts when it needs to insert the OS_TCB into the doubly linked list of tasks that have been created. 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[16] is 1.

Bit 2 7 in OSRdyGrp is 1 when any bit in OSRdyTbl[27] 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 The code in Listing 3.7 is used to place a task in the ready list. prio is the task’s priority.

...

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 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.

L3.10(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).

L3.10(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[].

L3.10(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.

L3.10(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.

L3.10(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.

L3.10(6) Finally, the macro 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 invoked to do the actual called to perform a context switch. A context switch simply consists of saving the processor registers on the stack 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 suspended and restoring preempted and load into the CPU the values of the registers of for the higher priority task from its stack. In task to resume.

OS_TASK_SW() is a macro that ‘normally’ invokes a microprocessor software interrupt because µ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 assumes that context switching will be done by interrupt level code. What µ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 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() 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 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 3.6 µC/OS-II structures when OS_TASK_SW() is called.

Figure 3.7 shows the state of the variables and data structures after calling 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.

3.06. 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)

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

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

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

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

F3.6(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.6 µC/OS-II structures when OS_TASK_SW() is called.

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.

F3.7(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.

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

F3.7(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.7 Saving the current task’s context.

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

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

F3.8(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.

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

F3.8(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.

Figure 3.8 Resuming the current task.

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.

3.07. 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.

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

L3.12(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.

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

L3.13(2) We make sure OSLockNesting is not already 0. If it was, it would be an indication that you called OSSchedUnlock() too many times. In other words, you would not have the same number of OSSchedLock() as OSSchedUnlock().

L3.13(3) OSLockNesting is decremented.

L3.13(4)

L3.13(5) We only want to allow the scheduler to execute when all nesting have completed. OSSchedUnlock() is called from a task because events could have made higher priority tasks ready to run while scheduling was locked.

3.08. Idle Task

µC/OS-II always creates a task (a.k.a. the idle task) that is executed when none of the other tasks is ready to run. The idle task, OS_TaskIdle(), is always set to the lowest priority, OS_LOWEST_PRIO. The code for the idle task is shown in Listing 3.14. The idle task can never be deleted by application software.

L3.14(1) OS_TaskIdle() increments a 32-bit counter called OSIdleCtr, which is used by the statistics task (see section 3.??, Statistics Task) to determine the percent CPU time actually being consumed by the application software. Interrupts are disabled then enabled around the increment because on 8- and most 16-bit processors, a 32-bit increment requires multiple instructions that must be protected from being accessed by higher priority tasks or ISRs.

L3.14(2) OS_TaskIdle() calls OSTaskIdleHook() which is a function that you can write to do just about anything you want. You can use OSTaskIdleHook() to STOP the CPU so that it can enter low-power mode. This is useful when your application is battery powered. OS_TaskIdle() MUST ALWAYS be ready to run so don’t call one of the PEND functions, OSTimeDly???() functions or OSTaskSuspend() from OSTaskIdleHook().

3.09. Statistics Task

µC/OS-II contains a task that provides run-time statistics. This task is called OS_TaskStat() and is created by µC/OS-II if you set the configuration constant OS_TASK_STAT_EN (see OS_CFG.H) to 1. When enabled, OS_TaskStat() (see OS_CORE.C) executes every second and computes the percent CPU usage. In other words, OS_TaskStat() tells you how much of the CPU time is used by your application, as a percentage. This value is placed in the signed 8-bit integer variable OSCPUUsage. The resolution of OSCPUUsage is 1 percent.

If your application is to use the statistic task, you must call OSStatInit() (see OS_CORE.C) from the first and only task created in your application during initialization. In other words, your startup code must create only one task before calling OSStart(). From this one task, you must call OSStatInit() before you create your other application tasks. The single task that you create will, of course, be allowed to create other tasks. The pseudocode in Listing 3.15 shows what needs to be done.

Because your application must create only one task, TaskStart(), µC/OS-II has only three tasks to manage when main() calls OSStart(): TaskStart(), OSTaskIdle(), and OS_TaskStat(). Please note that you don’t have to call the startup task: TaskStart() — you can call it anything you like. Your startup task will have the highest priority because µC/OS-II sets the priority of the idle task to OS_LOWEST_PRIO and the priority of the statistic task to OS_LOWEST_PRIO – 1 internally.

Figure 3.9 illustrates the flow of execution when initializing the statistic task.

F3.9(1) The first function that you must call in µC/OS-II is OSInit(), which initializes µC/OS-II.

F3.9(2) Next, you need to install the interrupt vector that will be used to perform context switches. Note that on some processors (specifically the Motorola 68HC11), there is no need to “install” a vector because the vector is already resident in ROM.

F3.9(3) You must create TaskStart() by calling either OSTaskCreate() or OSTaskCreateExt().

F3.9(4) Once you are ready to multitask, call OSStart(), which schedules TaskStart() for execution because it has the highest priority.

F3.9(5) TaskStart() is responsible for initializing and starting the ticker. You want to initialize the ticker in the first task to execute because you don’t want to receive a tick interrupt until you are actually multitasking.

F3.9(6) Next, TaskStart() calls OSStatInit(). OSStatInit() determines how high the idle counter (OSIdleCtr) can count if no other task in the application is executing. A Pentium II running at 333MHz increments this counter to a value of about 15,000,000. OSIdleCtr is still far from wrapping around the 4,294,967,296 limit of a 32-bit value. At the rate processor speeds are getting, it will not be too long before OSIdleCtr overflows. If this becomes a problem, you can always introduce some software delays in OSTaskIdleHook(). Because OS_TaskIdle() really doesn’t execute any ‘useful’ code, it’s OK to throw away CPU cycles.

F3.9(7) OSStatInit() starts off by calling OSTimeDly(), which puts TaskStart() to sleep for two ticks. This is done to synchronize OSStatInit() to the ticker. µC/OS-II then picks the next highest priority task that is ready to run, which happens to be OS_TaskStat().

F3.9(8) You will see the code for OS_TaskStat() later, but as a preview, the very first thing OS_TaskStat() does is check to see if the flag OSStatRdy is set to FALSE and delays for two seconds if it is.

F3.9(9) It so happens that OSStatRdy is initialized to FALSE by OSInit(), so OS_TaskStat() in fact puts itself to sleep for two seconds. This causes a context switch to the only task that is ready to run, OSTaskIdle().

F3.9(10) The CPU stays in OS_TaskIdle() until the two ticks of TaskStart() expire.

F3.9(11)

F3.9(12) After two ticks, TaskStart() resumes execution in OSStatInit() and OSIdleCtr is cleared.

F3.9(13) Then, OSStatInit() delays itself for one full second. Because no other task is ready to run, OS_TaskIdle() again gets control of the CPU.

F3.9(14) During that time, OSIdleCtr is continuously incremented.

F3.9(15) After one second, TaskStart() is resumed, still in OSStatInit(), and the value that OSIdleCtr reached during that one second is saved in OSIdleCtrMax.

F3.9(16)

F3.9(17) OSStatInit() sets OSStatRdy to TRUE, which allows OS_TaskStat() to perform a CPU usage computation after its delay of two seconds expires.

Figure 3.9 Statistic task initialization.

The code for OSStatInit() is shown in Listing 3.16.

The code for OS_TaskStat() is shown in Listing 3.17.

L3.17(1) I’ve already discussed why OS_TaskStat() has to wait for the flag OSStatRdy to be set to TRUE in the previous paragraphs. The task code executes every second and basically determines how much CPU time is actually consumed by all the application tasks. When you start adding application code, the idle task will get less of the processor’s time, and OSIdleCtr will not be allowed to count as high as it did when nothing else was running. Remember that OSStatInit() saved this maximum value in OSIdleCtrMax.

L3.17(3) Every second, the value of the idle counter is copied into the global variable OSIdleCtrRun. This variable thus holds the maximum value of the idle counter for the second that just passed. This value is not used anywhere else by µC/OS-II but can be monitored (and possibly displayed) by your application. The idle counter is then reset to 0 for the next measurement.

L3.17(4) CPU utilization (Equation [3.1]) is stored in the variable OSCPUUsage:

 

[3.1]

L3.17(2) The above equation needs to be re-written because OSIdleCtr / OSIdleCtrMax would always yield 0 because of the integer operation. The new equation is:

[3.2]

Multiplying OSIdleCtr by 100 limits the maximum value that OSIdleCtr can take, especially on fast processors. In other words, in order for the multiplication of OSIdleCtr to not overflow, OSIdleCtr must never be higher than 42,949,672! With fast processors, it’s quite likely that OSIdleCtr can reach this value. To correct this potential problem, all we need to do is divide OSIdleCtrMax by 100 instead as shown below.

[3.3]

The local variable max is thus precomputed to hold OSIdleCtrMax divided by 100.

L3.17(5) Once the above computation is performed, OS_TaskStat() calls OSTaskStatHook(), a user-definable function that allows the statistic task to be expanded. Indeed, your application could compute and display the total execution time of all tasks, the percent time actually consumed by each task, and more (see Chapter 1, Example 3).

3.10. Interrupts under µC/OS-II

µC/OS-II requires that an Interrupt Service Routine (ISR) be written in assembly language. However, if your C compiler supports in-line assembly language, you can put the ISR code directly in a C source file.

The pseudocode for an ISR is shown in Listing 3.18.

L3.18(1) Your code should save all CPU registers onto the current task stack. Note that on some processors, like the Motorola 68020 (and higher), a different stack is used when servicing an interrupt. µC/OS-II can work with such processors as long as the registers are saved on the interrupted task’s stack when a context switch occurs.

L3.18(2) µC/OS-II needs to know that you are servicing an ISR, so you need to either call OSIntEnter() or increment the global variable OSIntNesting. OSIntNesting can be incremented directly if your processor performs an increment operation to memory using a single instruction. If your processor forces you to read OSIntNesting in a register, increment the register, store the result back in OSIntNesting, then call OSIntEnter(). OSIntEnter() wraps these three instructions with code to disable and then enable interrupts, thus ensuring exclusive access to OSIntNesting, which is considered a shared resource. Incrementing OSIntNesting directly is much faster than calling OSIntEnter() and is thus the preferred way. One word of caution: some implementations of OSIntEnter() cause interrupts to be enabled when OSIntEnter() returns. In these cases, you need to clear the interrupt source before calling OSIntEnter(); otherwise, your interrupt will be re-entered continuously and your application will crash!

Certain processors such as the Motorola 68020 allow interrupts to be nested even though you are just starting to service an interrupt. The beginning of the ISR needs to be different for these processors. I will not get into this here but, it may be worthwhile for you to download the 68020 port from Micrium web site to see how to handle this situation.

L3.18(3)

L3.18(4) We check to see if this is the first interrupt level and if it is, we immediately save the stack pointer into the current task’s OS_TCB. You should note that I added these two lines of code since V2.04. If you have a port that assumes V2.04 or earlier, you should simply add these two lines in ALL your ISRs.

L3.18(5) You must clear the interrupt source because you stand the chance of re-entering the ISR if you decide to re-enable interrupts.

L3.18(6) You can re-enable interrupts if you want to allow interrupt nesting. µC/OS-II allows you to nest interrupts because it keeps track of ISR nesting in OSIntNesting.

L3.18(7) Once the previous steps have been accomplished, you can start servicing the interrupting device. This section is obviously application specific.

L3.18(8) The conclusion of the ISR is marked by calling OSIntExit(), which decrements the interrupt nesting counter. When the nesting counter reaches 0, all nested interrupts have completed and µC/OS-II needs to determine whether a higher priority task has been awakened by the ISR (or any other nested ISR). If a higher priority task is ready to run, µC/OS-II returns to the higher priority task rather than to the interrupted task.

L3.18(9) If the interrupted task is still the most important task to run, OSIntExit() returns to the interrupted task.

L3.18(10) At that point the saved registers are restored and a return from interrupt instruction is executed. Note that µC/OS-II will return to the interrupted task if scheduling has been disabled (OSLockNesting > 0).

The above description is further illustrated in Figure 3.10.

F3.10(1) The interrupt is received but is not recognized by the CPU, either because interrupts have been disabled by µC/OS-II or your application or because the CPU has not completed executing the current instruction.

F3.10(2)

F3.10(3) Once the CPU recognizes the interrupt, the CPU vectors (at least on most microprocessors) to the ISR.

F3.10(4) As described above, the ISR saves the CPU registers (i.e., the CPU’s context).

F3.10(5) Once this is done, your ISR notifies µC/OS-II by calling OSIntEnter() or by incrementing OSIntNesting. You also need to save the stack pointer into the current task’s OS_TCB.

F3.10(6) Your ISR code then gets to execute. Your ISR should do as little work as possible and defer most of the work at the task level. A task is notified of the ISR by calling either OSFlagPost(), OSMboxPost(), OSQPost(), OSQPostFront(), or OSSemPost(). The receiving task may or may not be pending at the event flag, mailbox, queue, or semaphore when the ISR occurs and the post is made.

F3.10(7) Once the user ISR code has completed, your need to call OSIntExit(). As can be seen from the timing diagram, OSIntExit() takes less time to return to the interrupted task when there is no higher priority task (HPT) readied by the ISR.

F3.10(8)

F3.10(9) In this case, the CPU registers are then simply restored and a return from interrupt instruction is executed.

F3.10(10) If the ISR makes a higher priority task ready to run, then OSIntExit() takes longer to execute because a context switch is now needed.

F3.10(11)

F3.10(12) The registers of the new task are restored, and a return from interrupt instruction is executed.

Figure 3.10 Servicing an interrupt.

The code for OSIntEnter() is shown in Listing 3.19 and the code for OSIntExit() is shown in Listing 3.20. Very little needs to be said about OSIntEnter().

OSIntExit() looks strangely like OS_Sched() except for three differences:

L3.20(1) The interrupt nesting counter is decremented in OSIntExit() and rescheduling occurs when both the interrupt nesting counter and the lock nesting counter (OSLockNesting) are 0.

L3.20(2) The Y index needed for OSRdyTbl[] is stored in the global variable OSIntExitY. This is done because prior to V2.51, OSIntCtxSw() needed to account for local variables and return addresses. As of V2.51, OSIntCtxSw() doesn’t need to account for these. However, I decided to leave OSIntExitY as a global for backwards compatibility with previous ports.

L3.20(3) If a context switch is needed, OSIntExit() calls OSIntCtxSw() instead of OS_TASK_SW() as it did in OS_Sched().

You need to call OSIntCtxSw() instead of OS_TASK_SW() because the ISR has already saved the CPU registers onto the interrupted task and thus shouldn’t be saved again. Implementation details about OSIntCtxSw() are provided in Chapter 13, Porting µC/OS-II.

Some processors, like the Motorola 68HC11, require that you implicitly re-enable interrupts in order to allow nesting. This can be used to your advantage. Indeed, if your ISR needs to be serviced quickly and it doesn’t need to notify a task about itself, you don’t need to call OSIntEnter() (or increment OSIntNesting) or OSIntExit() as long as you don’t enable interrupts within the ISR. The pseudocode in Listing 3.21 shows this situation. In this case, the only way a task and this ISR can communicate is through global variables.

3.11. Clock Tick

µC/OS-II requires that you provide a periodic time source to keep track of time delays and timeouts. A tick should occur between 10 and 100 times per second, or Hertz. The faster the tick rate, the more overhead µC/OS-II will impose on the system. The actual frequency of the clock tick depends on the desired tick resolution of your application. You can obtain a tick source either by dedicating a hardware timer or generating an interrupt from an AC power line (50/60Hz) signal.

You MUST enable ticker interrupts AFTER multitasking has started; that is, after calling OSStart(). In other words, you should initialize ticker interrupts in the first task that executes following a call to OSStart(). A common mistake is to enable ticker interrupts after OSInit() and before OSStart() as shown in Listing 3.22.

Potentially, the tick interrupt could be serviced before µC/OS-II starts the first task. At this point, µC/OS-II is in an unknown state and your application will crash.

The µC/OS-II clock tick is serviced by calling OSTimeTick() from a tick ISR. OSTimeTick() keeps track of all the task timers and timeouts. The tick ISR follows all the rules described in the previous section. The pseudocode for the tick ISR is shown in Listing 3.23. This code must be written in assembly language because you cannot access CPU registers directly from C. Because the tick ISR is always needed, it is generally provided with a port.

The code for OSTimeTick() is shown in Listing 3.24.

L3.24(1) OSTimeTick() starts by calling the user-definable function OSTimeTickHook(), which can be used to extend the functionality of OSTimeTick(). I decided to call OSTimeTickHook() first to give your application a chance to do something as soon as the tick is serviced because you may have some time-critical work to do. Most of the work done by OSTimeTick() basically consists of decrementing the OSTCBDly field for each OS_TCB (if it’s nonzero).

L3.24(2) OSTimeTick() also accumulates the number of clock ticks since power-up in an unsigned 32-bit variable called OSTime. Note that I disable interrupts before incrementing OSTime because on some processors, a 32-bit increment will most likely be done using multiple CPU instructions.

L3.24(3)

L3.24(4) OSTimeTick() follows the chain of OS_TCB, starting at OSTCBList, until it reaches the idle task.

L3.24(5) When the OSTCBDly field of a task’s OS_TCB is decremented to 0, the task is made ready to run.

L3.24(6) The task is not readied, however, if it was explicitly suspended by OSTaskSuspend().

The execution time of OSTimeTick() is directly proportional to the number of tasks created in an application, however execution time is still very deterministic.

If you don’t like to make ISRs any longer than they must be, OSTimeTick() can be called at the task level as shown in Listing 3.25. To do this, create a task that has a higher priority than all your other application tasks. The tick ISR needs to signal this high-priority task by using either a semaphore or a message mailbox.

You obviously need to create a mailbox (contents initialized to NULL) that will be used to signal the task that a tick interrupt has occurred (Listing 3.26).

3.12. µC/OS-II Initialization

A requirement of µC/OS-II is that you call OSInit() before you call any of µC/OS-II’s other services. OSInit() initializes all µC/OS-II variables and data structures (see OS_CORE.C).

OSInit() creates the idle task OSTaskIdle(), which is always ready to run. The priority of OSTaskIdle() is always set to OS_LOWEST_PRIO. If OS_TASK_STAT_EN and OS_TASK_CREATE_EXT_EN (see OS_CFG.H) are both set to 1, OSInit() also creates the statistic task OS_TaskStat() and makes it ready to run. The priority of OS_TaskStat() is always set to OS_LOWEST_PRIO-1.

Figure 3.11 shows the relationship between some µC/OS-II variables and data structures after calling OSInit(). The illustration assumes that the following #define constants are set as follows in OS_CFG.H:

  • OS_TASK_STAT_EN is set to 1,
  • OS_FLAG_EN is set to 1,
  • OS_LOWEST_PRIO is set to 63, and
  • OS_MAX_TASKS is set to 62.

F3.11(1) You will notice that the task control blocks (OS_TCBs) of OS_TaskIdle() and OS_TaskStat() are chained together in a doubly linked list.

F3.11(2) OSTCBList points to the beginning of this chain. When a task is created, it is always placed at the beginning of the list. In other words, OSTCBList always points to the OS_TCB of last task created.

F3.11(3) Both ends of the doubly linked list point to NULL (i.e., 0).

F3.11(4) Because both tasks are ready to run, their corresponding bits in OSRdyTbl[] are set to 1. Also, because the bits of both tasks are on the same row in OSRdyTbl[], only one bit in OSRdyGrp is set to 1.

Figure 3.11 Variables and Data structures after calling OSInit().

µC/OS-II also initializes five pools of free data structures as shown in Figure 3.12. Each of these pools is a singly linked list and allows µC/OS-II to obtain and return an element from and to a pool quickly.

Figure 3.12 Free Pools

After OSInit() has been called, the OS_TCB pool contains OS_MAX_TASKS entries. The OS_EVENT pool contains OS_MAX_EVENTS entries, the OS_Q pool contains OS_MAX_QS entries, the OS_FLAG_GRP pool contains OS_MAX_FLAGS entries and finally, the OS_MEM pool contains OS_MAX_MEM_PART entries. Each of the free pools are NULL pointer terminated to indicate the end. The pool is of course empty if any of the list pointers point to NULL. The size of these pools are defined by you in OS_CFG.H.

3.13. Starting µC/OS-II

You start multitasking by calling OSStart(). However, before you start µC/OS-II, you must create at least one of your application tasks as shown in Listing 3.27.

The code for OSStart() is shown in Listing 3.28.

L3.28(1) When called, OSStart() finds the OS_TCB (from the ready list) of the highest priority task that you have created.

L3.28(2) Then, OSStart() calls OSStartHighRdy() which is found in OS_CPU_A.ASM for the processor being used (see Chapter 13, Porting µC/OS-II). Basically, OSStartHighRdy() restores the CPU registers by popping them off the task’s stack then executes a return from interrupt instruction, which forces the CPU to execute your task’s code. Note that OSStartHighRdy() will never return to OSStart() and after saving the context of the task to suspend.

Figure 3.7 Saving the current task’s context.

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

Figure 3.8 Resuming the current task.

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.

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.

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.

Idle Task

µC/OS-II always creates a task (a.k.a. the idle task) that is executed when none of the other tasks is ready to run. The idle task, OS_TaskIdle(), is always set to the lowest priority, OS_LOWEST_PRIO. The code for the idle task is shown in Listing 3.14. The idle task can never be deleted by application software.

Statistics Task

µC/OS-II contains a task that provides run-time statistics. This task is called OS_TaskStat() and is created by µC/OS-II if you set the configuration constant OS_TASK_STAT_EN (see OS_CFG.H) to 1. When enabled, OS_TaskStat() (see OS_CORE.C) executes every second and computes the percent CPU usage. In other words, OS_TaskStat() tells you how much of the CPU time is used by your application, as a percentage. This value is placed in the signed 8-bit integer variable OSCPUUsage. The resolution of OSCPUUsage is 1 percent.

If your application is to use the statistic task, you must call OSStatInit() (see OS_CORE.C) from the first and only task created in your application during initialization. In other words, your startup code must create only one task before calling OSStart(). From this one task, you must call OSStatInit() before you create your other application tasks. The single task that you create will, of course, be allowed to create other tasks. The pseudocode in Listing 3.15 shows what needs to be done.

Because your application must create only one task, TaskStart(), µC/OS-II has only three tasks to manage when main() calls OSStart(): TaskStart(), OSTaskIdle(), and OS_TaskStat(). Please note that you don’t have to call the startup task: TaskStart() — you can call it anything you like. Your startup task will have the highest priority because µC/OS-II sets the priority of the idle task to OS_LOWEST_PRIO and the priority of the statistic task to OS_LOWEST_PRIO – 1 internally.

Figure 3.9 illustrates the flow of execution when initializing the statistic task.

Figure 3.9 Statistic task initialization.

The code for OSStatInit() is shown in Listing 3.16.

The code for OS_TaskStat() is shown in Listing 3.17.

[3.1]

[3.2]

Multiplying OSIdleCtr by 100 limits the maximum value that OSIdleCtr can take, especially on fast processors. In other words, in order for the multiplication of OSIdleCtr to not overflow, OSIdleCtr must never be higher than 42,949,672! With fast processors, it’s quite likely that OSIdleCtr can reach this value. To correct this potential problem, all we need to do is divide OSIdleCtrMax by 100 instead as shown below.

[3.3]

The local variable max is thus precomputed to hold OSIdleCtrMax divided by 100.

Interrupts under µC/OS-II

µC/OS-II requires that an Interrupt Service Routine (ISR) be written in assembly language. However, if your C compiler supports in-line assembly language, you can put the ISR code directly in a C source file.

The pseudocode for an ISR is shown in Listing 3.18.

The above description is further illustrated in Figure 3.10.

Figure 3.10 Servicing an interrupt.

The code for OSIntEnter() is shown in Listing 3.19 and the code for OSIntExit() is shown in Listing 3.20. Very little needs to be said about OSIntEnter().

OSIntExit() looks strangely like OS_Sched() except for three differences:

You need to call OSIntCtxSw() instead of OS_TASK_SW() because the ISR has already saved the CPU registers onto the interrupted task and thus shouldn’t be saved again. Implementation details about OSIntCtxSw() are provided in Chapter 13, Porting µC/OS-II.

Some processors, like the Motorola 68HC11, require that you implicitly re-enable interrupts in order to allow nesting. This can be used to your advantage. Indeed, if your ISR needs to be serviced quickly and it doesn’t need to notify a task about itself, you don’t need to call OSIntEnter() (or increment OSIntNesting) or OSIntExit() as long as you don’t enable interrupts within the ISR. The pseudocode in Listing 3.21 shows this situation. In this case, the only way a task and this ISR can communicate is through global variables.

Clock Tick

µC/OS-II requires that you provide a periodic time source to keep track of time delays and timeouts. A tick should occur between 10 and 100 times per second, or Hertz. The faster the tick rate, the more overhead µC/OS-II will impose on the system. The actual frequency of the clock tick depends on the desired tick resolution of your application. You can obtain a tick source either by dedicating a hardware timer or generating an interrupt from an AC power line (50/60Hz) signal.

You MUST enable ticker interrupts AFTER multitasking has started; that is, after calling OSStart(). In other words, you should initialize ticker interrupts in the first task that executes following a call to OSStart(). A common mistake is to enable ticker interrupts after OSInit() and before OSStart() as shown in Listing 3.22.

Potentially, the tick interrupt could be serviced before µC/OS-II starts the first task. At this point, µC/OS-II is in an unknown state and your application will crash.

The µC/OS-II clock tick is serviced by calling OSTimeTick() from a tick ISR. OSTimeTick() keeps track of all the task timers and timeouts. The tick ISR follows all the rules described in the previous section. The pseudocode for the tick ISR is shown in Listing 3.23. This code must be written in assembly language because you cannot access CPU registers directly from C. Because the tick ISR is always needed, it is generally provided with a port.

The code for OSTimeTick() is shown in Listing 3.24.

If you don’t like to make ISRs any longer than they must be, OSTimeTick() can be called at the task level as shown in Listing 3.25. To do this, create a task that has a higher priority than all your other application tasks. The tick ISR needs to signal this high-priority task by using either a semaphore or a message mailbox.

You obviously need to create a mailbox (contents initialized to NULL) that will be used to signal the task that a tick interrupt has occurred (Listing 3.26).

µC/OS-II Initialization

A requirement of µC/OS-II is that you call OSInit() before you call any of µC/OS-II’s other services. OSInit() initializes all µC/OS-II variables and data structures (see OS_CORE.C).

OSInit() creates the idle task OSTaskIdle(), which is always ready to run. The priority of OSTaskIdle() is always set to OS_LOWEST_PRIO. If OS_TASK_STAT_EN and OS_TASK_CREATE_EXT_EN (see OS_CFG.H) are both set to 1, OSInit() also creates the statistic task OS_TaskStat() and makes it ready to run. The priority of OS_TaskStat() is always set to OS_LOWEST_PRIO-1.

Figure 3.11 shows the relationship between some µC/OS-II variables and data structures after calling OSInit(). The illustration assumes that the following #define constants are set as follows in OS_CFG.H:

  • OS_TASK_STAT_EN is set to 1,
  • OS_FLAG_EN is set to 1,
  • OS_LOWEST_PRIO is set to 63, and
  • OS_MAX_TASKS is set to 62.

F3.11(1) You will notice that the task control blocks (OS_TCBs) of OS_TaskIdle() and OS_TaskStat() are chained together in a doubly linked list.

F3.11(2) OSTCBList points to the beginning of this chain. When a task is created, it is always placed at the beginning of the list. In other words, OSTCBList always points to the OS_TCB of last task created.

F3.11(3) Both ends of the doubly linked list point to NULL (i.e., 0).

F3.11(4) Because both tasks are ready to run, their corresponding bits in OSRdyTbl[] are set to 1. Also, because the bits of both tasks are on the same row in OSRdyTbl[], only one bit in OSRdyGrp is set to 1.

Figure 3.11 Variables and Data structures after calling OSInit().

µC/OS-II also initializes five pools of free data structures as shown in Figure 3.12. Each of these pools is a singly linked list and allows µC/OS-II to obtain and return an element from and to a pool quickly.

Figure 3.12 Free Pools

After OSInit() has been called, the OS_TCB pool contains OS_MAX_TASKS entries. The OS_EVENT pool contains OS_MAX_EVENTS entries, the OS_Q pool contains OS_MAX_QS entries, the OS_FLAG_GRP pool contains OS_MAX_FLAGS entries and finally, the OS_MEM pool contains OS_MAX_MEM_PART entries. Each of the free pools are NULL pointer terminated to indicate the end. The pool is of course empty if any of the list pointers point to NULL. The size of these pools are defined by you in OS_CFG.H.

Starting µC/OS-II

You start multitasking by calling OSStart(). However, before you start µC/OS-II, you must create at least one of your application tasks as shown in Listing 3.27.

The code for OSStart() is shown in Listing 3.28.

Figure 3.13 shows the contents of the variables and data structures after multitasking has started. Here, I assume that the task you created has a priority of 6. Notice that OSTaskCtr indicates that three tasks have been created: OSRunning is set to TRUE, indicating that multitasking has started, OSPrioCur and OSPrioHighRdy contain the priority of your application task, and OSTCBCur and OSTCBHighRdy both point to the OS_TCB of your task.

Figure 3.13 Variables and data structures after calling OSStart().

...

Obtaining the Current µC/OS-II Version

You can obtain the current version of µC/OS-II from your application by calling OSVersion() (Listing 3.29). OSVersion() returns the version number multiplied by 100. In other words, version 2.52 is returned as 252.

...