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.
- 1 µC/OS-II Hardware/Software Architecture
- 2 Development Tools
- 3 Directories and Files
- 4 INCLUDES.H
- 5 OS_CPU.H
- 6 OS_CPU_C.C
- 7 OS_CPU_A.ASM
- 8 Testing a Port
- 9 OSCtxSw()
- 10 OSInitHookBegin()
- 11 OSInitHookEnd()
- 12 OSIntCtxSw()
- 13 OSStartHighRdy()
- 14 OSTaskCreateHook()
- 15 OSTaskDelHook()
- 16 OSTaskIdleHook()
- 17 OSTaskStatHook()
- 18 OSTaskStkInit()
- 19 OSTaskSwHook()
- 20 OSTCBInitHook()
- 21 OSTickISR()
- 22 OSTimeTickHook()
µC/OS-II Hardware/Software Architecture
A processor can run µC/OS-II if it satisfies the following general requirements:
The processor has a C compiler that generates reentrant code.
Interrupts can be disabled and enabled from C.
The processor supports interrupts and can provide an interrupt that occurs at regular intervals (typically between 10 and 100Hz).
The processor supports a hardware stack that can accommodate a fair amount of data (possibly many kilobytes).
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.
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.
Name | Type | File | C or Assembly? | Complexity |
|---|---|---|---|---|
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 1 |
| Data Type |
| C | 2 |
| Data Type |
| C | 2 |
| #define |
| C | 3 |
| #define |
| C | 1 |
| Macro |
| C | 3 |
| Macro |
| C | 3 |
| Function |
| Assembly | 2 |
| Function |
| Assembly | 3 |
| Function |
| Assembly | 3 |
| Function |
| Assembly | 3 |
| Function |
| C | 3 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
| Function |
| C | 1 |
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.
Intel/AMD 80186 |
|
Motorola 68HC11 |
|
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);
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.