/
Porting µC/OS-II

Porting µC/OS-II

This chapter describes in general terms what needs to be done in order to adapt µC/OS-II to different processors. Adapting a real-time kernel to a microprocessor or a microcontroller is called a port. Most of µC/OS-II is written in C for portability; however, it is still necessary to write some processor-specific code in C and assembly language. Specifically, µC/OS-II manipulates processor registers, which can only be done through assembly language. Porting µC/OS-II to different processors is relatively easy because µC/OS-II was designed to be portable. If you already have a port for the processor you are intending to use, you dont need to read this chapter, unless of course you want to know how µC/OS-II processor-specific code works.

µC/OS-II Hardware/Software Architecture

A processor can run µC/OS-II if it satisfies the following general requirements:

  1. The processor has a C compiler that generates reentrant code.
  2. Interrupts can be disabled and enabled from C.
  3. The processor supports interrupts and can provide an interrupt that occurs at regular intervals (typically between 10 and 100Hz).
  4. The processor supports a hardware stack that can accommodate a fair amount of data (possibly many kilobytes).
  5. The processor has instructions to load and store the stack pointer and other CPU registers, either on the stack or in memory.

Processors like the Motorola 6805 series do not satisfy requirements number 4 and 5, so µC/OS-II cannot run on such processors.

Figure 13.1 shows the µC/OS-II architecture and its relationship with the hardware. When you use µC/OS-II in an application, you are responsible for providing the Application Software and the µC/OS-II Configuration sections. This book and companion CD contains all the source code for the Processor-Independent Code section as well as the Processor-Specific Code section for the Intel 80x86, real mode, large model. If you intend to use µC/OS-II on a different processor, you need to either obtain a copy of a port for the processor you intend to use or write one yourself if the desired processor port has not already been ported. Check the Micrium Web site at www.micrium.com for a list of available ports. In fact, you may want to look at other ports and learn from the experience of others.

Figure - Figure 13.1 µC/OS-II hardware/software architecture


Porting µC/OS-II is actually quite straightforward once you understand the subtleties of the target processor and the C compiler you are using. Depending on the processor, a port can consist of writing or changing between 50 and 300 lines of code and could take anywhere from a few hours to about a week to accomplish. The easiest thing to do, however, is to modify an existing port from a processor that is similar to the one you intend to use. Table 3.1 summarizes the code you will have to write or modify. I decided to add a column which indicates the relative complexity involved: 1 means easy, 2 means average and 3 means more complicated.

Table - Table 13.1, Port Summary
NameTypeFileC or Assembly?Complexity
BOOLEANData TypeOS_CPU.HC1
INT8UData TypeOS_CPU.HC1
INT8SData TypeOS_CPU.HC1
INT16UData TypeOS_CPU.HC1
INT16SData TypeOS_CPU.HC1
INT32UData TypeOS_CPU.HC1
INT32SData TypeOS_CPU.HC1
FP32Data TypeOS_CPU.HC1
FP64Data TypeOS_CPU.HC1
OS_STKData TypeOS_CPU.HC2
OS_CPU_SRData TypeOS_CPU.HC2
OS_CRITICAL_METHOD#defineOS_CPU.HC3
OS_STK_GROWTH#defineOS_CPU.HC1
OS_ENTER_CRITICAL()MacroOS_CPU.HC3
OS_EXIT_CRITICAL()MacroOS_CPU.HC3
OSStartHighRdy()FunctionOS_CPU_A.ASMAssembly2
OSCtxSw()FunctionOS_CPU_A.ASMAssembly3
OSIntCtxSw()FunctionOS_CPU_A.ASMAssembly3
OSTickISR()FunctionOS_CPU_A.ASMAssembly3
OSTaskStkInit()FunctionOS_CPU_C.CC3
OSInitHookBegin()FunctionOS_CPU_C.CC1
OSInitHookEnd()FunctionOS_CPU_C.CC1
OSTaskCreateHook()FunctionOS_CPU_C.CC1
OSTaskDelHook()FunctionOS_CPU_C.CC1
OSTaskSwHook()FunctionOS_CPU_C.CC1
OSTaskStatHook()FunctionOS_CPU_C.CC1
OSTCBInitHook()FunctionOS_CPU_C.CC1
OSTimeTickHook()FunctionOS_CPU_C.CC1
OSTaskIdleHook()FunctionOS_CPU_C.CC1


Development Tools

As previously stated, because µC/OS-II is written mostly in ANSI C, you need an ANSI C compiler for the processor you intend to use. Also, because µC/OS-II is a preemptive kernel, you should only use a C compiler that generates reentrant code.

Your tools should also include an assembler because some of the port requires to save and restore CPU registers which are generally not accessible from C. However, some C compilers do have extensions that allow you to manipulate CPU registers directly from C or, allow you to write in-line assembly language statements.

Most C compilers designed for embedded systems also include a linker and a locator. The linker is used to combine object files (compiled and assembled files) from different modules while the locator, allows you to place the code and data anywhere in the memory map of the target processor.

