Versions Compared

Key

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

This chapter describes in general terms what needs to be done in order to adapt μCµC/OS-II to different processors. Adapting a real-time kernel to a microprocessor or a microcontroller is called a port. Most of μCµ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µC/OS-II manipulates processor registers, which can only be done through assembly language. Porting μCµC/OS-II to different processors is relatively easy because μCµ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µC/OS-II processor-specific code works.

Table of Contents
maxLevel2

...

µC/OS-II Hardware/Software Architecture

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

...

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

Figure 13.1 shows the μCµC/OS-II architecture and its relationship with the hardware. When you use μCµC/OS-II in an application, you are responsible for providing the Application Software and the μCµ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µ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.

Porting μCµ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.

Development Tools

As previously stated, because μCµ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µC/OS-II is a preemptive kernel, you should only use a C compiler that generates reentrant code.

...

The installation program provided on the distribution diskette installs μCµ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µ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.

...

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

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

...

Because different microprocessors have different word lengths, the port of μCµC/OS-II includes a series of type definitions that ensures portability. Specifically, μCµ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µC/OS-II.

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µ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µC/OS-II to protect critical code from being entered simultaneously from either multiple tasks or ISRs.

...

To hide the implementation method chosen by the compiler manufacturer, μCµ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.

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µ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µC/OS-II allocates a local variable called cpu_sr if OS_CRITICAL_METHOD is set to 3.

...

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µC/OS-II function with interrupts disabled, on return from a μCµC/OS-II service (i.e., function), interrupts would be enabled! If you had disabled interrupts prior to calling μCµC/OS-II, you may want them to be disabled on return from the μCµ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.

...

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

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.

...

Because I dont know what the compiler functions are (there is no standard naming convention), the μCµC/OS-II macros are used to encapsulate the functionality as follows:

...

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

OS_TASK_SW()

In μCµ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µ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.

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µC/OS and is thus portable to μCµC/OS-II.

OS_CPU_C.C

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

...

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.

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.

Recall that under μCµ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µC/OS-II, it receives an argument just as if it was called by another function as shown in Listing 13.10.

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.

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.

...

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µC/OS-II. OSTaskCreateHook() is called when μCµ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.

...

Note

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µC/OS-II through this mechanism then you can simply leave the function bodies empty. Again, μCµC/OS-II always expects that the hook functions exist (i.e., they must ALWAYS be declared somewhere).

...

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

...

OSTaskTimeHook() is called by OSTimeTick() at every system tick. In fact, OSTimeTickHook() is called before a tick is actually processed by μCµ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.

...

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µC/OS-II port requires that you write four assembly language functions:

...

The sequence of events that leads μCµC/OS-II to vector to OSCtxSw() begins when the current task calls a service provided by μCµC/OS-II, which causes a higher priority task to be ready to run. At the end of the service call, μCµ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.

OSTickISR()

μCµC/OS-II requires you to 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. To accomplish this, either dedicate a hardware timer or obtain 50/60Hz from an AC power line.

You must enable ticker interrupts after multitasking has started; that is, after calling OSStart(). Note that you really cant do this because OSStart() never returns. However, you can and should initialize and tick interrupts in the first task that executes following a call to OSStart(). This would of course be the highest priority task that you would have created before calling OSStart(). A common mistake is to enable ticker interrupts between calling OSInit() and OSStart(), as shown in Listing 13.14. This is a problem because the tick interrupt could be serviced before μCµC/OS-II starts the first task and, at that point, μCµC/OS-II is in an unknown state and your application could crash.

...

A lot of the code is identical to OSCtxSw() except that we dont save the CPU registers onto the current task because thats already done by the ISR. In fact, you can reduce the amount of code in the port by jumping to the appropriate section of code in OSCtxSw() if you want. Because of the similarity between OSCtxSw() and OSIntCtxSw(), once you figure out how to do OSCtxSw(), you have automatically figured out how to do OSIntCtxSw()!

Listing 13.17 shows the pseudocode for OSIntCtxSw() for a port made for a version of μCµC/OS-II prior to V2.51. You will recognize such a port because of the added two items before calling OSTaskSwHook() : L13.17(1) and L13.17(2). ISRs for such a port also would not have the statements shown in L13.15(3) to save the stack pointer into the OS_TCB of the interrupted task. Because of this, OSIntCtxSw() had to do these operations (again, L13.17(1) and L13.17(2)). However, because the stack pointer was not pointing to the proper stack frame location (when OSIntCtxSw() starts executing, the return address of OSIntExit() and OSIntCtxSw() were placed on the stack by the calls), the stack pointer needed to be adjusted. The solution was to add an offset to the stack pointer. The value of this offset was dependent on the compiler options and generated more e-mail than I expected or cared for. One of those e-mail was from a clever individual named Nicolas Pinault which pointed out how this stack adjustment business could all be avoided as previously described. Because of Nicolas, μCµC/OS-II is no longer dependent on compiler options. Thanks again Nicolas!

Testing a Port

