DOC

Chapter8PORTING

By Warren Brown,2014-04-16 09:18
6 views 0
Chapter8PORTING

     Chapter 8

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 then you don’t need to read this chapter, unless of course you want to know how µC/OS-II’s processor specific code works.

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

    1. You must have a C compiler for the processor and the C compiler must be able to produce reentrant

    code.

    2. You must be able to disable and enable interrupts from C.

    3. The processor must support interrupts and you need to provide an interrupt that occurs at regular

    intervals (typically between 10 to 100 Hz).

    4. The processor must support a hardware stack, and the processor must be able to store a fair amount of

    data on the stack (possibly many Kbytes).

    5. The processor must have 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 #4 and #5 so µC/OS-II cannot run on such processors.

    Application Software

    /OS-II/OS-II Configuration

    (Processor Independent Code)(Application Specific)

    uCOS_II.COS_CORE.C

    uCOS_II.HOS_MBOX.COS_CFG.HOS_MEM.CINCLUDES.HOS_Q.C

    OS_SEM.C

    OS_TASK.C

    OS_TIME.C

    /OS-II Port

    (Processor Specific Code)

    OS_CPU.H

    OS_CPU_A.ASM

    OS_CPU_C.C

    Software

    Hardware

    CPUTimer

    Figure 8-1, µC/OS-II Hardware/Software Architecture

Figure 8-1 shows µC/OS-II’s 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 books (and diskette) contains all the source code for the Processor Independent Code section as well as the Processor Specific Code for the Intel 80x86, Real Mode, Large Model. If you intend to use µC/OS-II on a different

    processor, you will 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 is not available. Check the official µC/OS-II WEB site at www.uCOS-II.com for a list of available ports.

Porting µC/OS-II is actually quite straightforward once you understand the subtleties of the target processor and the

    C compiler you will be using. If your processor and compiler satisfy µC/OS-II’s requirements, and you have all the necessary tools at your disposal, porting µC/OS-II consists of:

    1. Setting the value of 1 #define constants (OS_CPU.H)

    2. Declaring 10 data types (OS_CPU.H)

    3. Declaring 3 #define macros (OS_CPU.H)

    4. Writing 6 simple functions in C (OS_CPU_C.C)

    5. Writing 4 assembly language functions (OS_CPU_A.ASM)

    Depending on the processor, a port can consist of writing or changing between 50 and 300 lines of code! Porting µC/OS-II could take you anywhere between a few hours to about a week.

    Once you have a port of µC/OS-II for your processor, you will need to verify its operation. Testing a multitasking real-time kernel such as µC/OS-II is not as complicated as you may think. 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 don’t want to complicate things anymore than they need to be. Second, if something doesn’t 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, it’s quite simple to add your application tasks.

8.00 Development Tools

    As previously stated, you need a C compiler for the processor you intend to use in order to port µC/OS-II. Because µC/OS-II is a preemptive kernel, you should only use a C compiler that generates reentrant code. Your C compiler must also be able to support assembly language programming. Most C compiler designed for embedded systems will, in fact, also include an assembler, a linker, and a locator. The linker is used to combine object files (compiled and assembled files) from different modules while the locator will allow 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 will allow you to insert in-line assembly language statements in your C source code. This makes it quite easy to insert the proper processor instructions to enable and disable interrupts. Other compilers will actually contain language extensions to enable and disable interrupts directly from C.

8.01 Directories and Files

    The installation program provided on the distribution diskette will install µC/OS-II and the port for the Intel 80x86 (Real Mode, Large Model) on your hard disk. I devised a consistent directory structure to allow you to easily find the files for the desired target processor. If you add a port for another processor, you should consider following the same conventions.

    All the ports should be placed under the \SOFTWARE\uCOS-II directory on your hard drive. 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 will allow you to have in-line assembly language and thus, 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. Below are examples of the directories where you would store different ports. Note that each have the same file names even though they are totally different targets.

    Intel/AMD 80186: \SOFTWARE\uCOS-II\Ix86S:

     OS_CPU.H

     OS_CPU_A.ASM

     OS_CPU_C.C

     \SOFTWARE\uCOS-II\Ix86L:

     OS_CPU.H

     OS_CPU_A.ASM

     OS_CPU_C.C

    Motorola 68HC11: \SOFTWARE\uCOS-II\68HC11:

     OS_CPU.H

    OS_CPU_A.ASM

     OS_CPU_C.C