Your C compiler must also provide a mechanism to disable and enable interrupts from C. Some compilers allow you to insert in-line assembly language statements into your C source code. This makes it quite easy to insert the proper processor instructions to enable and disable interrupts. Other compilers actually contain language extensions to enable and disable interrupts directly from C.

Directories and Files

The installation program provided on the distribution diskette installs µC/OS-II and the port for the Intel 80x86 (real mode, large model) on your hard disk. I devised a consistent directory structure that allows you to find the files for the desired target processor easily. If you add a port for another processor, you should consider following the same conventions.

All ports should be placed under \SOFTWARE\uCOS-II on your hard drive. You should note that I dont specify which disk drive these files should reside; I leave this up to you. The source code for each microprocessor or microcontroller port must be found in either two or three files: OS_CPU.H, OS_CPU_C.C, and, optionally, OS_CPU_A.ASM. The assembly language file is optional because some compilers allow you to have in-line assembly language, so you can place the needed assembly language code directly in OS_CPU_C.C. The directory in which the port is located determines which processor you are using. Examples of directories where different ports would be stored are shown in the Table 13.2. Note that each directory contains the same filenames, even though they have totally different targets. Also, the directory structure accounts for different C compilers. For example, the µC/OS-II port files for the Paradigm C (see www.DevTools.com) compiler would be placed in a Paradigm sub-directory. Similarly, the port files for the Borland C (see www.Borland.com) compiler V4.5 would be placed in a BC45 sub-directory. The port files for other processors such as the Motorola 68HC11 processor using a COSMIC compiler (see www.Cosmic-US.com) would be placed as shown in Table 13.2.

Table - Table 13.2, Examples of Port Directories
Intel/AMD 80186\SOFTWARE\uCOS-II\Ix86L\PARADIGM
    \OS_CPU.H
    \OS_CPU_A.ASM
    \OS_CPU_C.C

\SOFTWARE\uCOS-II\Ix86L\BC45
    \OS_CPU.H
    \OS_CPU_A.ASM
    \OS_CPU_C.C
Motorola 68HC11\SOFTWARE\uCOS-II\68HC11\COSMIC
    \OS_CPU.H
    \OS_CPU_A.ASM
    \OS_CPU_C.C


INCLUDES.H

As mentioned in Chapter 1, INCLUDES.H is a master include file found at the top of all .C files:


#include "includes.h"


INCLUDES.H allows every .C file in your project to be written without concern about which header file will actually be needed. The only drawback to having a master include file is that INCLUDES.H may include header files that are not pertinent to the actual .C file being compiled. This means that each file will require extra time to compile. This inconvenience is offset by code portability. I assume that you would have an INCLUDES.H in each project that uses µC/OS-II. You can thus edit the INCLUDES.H file that I provide to add your own header files, but your header files should be added at the end of the list. INCLUDES.H is not actually considered part of a port but, I decided to mention it here because every µC/OS-II file assumes it.

OS_CPU.H

OS_CPU.H contains processor- and implementation-specific #defines constants, macros, and typedefs. The general layout of OS_CPU.H is shown in Listing 13.1.

Listing - Listing 13.1
/*
**********************************************************************************
*                                  DATA TYPES
*                             (Compiler Specific)
**********************************************************************************
*/
 
typedef unsigned char  BOOLEAN;                                               (1)
typedef unsigned char  INT8U;       /* Unsigned  8 bit quantity           */
typedef signed   char  INT8S;       /* Signed    8 bit quantity           */
typedef unsigned int   INT16U;      /* Unsigned 16 bit quantity           */
typedef signed   int   INT16S;      /* Signed   16 bit quantity           */
typedef unsigned long  INT32U;      /* Unsigned 32 bit quantity           */
typedef signed   long  INT32S;      /* Signed   32 bit quantity           */
typedef float          FP32;        /* Single precision floating point    */  (2)
typedef double         FP64;        /* Double precision floating point    */
 
typedef unsigned int   OS_STK;      /* Each stack entry is 16-bit wide    */  (3)
typedef unsigned short OS_CPU_SR;   /* Define size of CPU status register */  (4)
 
/* 
*********************************************************************************
*                             Processor Specifics
*********************************************************************************
*/
#define  OS_CRITICAL_METHOD   ??                                              (5)
 
#if      OS_CRITICAL_METHOD == 1
#define  OS_ENTER_CRITICAL()  ????                                            (6)
#define  OS_EXIT_CRITICAL()   ????
#endif
 
#if      OS_CRITICAL_METHOD == 2
#define  OS_ENTER_CRITICAL()  ????                                            (7)
#define  OS_EXIT_CRITICAL()   ????
#endif
 
#if      OS_CRITICAL_METHOD == 3
#define  OS_ENTER_CRITICAL()  ????                                            (8)
#define  OS_EXIT_CRITICAL()   ????
#endif
 
 
#define  OS_STK_GROWTH        1          /* Stack growth (0=Up, 1=Down) */    (9)
 