Once you have a port of μCµC/OS-II for your processor, you need to verify its operation. This is probably the most complicated part of writing a port. You should test your port without application code. In other words, test the operations of the kernel by itself. There are two reasons to do this. First, you dont want to complicate things anymore than they need to be. Second, if something doesnt work, you know that the problem lies in the port as opposed to your application. Start with a couple of simple tasks and only the ticker interrupt service routine. Once you get multitasking going, its quite simple to add your application tasks.

...

Once you complete the port, you need to compile, assemble and link it along with the μCµC/OS-II processor independent code. This step is obviously compiler specific and you will need to consult your compiler documentation to determine how to do this.

...

Listing 13.18 shows the contents of a typical INCLUDES.H. STRING.H is needed because OSTaskCreateExt() uses the ANSI C function memset() to initialize the stack of a task. The other standard C header files (STDIO.H, CTYPE.H and STDLIB.H) are not actually used by μCµC/OS-II but are included in case your application needs them.

Listing 13.19 shows the content of OS_CFG.H which was setup to enable ALL the features of μCµC/OS-II. You can find a similar file in the \SOFTWARE\uCOS-II\EX1_x86L\BC45\SOURCE directory of the companion CD so that you can use it as a starting point instead of typing an OS_CFG.H from scratch.

Listing 13.20 shows the contents of a simple TEST.C file that you can start with to prove your compile process. For this first step, there is no need for any more code because all we are trying to accomplish is a build. At this point, its up to you to resolve any compiler, assembler and/or linker errors. You may also get some warnings and you will need to determine whether the warnings are severe enough to be a problem.

Verify OSTaskStkInit() and OSStartHighRdy()

...

Start by modifying OS_CFG.H to disable the statistic task by setting OS_TASK_STAT_EN to 0. Because your TEST.C file (see Listing 13.20) doesnt create any application task, the only task created is the μCµC/OS-II idle task: OS_TaskIdle(). We will step into the code until μCµC/OS-II switches to OS_TaskIdle().

You should load the code into the debugger and start single stepping into main(). You should step over the function OSInit() and then step into the code for OSStart() (shown in listing 13.21). Step through the code until you reach the call to OSStartHighRdy() (the last statement in OSStart()) then step into the code for OSStartHighRdy(). At this point, your debugger should switch to assembly language mode since OSStartHighRdy() is written in assembly language. This is the code you wrote to start the first task and because we didnt create any other task than OS_TaskIdle(), OSStartHighRdy() should start this task.

Step through your code and verify that it does what you expect. Specifically, OSStartHighRdy() should start populating CPU registers in the reverse order that they were placed onto the task stack by OSTaskStkInit() (see OS_CPU_C.C ). If this doesnt happen, you most likely misaligned the stack pointer. In this case, you will have to correct OSTaskStkInit() accordingly. The last instruction in OSStartHighRdy() should be a return from interrupt and, as soon as you execute that code, your debugger should be positioned at the first instruction of OS_TaskIdle() . If this doesnt happen, you may not have placed the proper start address of the task onto the task stack and, you will most likely have to correct this in OSTaskStkInit() . If your debugger ends up in OS_TaskIdle() and you can execute a few times through the infinite loop, you are done with this step and have succesfully verified OSTaskStkInit() and OSStartHighRdy() .

GO/noGO Testing

If you dont have access to a source level debugger but have an LED (Light Emitting Diode) on your target system, you can write a GO/noGO test. What we will do is start by turning OFF the LED and if OSTaskStkInit() and OSStartHighRdy() works, the LED will be turned ON by the idle task. In fact, the LED will be turned ON and OFF very quickly and will appear to always be ON. If you have an oscilloscope, you will be able to confirm that the LED is blinking at a roughly 50% duty cycle.

...

This function is called by OSInit() at the very beginning of OSInit(). This allows you to perform CPU (or other) initialization as part of OSInit(). For example, you can initialize I/O devices from OSInitHookBegin(). The reason this is done is to encapsulate this initialization as part of the port. In other words, it prevents requiring that the user of μCµC/OS-II know anything about such additional initialization.

...

This function is called by OSInit() at the very end of OSInit(). This allows you to perform CPU (or other) initialization as part of OSInit(). For example, you can initialize I/O devices from OSInitHookEnd(). The reason this is done is to encapsulate this initialization as part of the port. In other words, it prevents requiring that the user of μCµC/OS-II know anything about such additional initialization.

...

Interrupts are disabled when this function is called. Because of this, you should keep the code in this function to a minimum because it directly affects interrupt latency.

Example

OSTaskIdleHook()

void OSTaskIdleHook(void)

...

OSTaskIdleHook() is called with interrupts enabled.

Example

OSTaskStatHook()

void OSTaskStatHook(void)

...

This function is called every second by μCµC/OS-IIs statistic task. OSTaskStatHook() allows you to add your own statistics.

...

The statistic task starts executing about five seconds after calling OSStart(). Note that this function is not called if either OS_TASK_STAT_EN or OS_TASK_CREATE_EXT_EN is set to 0.

Example

  OSTaskStkInit()

OS_STK *OSTaskStkInit(void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt);

...