8.02 INCLUDES.H

    As I mentioned in chapter 1, INCLUDES.H is a MASTER include file and is found at the top of all .C files as follows:

     #include “includes.h”

INCLUDES.H allows every .C file in your project to be written without concerns 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. You can edit INCLUDES.H to add your own header

    files but, your header files should be added at the end of the list.

8.03 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 8.1:

     #ifdef OS_CPU_GLOBALS #define OS_CPU_EXT #else #define OS_CPU_EXT extern #endif /* ********************************************************************************************************* * DATA TYPES * (Compiler Specific) ********************************************************************************************************* */ typedef unsigned char BOOLEAN; typedef unsigned char INT8U; /* Unsigned 8 bit quantity */ (1) 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 */ /* ********************************************************************************************************* * Processor Specifics ********************************************************************************************************* */ #define OS_ENTER_CRITICAL() ??? /* Disable interrupts */ (3) #define OS_EXIT_CRITICAL() ??? /* Enable interrupts */ #define OS_STK_GROWTH 1 /* Define stack growth: 1 = Down, 0 = Up */ (4) #define OS_TASK_SW() ??? (5)

    Listing 8.1, Contents of OS_CPU.H

8.03.01 OS_CPU.H, Compiler Specific Data Types

    Because different microprocessors have different word length, the port of µC/OS-II includes a series of type definitions that ensures portability. Specifically, µC/OS-II’s code never makes use of C’s short, int and, long

    data types because they are inherently non-portable. Instead, I defined integer data types that are both portable and intuitive L8.1(1). Also, for convenience, I have included floating-point data types L8.1(2) even though µC/OS-II doesn’t make use of floating-point.

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

You must tell µC/OS-II the data type of a task’s stack. This is done by declaring the proper C data type for OS_STK.

    If stack elements on your processor are 32-bit and your compiler documentation specify that an int is 32-bit then,

    you would declare OS_STK as being of type unsigned int. All task stacks MUST be declared using

    OS_STK as its data type.

    All you have to do is to consult the compiler’s manual and find the standard C data types that corresponds to the types expected by µC/OS-II.

8.03.02 OS_CPU.H, OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL()

    µC/OS-II like all real-time kernels need to disable interrupts in order to access critical sections of code, and re-enable interrupts when done. This allows µC/OS-II to protect critical code from being entered simultaneously from either multiple tasks or Interrupt Service Routines (ISRs). The interrupt disable time is one of the most important specification 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. Every processor generally provide instructions to disable/enable interrupts and your C compiler must have a mechanism to perform these operations directly from C. Some compilers will allow you to insert in-line assembly language statements in your C source code. This makes it quite easy to insert processor instructions to enable and disable interrupts. Other compilers will actually 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 L8.1(3).

µC/OS-II’s critical sections are wrapped with OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL()

    as shown below:

µC/OS-II Service Function

    {

     OS_ENTER_CRITICAL();

     /* µC/OS-II critical code section */

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

    There is, however, a little problem with this scenario. If you called the µC/OS-II function with interrupts disabled then, upon return from µC/OS-II, interrupts would be enabled! If you had interrupts disabled, you may have wanted them to be disabled upon return from the µC/OS-II function. In this case, the above implementation would not be adequate.

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() would simply be implemented by restoring the interrupt

    status from the stack. Using this scheme, if you called a µC/OS-II service with either interrupts enabled or disabled then, the status would be preserved across the call. If you call a µC/OS-II service with interrupts disabled, you are potentially extending the interrupt latency of your application. Your application can use

    OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() to also protect critical sections of code. Be

    careful, however, because your application will ‘crash’ if you have interrupts disabled before calling a service such as OSTimeDly(). This will happen because the task will be 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!

The question is thus, which one is better? Well, that all depends on what you are willing to sacrifice. If you don’t care

    in your application whether interrupts are enabled after calling a µC/OS-II service then, you should opt for the first method because of performance. If you want to preserve the interrupt disable status across µC/OS-II service calls then obviously the second method is for you.

Just to give you an example, disabling interrupts on an Intel 80186 is done by executing the STI instructions and

    enabling interrupts is done by executing the CLI instruction. You can thus implement the macros as follows:

#define OS_ENTER_CRITICAL() asm CLI

    #define OS_EXIT_CRITICAL() asm STI

    Both the CLI and STI instructions execute in less that 2 clock cycles each on this processor (i.e. total of 4 cycles). To preserve the interrupt status you would need to implement the macros as follows:

#define OS_ENTER_CRITICAL() asm PUSHF; CLI

    #define OS_EXIT_CRITICAL() asm POPF

In this case, OS_ENTER_CRITICAL() would consume 12 clock cycles while OS_EXIT_CRITICAL()

    would use up another 8 clock cycles (i.e. a total of 20 cycles). Preserving the state of the interrupt disable status would thus take 16 clock cycles longer than simply disabling/enabling interrupts (at least on the 80186). Obviously, if you have a faster processor such as an Intel Pentium-II then, the difference would be minimal.

8.03.03 OS_CPU.H, OS_STK_GROWTH

    The stack on most microprocessors and microcontrollers grows from high-memory to low-memory. There are, however, some processors that work the other way around. µC/OS-II has been designed to be able to handle either flavor. This is accomplished by specifying to µC/OS-II which way the stack grows through the configuration constant OS_STK_GROWTH L8.1(4) 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.

8.03.04 OS_CPU.H, OS_TASK_SW()

    OS_TASK_SW() L8.1(5) 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 consist 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 task’s stack and execute a return from interrupt. To switch context, you should implement OS_TASK_SW() so that you 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 8.04.02).

For example, a port for an Intel or AMD 80x86 processor would use an INT instruction. The interrupt handler would

    need to vector to OSCtxSw(). A port for the Motorola 68HC11 processor would most likely use the SWI instruction.

    Again, the SWI handler would be OSCtxSw(). Finally, a port for a Motorola 680x0/CPU32 processor would

    probably use one of the 16 TRAP instructions. Of course, the selected TRAP handler would be none other than

    OSCtxSw().

    There are some processors like the Zilog Z80 that do not provide a software interrupt mechanism. In this case, you would need to simulate the stack frame as closely to an interrupt stack frame as you can. In this case, OS_TASK_SW() would simply call OSCtxSw() instead of vector to it. The Z80 is a processor that has been

    ported to µC/OS and thus would be portable to µC/OS-II.

8.04 OS_CPU_A.ASM

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

     OSStartHighRdy()

     OSCtxSw()

     OSIntCtxSw()

     OSTickISR()

    If your compiler supports in-line assembly language code, you could actually place all the processor specific code into OS_CPU_C.C instead of having a separate assembly language file.

8.04.01 OS_CPU_A.ASM, OSStartHighRdy()

    This function is called by OSStart() to start the highest priority task ready-to-run. Before you can call OSStart(), however, you MUST have created at least one of your tasks (see OSTaskCreate() and

    OSTaskCreateExt()). OSStartHighRdy() assumes that OSTCBHighRdy points to the task

    control block of the task with the highest priority. As mentioned previously, 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 task’s stack in the proper order and, execute

    a return from interrupt. 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.