#define  OS_TASK_SW()         ????                                           (10)


Compiler-Specific Data Types

Because different microprocessors have different word lengths, the port of µC/OS-II includes a series of type definitions that ensures portability. Specifically, µC/OS-II code never makes use of Cs short, int, and long data types because they are inherently nonportable.

To complete the data type section, you simply need to consult your compiler documentation and find the standard C data types that correspond to the types expected by µC/OS-II.


(1) Instead, I defined integer data types that are both portable and intuitive. The INT16U data type, for example, always represents a 16-bit unsigned integer. µC/OS-II and your application code can now assume that the range of values for variables declared with this type is from 0 to 65,535. A µC/OS-II port to a 32-bit processor could mean that an INT16U is actually declared as an unsigned short instead of an unsigned int. Where µC/OS-II is concerned, however, it still deals with an INT16U. All you have to do is determine from your compiler documentation what combination of standard C data types map to the data types µC/OS-II expects.

(2) Also, for convenience, I have included floating-point data types even though µC/OS-II doesnt make use of floating-point numbers.

(3) You must tell µC/OS-II the data type of a tasks stack. This is done by declaring the proper C data type for OS_STK. If stack elements on your processor are 32 bits you can simply declare OS_STK as:

typedef INT32U OS_STK;

This assumes that the declaration of INT32U precedes that of OS_STK. When you create a task and you declare a stack for this task then, you MUST always use OS_STK as its data type.

(4) If you use OS_CRITICAL_METHOD #3 (see next section), you will need to declare the data type for the Processor Status Word (PSW) . The PSW is also called the processor flags or status register. If the PSW of your processor is 16 bit wide, simply declare it as:

typedef INT16U OS_CPU_SR;


OS_ENTER_CRITICAL(), and OS_EXIT_CRITICAL()

This section is basically a repeat of section 3.00 with some items removed and others added. I decided to repeat this text here to avoid having you flip back and forth between sections. µ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.

Processors generally provide instructions to disable/enable interrupts, and your C compiler must have a mechanism to perform these operations directly from C. Some compilers allow you to insert in-line assembly language statements into your C source code. This makes it quite easy to insert processor instructions to enable and disable interrupts. Other compilers contain language extensions to enable and disable interrupts directly from C.

To hide the implementation method chosen by the compiler manufacturer, µC/OS-II defines two macros to disable and enable interrupts: OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL(), respectively (see L13.1(5) through L13.1(8)).

OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() are always used in pair to wrap critical sections of code as shown in listing 13.2.

Listing - Listing 13.2 Use of critical section
{
    .
    .
    OS_ENTER_CRITICAL();
    /* μC/OS-II critical code section */
    OS_EXIT_CRITICAL();
    .
    .
}


Your application can also use OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() to protect your own critical sections of code. Be careful, however, because your application will crash (i.e., hang) if you disable interrupts before calling a service such as OSTimeDly() (see chapter 5). This happens because the task is suspended until time expires, but because interrupts are disabled, you would never service the tick interrupt! Obviously, all the PEND calls are also subject to this problem, so be careful. As a general rule, you should always call µC/OS-II services with interrupts enabled!

OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() can be implemented using three different methods. You only need one of the three methods even though I show OS_CPU.H (Listing 13.1) containing three different methods. The actual method used by your application depends on the capabilities of the processor as well as the compiler used. The method used is selected by the #define constant OS_CRITICAL_METHOD which is defined in OS_CPU.H of the port you will be using for your application (i.e., product). The #define constant OS_CRITICAL_METHOD is necessary in OS_CPU.H because µC/OS-II allocates a local variable called cpu_sr if OS_CRITICAL_METHOD is set to 3.

OS_CRITICAL_METHOD == 1

The first and simplest way to implement these two macros is to invoke the processor instruction to disable interrupts for OS_ENTER_CRITICAL() and the enable interrupts instruction for OS_EXIT_CRITICAL(). However, there is a little problem with this scenario. If you call a µC/OS-II function with interrupts disabled, on return from a µC/OS-II service (i.e., function), interrupts would be enabled! If you had disabled interrupts prior to calling µC/OS-II, you may want them to be disabled on return from the µC/OS-II function. In this case, this implementation would not be adequate. However, with some processors/compilers, this is the only method you can use. An example declaration is shown in listing 13.3. Here, I assume that the compiler you are using provides you with two functions to disable and enable interrupts, respectively. The names disable_int() and enable_int() are arbitrarily chosen for sake of illustration. You compiler may have different names for them.

Listing - Listing 13.3 Critical Method #1
#define OS_ENTER_CRITICAL() disable_int() /* Disable interrupts */
#define OS_EXIT_CRITICAL() enable_int() /* Enable interrupts */


OS_CRITICAL_METHOD == 2

The second way to implement OS_ENTER_CRITICAL() is to save the interrupt disable status onto the stack and then disable interrupts. OS_EXIT_CRITICAL() is implemented by restoring the interrupt status from the stack. Using this scheme, if you call a µC/OS-II service with interrupts either enabled or disabled, the status is preserved across the call. In other words, interrupts would be enabled after the call if they were enabled before the call and, interrupts would be disabled after the call if they were disabled before the call. Be careful when you call a µC/OS-II service with interrupts disabled because you are extending the interrupt latency of your application. The pseudo code for these macros is shown in Listing 13.4.

Listing - Listing 13.4 Critical Method #2
#define OS_ENTER_CRITICAL() \
       asm( PUSH PSW);    \
       asm( DI);

#define OS_EXIT_CRITICAL() \
       asm( POP PSW);


Here, I'm assuming that your compiler will allow you to execute inline assembly language statements directly from your C code as shown above (thus the asm() pseudo-function). You will need to consult your compiler documentation for this.

The PUSH PSW instruction pushes the Processor Startus Word, PSW (also known as the condition code register or, processor flags) onto the stack. The DI instruction stands for Disable Interrupts. Finally, the POP PSW instruction is assumed to restore the original state of the interrupt flag from the stack. The instructions I used are only for illustration purposes and may not be actual processor instructions.

Some compilers do not optimize inline code real well and thus, this method may not work because the compiler may not be smart enough to know that the stack pointer was changed (by the PUSH instruction). Specifically, the processor you are using may provide a stack pointer relative addressing mode which the compiler can use to access local variables or function arguments using and offset from the stack pointer. Of course, if the stack pointer is changed by the OS_ENTER_CRITICAL() macro then all these stack offsets may be wrong and would most likely lead to incorrect behavior.

OS_CRITICAL_METHOD == 3

Some compiler provides you with extensions that allow you to obtain the current value of the PSW (Processor Status Word) and save it into a local variable declared within a C function. The variable can then be used to restore the PSW back as shown in listing 13.5.

Listing - Listing 13.5 Saving and restoring the PSW
void Some_uCOS_II_Service (arguments)
{ 
    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)
    .
}

(1) OS_CPU_SR is a µ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 OS_EXIT_CRITICAL() always assume the presence of the cpu_sr variable. In other words, if you use this method to protect your own critical sections, you will need to declare a cpu_sr variable in your function. However, you will not need to declare this variable in any of the µC/OS-II functions because thats already done.

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

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

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