Note that OSStartHighRdy() MUST call OSTaskSwHook() because we are basically doing a ‘half’

    context switch we are restoring the registers of the highest priority task. OSTaskSwHook() can examine

    OSRunning to tell it whether OSTaskSwHook() was called from OSStartHighRdy() (i.e. if

    OSRunning is FALSE) or from a regular context switch (i.e. OSRunning is TRUE).

OSStartHighRdy() MUST also set OSRunning to TRUE before the high priority task is restored (but

    after calling OSTaskSwHook()).

8.04.02 OS_CPU_A.ASM, OSCtxSw()

    As previously mentioned, 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() is as follows. 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 the function OSSched() which concludes that the current task is no longer the most important task to run. OSSched() loads the address of the highest priority task into OSTCBHighRdy and 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 task’s Task Control Block, OS_TCB. The software interrupt

    instruction (or trap) forces some of the processor registers (most likely the return address and the processor’s status

    word) onto the current task’s stack and the processor then vectors to OSCtxSw().

The pseudo code of what needs to be done by OSCtxSw() is shown in listing 8.2. This code must be written in

    assembly language because you cannot access CPU registers directly from C.

void OSCtxSw(void)

    {

     Save processor registers;

     Save the current task’s stack pointer into the current task’s OS_TCB:

     OSTCBCur->OSTCBStkPtr = Stack pointer;

     Call user definable OSTaskSwHook();

     OSTCBCur = OSTCBHighRdy;

     OSPrioCur = OSPrioHighRdy;

     Get the stack pointer of the task to resume:

     Stack pointer = OSTCBHighRdy->OSTCBStkPtr;

     Restore all processor registers from the new task’s stack;

     Execute a return from interrupt instruction; }

    Listing 8.2, Pseudo code for OSCtxSw()

    You should note that interrupts are disabled during OSCtxSw() and also during execution of the user definable function OSTaskSwHook().

8.04.03 OS_CPU_A.ASM, OSIntCtxSw()

    OSIntCtxSw() is a function that is called by OSIntExit() to perform a context switch from an ISR.

    Because OSIntCtxSw() is called from an ISR, it is assumed that all the processor registers are properly saved onto the interrupted task’s stack. In fact, there are more things on the stack frame than we need. OSIntCtxSw()

    will thus have to clean up the stack so that the interrupted task is left with just the proper stack frame content.

To understand what needs to be done in OSIntCtxSw(), lets look at the sequence of events that leads µC/OS-II

    to call OSIntCtxSw(). You may want to refer to figure 8-2 to help understand the following description. We will assume that interrupts are not nested (i.e. an ISRs will not be interrupted), interrupts are enabled, and the processor is executing task level code. When an interrupt arrives, the processor completes the current instruction, recognizes the interrupt and initiates an interrupt handling procedure. This generally consist of pushing the processor status register and the return address of the interrupted task onto the stack F8-2(1). The order and which registers are pushed onto the stack is irrelevant.

    (5)

    LOW MEMORY(4)(6)

    (3)SP Points Here!Return address to caller of OSIntCtxSw()

    Processor Status Word

    Return address to caller of OSIntExit()

    SP must be adjusted

    to point here. (2)

    This new SP is saved intoSaved Processor Registers(7)the preempted task's OS_TCB.

    Interrupt Return AddressStack Growth

    (1)Processor Status Word

    HIGH MEMORY

    Figure 8-2, Stack contents during an ISR

    The CPU then vectors to the proper ISR. µC/OS-II requires that your ISR begins by saving the rest of the processor registers F8-2(2). Once the registers are saved, µC/OS-II requires that you either call OSIntEnter() or, that

    you increment the global variable OSIntNesting by one. At this point, the interrupted task’s stack frame only

    contains the register contents of the interrupted task. The ISR can now start servicing the interrupting device and possibly, make a higher priority task ready. This would occur if the ISR sends a message to a task (by calling OSMboxPost() or OSQPost()), resume a task (by calling OSTaskResume()), invoke

Report this document

For any questions or suggestions please email
cust-service@docsford.com