(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. Its assumed that this function will restore the processor PSW to this value.


Because I dont 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:

Listing - Listing 13.6 Critical Method #3
#define OS_ENTER_CRITICAL() \
        cpu_sr = get_processor_psw(); \
        disable_interrupts();

#define OS_EXIT_CRITICAL() \
        set_processor_psw(cpu_sr);


OS_STK_GROWTH

The stack on most microprocessors and microcontrollers grows from high to low memory. However, some processors work the other way around.


(9) µC/OS-II has been designed to be able to handle either flavor by specifying which way the stack grows through the configuration constant OS_STK_GROWTH, as shown below.

Set OS_STK_GROWTH to 0 for low to high memory stack growth.

Set OS_STK_GROWTH to 1 for high to low memory stack growth.

The reason this #define constant is provided is twofold. First, OSInit() needs to know where the top-of-stack is when its creating OSTaskIdle() and OSTaskStat(). Second, if you call OSTaskStkChk(), µC/OS-II needs to know where the bottom of stack is (high-memory or low-memory) in order to determine stack usage.


OS_TASK_SW()


(10) OS_TASK_SW()is a macro that is invoked when µC/OS-II switches from a low-priority task to the highest priority task. OS_TASK_SW() is always called from task-level code. Another mechanism, OSIntExit(), is used to perform a context switch when an ISR makes a higher priority task ready for execution. 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 to restore all processor registers from the tasks stack and execute a return from interrupt. You thus need to implement OS_TASK_SW() to simulate an interrupt. Most processors provide either software interrupt or TRAP instructions to accomplish this. The ISR or trap handler (also called the exception handler) must vector to the assembly language function OSCtxSw() (see section 13.04.02).

For example, a port for an Intel or AMD 80x86 processor would use an INT instruction as shown in listing 13.7. The interrupt handler needs to vector to OSCtxSw(). You must determine how to do this with your compiler/processor.

Listing - Listing 13.7 Task level context switch macro
#define OS_TASK_SW() asm INT 080H


A port for the Motorola 68HC11 processor would most likely uses the SWI instruction. Again, the SWI handler is OSCtxSw() . Finally, a port for a Motorola 680x0/CPU32 processor probably uses one of the 16 TRAP instructions. Of course, the selected TRAP handler is none other than OSCtxSw() .

Some processors, like the Zilog Z80, do not provide a software interrupt mechanism. In this case, you need to simulate the stack frame as closely to an interrupt stack frame as you can. OS_TASK_SW() would simply call OSCtxSw() instead of vectoring to it. The Z80 is a processor that has been ported to µC/OS and is thus portable to µC/OS-II.

OS_CPU_C.C

A µC/OS-II port requires that you write ten (10) fairly simple C functions:

OSTaskStkInit()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskSwHook()
OSTaskIdleHook()
OSTaskStatHook()
OSTimeTickHook()
OSInitHookBegin()
OSInitHookEnd()
OSTCBInitHook()

The only required function is OSTaskStkInit(). The other nine functions must be declared but may not need to contain any code. Function prototypes as well as a reference manual type summary is provided at the end of this chapter.

OSTaskStkInit()

This function is called by OSTaskCreate() and OSTaskCreateExt() to initialize the stack frame of a task so that the stack looks as if an interrupt just occurred and all the processor registers were pushed onto that stack. The pseudo code for OSTaskStkInit() is shown in listing 13.8.

Listing - Listing 13.8 Pseudo-code for OSTaskStkInit
OS_STK *OSTaskStkInit (void  (*task)(void *pd), 
                       void   *pdata, 
                       OS_STK *ptos, 
                       INT16U  opt);
{
    Simulate call to function with an argument (i.e., pdata);               (1)
    Simulate ISR vector;                                                    (2)
    Setup stack frame to contain desired initial values of all registers;   (3)
    Return new top-of-stack pointer to caller;                              (4)
}


Figure 13.2 shows what OSTaskStkInit() needs to put on the stack of the task being created. Note that I assume a stack grows from high to low memory. The discussion that follows applies just as well for a stack growing in the opposite direction.

Figure 13.2 Stack frame initialization with pdata passed on the stack.

Listing 13.9 shows the function prototypes for OSTaskCreate(), OSTaskCreateExt() and OSTaskStkInit(). The arguments in bold font are passed from the create calls to OSTaskStkInit(). When OSTaskCreate() calls OSTaskStkInit(), it sets the opt argument to 0x0000 because OSTaskCreate() doesnt support additional options.

Listing - Listing 13.9 Function prototypes
INT8U  OSTaskCreate (void  (*task)(void *pd), 
                     Void   *pdata, 
                     OS_STK *ptos, 
                     INT8U   prio)
 
INT8U  OSTaskCreateExt (void   (*task)(void *pd),
                        void    *pdata,
                        OS_STK  *ptos,
                        INT8U    prio,
                        INT16U   id,
                        OS_STK  *pbos,
                        INT32U   stk_size,
                        void    *pext,
                        INT16U   opt)
 
OS_STK *OSTaskStkInit (void  (*task)(void *pd), 
                       void   *pdata, 
                       OS_STK *ptos, 
                       INT16U  opt);


Figure - Figure 13.2: Stack-frame initialization with pdata passed to the stack

Recall that under µC/OS-II, a task is an infinite loop but otherwise looks just like any other C function. When the task is started by µC/OS-II, it receives an argument just as if it was called by another function as shown in Listing 13.10.

Listing - Listing 13.10 Task Code
void MyTask (void *pdata)
{
    /* Do something with argument 'pdata' */
    for (;;) {
        /* Task code                      */
    }
}


If I were to call MyTask() from another function, the C compiler would push the argument onto the stack followed by the return address of the function calling MyTask() . OSTaskStkInit() needs to simulate this behavior. Some compilers actually pass pdata in one or more registers. Ill discuss this situation later.


The notes below apply both and simultaneously to Listing 13.8 and Figure 13.2. When reading each numbered note, refer to both the listing and the figure.

(1) F13.2

(1) L13.8 - Assuming pdata is pushed onto the stack, OSTaskStkInit() simply simulates this scenario and loads the stack accordingly.

(2) F13.2

(1) L13.8 - Unlike a C function call, the return address of the caller is unknown because your task was never really called (we are just trying to setup the stack frame of a task, as if the code was called). All OSTaskStkInit() knows about is the start address of your task (its passed as an argument). It turns out that you dont really need the return address because the task is not supposed to return to another function anyway.

(3) F13.2

(2) L13.8 - At this point, OSTaskStkInit() needs to put on the stack the registers that are automatically pushed by the processor when it recognizes and starts servicing an interrupt. Some processors stack all of its registers; others stack just a few. Generally speaking, a processor stacks at least the value of the program counter of the instruction to return to upon returning from an interrupt, and the processor status word. Obviously, you must match the order exactly.

(4) F13.2

(3) L13.8 - Next, OSTaskStkInit() need to put the rest of the processor registers on the stack. The stacking order depends on whether your processor gives you a choice or not. Some processors have one or more instructions that push many registers at once. You would have to emulate the stacking order of such instructions. For example, the Intel 80x86 has the PUSHA instruction, which pushes eight registers onto the stack. On the Motorola 68HC11 processor, all the registers are automatically pushed onto the stack during an interrupt response, so you would also need to match the stacking order.

(5) F13.2

(4) L13.8 - Once youve initialized the stack, OSTaskStkInit() needs to return the address where the stack pointer points after the stacking is complete. OSTaskCreate() or OSTaskCreateExt() takes this address and saves it in the task control block (OS_TCB). The processor documentation tells you whether the stack pointer should point to the next free location on the stack or the location of the last stored value. For example, on an Intel 80x86 processor, the stack pointer points to the last stored data, whereas on a Motorola 68HC11 processor, it points at the next free location.


Now its time to come back to the issue of what to do if your C compiler passes the pdata argument in registers instead of on the stack.


(1) Similar to the previous case, OSTaskStkInit() saves the task address onto the stack in order to simulate a call to your task code.

(2) Again, OSTaskStkInit() needs to put on the stack the registers that are automatically pushed by the processor when it recognizes and starts servicing an interrupt. Some processors stack all of its registers; others stack just a few. Generally speaking, a processor stacks at least the value of the program counter for the instruction to return to upon returning from an interrupt, and the processor status word. Obviously, you must match the order exactly.

(3) Next, OSTaskStkInit() need to put the rest of the processor registers on the stack. The stacking order depends on whether your processor gives you a choice or not. Some processors have one or more instructions that push many registers at once. You would have to emulate the stacking order of such instructions. Because the compiler passed arguments to a function in registers (at least some of them), you need to find out from the compiler documentation the register in which pdata is stored. pdata is placed on the stack in the same area you save the corresponding register.

(4) Once youve initialized the stack, OSTaskStkInit() needs to return the address where the stack pointer points after the stacking is complete. OSTaskCreate() or OSTaskCreateExt() takes this address and saves it in the task control block (OS_TCB). Again, the processor documentation tells you whether the stack pointer should point to the next free location on the stack or the location of the last stored value.


Figure - Figure 13.3 Stack frame initialization with pdata passed in register

OSTaskCreateHook()

OSTaskCreateHook() is called by OS_TCBInit() whenever a task is created. This allows you or the user of your port to extend the functionality of µC/OS-II. OSTaskCreateHook() is called when µC/OS-II is done setting up most of the OS_TCB but before the OS_TCB is linked to the active task chain and before the task is made ready to run. Interrupts are enabled when this function is called.

When called, OSTaskCreateHook() receives a pointer to the OS_TCB of the task created and can thus access all of the structure elements. OSTaskCreateHook() has limited capability when the task is created with OSTaskCreate(). However, with OSTaskCreateExt(), you get access to a TCB extension pointer (OSTCBExtPtr) in OS_TCB that can be used to access additional data about the task, such as the contents of floating-point registers, MMU (Memory Management Unit) registers, task counters, and debug information. You may want to examine OS_TCBInit() to see exactly whats being done.

Note about OS_CPU_HOOKS_EN: The code for the hook functions (OS???Hook()) that are described in this and the following sections is generated from the file OS_CPU_C.C only if OS_CPU_HOOKS_EN is set to 1 in OS_CFG.H. The OS???Hook() functions are always needed and the #define constant OS_CPU_HOOKS_EN doesnt mean that the code will not be called. All OS_CPU_HOOKS_EN means is that the hook functions are in OS_CPU_C.C (when 1) or elsewhere, in another file (when 0). This allows the user of your port to redefine all the hook functions in a different file. Obviously, users of your port need access to the source to compile it with OS_CPU_HOOKS_EN set to 0 in order to prevent multiply defined symbols at link time. If you dont need to use hook functions because you dont intend to extend the functionality of µC/OS-II through this mechanism then you can simply leave the function bodies empty. Again, µC/OS-II always expects that the hook functions exist (i.e., they must ALWAYS be declared somewhere).

OSTaskDelHook()

OSTaskDelHook() is called by OSTaskDel() after removing the task from either the ready list or a wait list (if the task was waiting for an event to occur). It is called before unlinking the task from µC/OS-IIs internal linked list of active tasks. When called, OSTaskDelHook() receives a pointer to the task control block (OS_TCB) of the task being deleted and can thus access all of the structure members. OSTaskDelHook() can see if a TCB extension has been created (a non-NULL pointer) and is thus responsible for performing cleanup operations. OSTaskDelHook() is called with interrupts disabled which means that your OSTaskDelHook() can affect interrupt latency if its too long. You may want to study OSTaskDel() and see exactly what is accomplised before OSTaskDelHook() is called.

OSTaskSwHook()

OSTaskSwHook() is called whenever a task switch occurs. This happens whether the task switch is performed by OSCtxSw() or OSIntCtxSw() (see OS_CPU_A.ASM). OSTaskSwHook() can access OSTCBCur and OSTCBHighRdy directly because they are global variables. OSTCBCur points to the OS_TCB of the task being switched out, and OSTCBHighRdy points to the OS_TCB of the new task. Note that interrupts are always disabled during the call to OSTaskSwHook(), so you should keep additional code to a minimum since it will affect interrupt latency. OSTaskSwHook() has no arguments and is not expected to return anything.

OSTaskStatHook()

OSTaskStatHook() is called once every second by OSTaskStat(). You can thus extend the statistics capability with OSTaskStatHook(). For instance, you can keep track of and display the execution time of each task, the percentage of the CPU that is used by each task, how often each task executes, and more. OSTaskStatHook() has no arguments and is not expected to return anything. You may want to study OS_TaskStat().

OSTimeTickHook()

OSTaskTimeHook() is called by OSTimeTick() at every system tick. In fact, OSTimeTickHook() is called before a tick is actually processed by µC/OS-II to give your port or application first claim of the tick. OSTimeTickHook() has no arguments and is not expected to return anything.

OSTCBInitHook()

OSTCBInitHook() is called by OS_TCBInit() immediately before calling OSTaskCreateHook() which is also called by OS_TCBInit(). I did this so that you could initialize OS_TCB related data with OSTCBInitHook() and task related data with OSTaskCreateHook() (there may be a difference). Its up to you to decide whether you need to populate both of these functions. Like OSTaskCreateHook(), OSTCBInitHook() receives a pointer to the newly created tasks OS_TCB after initializing most of the field, but before linking the OS_TCB to the chain of created tasks. You may want to examine OS_TCBInit().

OSTaskIdleHook()

Many microprocessors allow you to execute instructions that brings the CPU into a low-power mode. The CPU exits low-power mode when it receives an interrupt. OSTaskIdleHook() is called by OS_TaskIdle() and, as shown in Listing 13.11, can be made to use this CPU feature.

Listing - Listing 13.11 Use of OSTaskIdleHook()
void  OS_TaskIdle (void *pdata)
{
#if OS_CRITICAL_METHOD == 3                      
    OS_CPU_SR  cpu_sr;
#endif    
    
    
    pdata = pdata;                               
    for (;;) {
        OS_ENTER_CRITICAL();
        OSIdleCtr++;                                      (1)
        OS_EXIT_CRITICAL();
        OSTaskIdleHook();                                 (2)
    }
}
 
void  OSTaskIdleHook (void)
{
    asm( STOP);                                         (3)
    /* Interrupt received and serviced */                 (4)                                   
}

(1) As you know, OS_TaskIdle() is executed whenever no other task is ready to run. OS_TaskIdle() increments the idle counter, OSIdleCtr.

(2) Next OS_TaskIdle() calls the hook function OSTaskIdleHook() that you would declare in the port file OS_CPU_C.C.

(3) OSTaskIdleHook() immediately invokes the CPU instruction to bring the CPU in low-power mode. I assumed, for sake of illustration, that your compiler supports inline assembly language and that the instruction to execute is called STOP. Other compilers may not allow you to do inline assembly language and, in those cases, you could declare OSTaskIdleHook() in the assembly language file OS_CPU_A.ASM but make sure you include a return from the call. Also, the instruction to bring the CPU in low-power mode may be called something else.

(4) When an interrupt occurs, the CPU exits low-power mode and processes the ISR (Interrupt Service Routine). The ISR signals a higher priority task which executes upon completion of the ISR because the ISR calls OSIntExit(). When all tasks are again waiting for events to occur, µC/OS-II switches back to the idle task immediately after item L13.9(4) and OSTaskIdleHook() returns to OS_TaskIdle() and the same process repeats.


You could also use OSTaskIdleHook() to blink an LED (Light Emitting Diode) which could be used as an indication of how busy the CPU is. A dim LED would indicate a very busy CPU while a bright LED indicates a lightly loaded CPU.

OSInitHookBegin()

OSInitHookBegin() is called immediately upon entering OSInit(). The reason I added this function is to encapsulate OS related initialization within OSInit(). This allows you to extend OSInit() with your own port specific code. The user of your port still only sees OSInit() and thus makes the code cleaner.

OSInitHookEnd()

OSInitHookEnd() is similar to OSInitHookBegin() except that the hook is called at the end of OSInit() just before returning to OSInit()s caller. The reason is the same as above and you can see an example of the use of OSInitHookEnd() in Chapter 15, 80x86 with Floating-Point.

OS_CPU_A.ASM

A µC/OS-II port requires that you write four assembly language functions:

OSStartHighRdy()
OSCtxSw()
OSIntCtxSw()
OSTickISR()

If your compiler supports in-line assembly language code, you could actually place these functions in OS_CPU_C.C instead of having a separate assembly language file.

OSStartHighRdy()

This function is called by OSStart() to start the highest priority task ready to run. The pseudo-code for this function is shown in Listing 13.12. You need to convert this pseudo-code to assembly language.

Listing - Listing 13.12 Pseudo-code for OSStartHighRdy()
void OSStartHighRdy (void)
{
    Call user definable OSTaskSwHook();                               (1)    
    OSRunning = TRUE;                                                 (2)
    Get the stack pointer of the task to resume:                      (3)     
        Stack pointer = OSTCBHighRdy->OSTCBStkPtr;
 
    Restore all processor registers from the new task's stack;        (4)  
    Execute a return from interrupt instruction;                      (5)  
}

(1) OSStartHighRdy() must call OSTaskSwHook(). However, OSStartHighRdy() only does half a context switch — you are only restoring the registers of the highest priority task and NOT saving the register of a task. OSTaskSwHook() can examine OSRunning to tell it whether OSTaskSwHook() was called from OSStartHighRdy() (OSRunning is FALSE) or from a regular context switch (OSRunning is TRUE).

(2) OSStartHighRdy() sets OSRunning to TRUE before the highest priority task is restored, but after calling OSTaskSwHook().

You should note that I should have placed the previous two statements in OSStart() instead of requiring that they be placed in OSStartHighRdy() because they dont need to be done in assembly language. Unfortunately, I didnt notice this fact when I first wrote OSStart(). If I were to change OSStart() at this point, a large number of ports may not work properly. I have thus decided to leave these statements in OSStartHighRdy() in order to avoid a lot of e-mails!

(3) OSStartHighRdy() then needs to load the stack pointer of the CPU with the top-of-stack pointer of the highest priority task. OSStartHighRdy() assumes that OSTCBHighRdy points to the task control block of the task with the highest priority. To simplify things, the stack pointer is always stored at the beginning of the task control block (i.e., its OS_TCB). In other words, the stack pointer of the task to resume is always stored at offset 0 in the OS_TCB.

(4) 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. To run the highest priority task, all you need to do is restore all processor registers from the tasks stack in the proper order and execute a return from interrupt. In this step, OSStartHighRdy()retrieves the contents of all the CPU registers from the stack. Its important to pop the registers in the reverse order from how they were placed onto the stack by OSTaskStkInit() (see Section 13.??, OSTaskStkInit()).

(5) The last step is to execute a return from interrupt instruction which causes the CPU to retrieve the program counter and possibly the CPU flags register (also called the status register) from the stack. This causes the CPU to resume execution at the first instruction of the highest priority task.


Before you can call OSStart(), however, you must have created at least one of your tasks [see OSTaskCreate() and OSTaskCreateExt()].

OSCtxSw()

A task-level context switch is accomplished by issuing a software interrupt instruction or, depending on the processor, executing a TRAP instruction. The interrupt service routine, trap, or exception handler must vector to OSCtxSw().

The sequence of events that leads µC/OS-II to vector to OSCtxSw() begins when the current task calls a service provided by µC/OS-II, which causes a higher priority task to be ready to run. At the end of the service call, µC/OS-II calls OS_Sched(), which concludes that the current task is no longer the most important task to run. OS_Sched() loads the address of the highest priority task into OSTCBHighRdy then executes the software interrupt or trap instruction by invoking the macro OS_TASK_SW(). Note that the variable OSTCBCur already contains a pointer to the current tasks task control block, OS_TCB. The software interrupt instruction (or TRAP) forces some of the processor registers (most likely the return address and the processors status word) onto the current tasks stack, then the processor vectors to OSCtxSw().

The pseudocode for OSCtxSw() is shown in Listing 13.13. This code must be written in assembly language because you cannot access CPU registers directly from C. Note that interrupts are disabled during OSCtxSw() and also during execution of the user-definable function OSTaskSwHook(). When OSCtxSw() is invoked, it is assumed that the processors program counter (PC) and possibly the flag register (or status register) are pushed onto the stack by the software interrupt instruction which is invoked by the OS_TASK_SW() macro.

Listing - Listing 13.13 Pseudocode for OSCtxSw()
void OSCtxSw(void)
{
    Save processor registers;                                                (1)
    Save the current tasks stack pointer into the current tasks OS_TCB:      (2)
        OSTCBCur->OSTCBStkPtr = Stack pointer;
    Call user definable OSTaskSwHook();                                      (3)
    OSTCBCur  = OSTCBHighRdy;                                                (4)
    OSPrioCur = OSPrioHighRdy;                                               (5)
    Get the stack pointer of the task to resume:                             (6)
        Stack pointer = OSTCBHighRdy->OSTCBStkPtr;
    Restore all processor registers from the new tasks stack;                (7)
    Execute a return from interrupt instruction;                             (8)
}

(1) OSCtxSw() saves all the processor registers (except the ones already saved by the software interrupt) in the SAME order as they are placed on the stack by OSTaskStkInit().

(2) Once all CPU registers are on the stack of the task to suspend, OSCtxSw() saves the stack pointer into the tasks OS_TCB.

(3) OSCtxSw() calls OSTaskSwHook() in case your port needs to extend the functionality of a context switch. Note that OSTaskSwHook() is ALWAYS called whether this function is declared in OS_CPU_C.C or elsewhere.

(4) OSCtxSw() then needs to make the pointer to the current OS_TCB point to the OS_TCB of the task being resumed. In other words, the new task will become the current task.

(5) O