Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 22 Current »

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

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.

INCLUDES.H

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

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.

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

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

OS_STK_GROWTH

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

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

OSTickISR()

µ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/OS-II starts the first task and, at that point, µC/OS-II is in an unknown state and your application could crash.

The pseudocode for the tick ISR is shown in Listing 13.15. This code must be written in assembly language because you cannot access CPU registers directly from C.

OSIntCtxSw()

OSIntCtxSw() 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 tasks stack (see section 13.05.03, OSTickISR() ).

The pseudocode for OSIntCtxSw() is shown in Listing 13.16. This code must be written in assembly language because you cannot access CPU registers directly from C. If your C compiler supports inline assembly, put the code for OSIntCtxSw() in OS_CPU_C.C instead of OS_CPU_A.ASM. You should note that this is the pseudocode for V2.51 (and higher) because prior to V2.51, OSIntCtxSw() required a few extra steps. If you have a port that was done for a version prior to V2.51, I highly recommend that you change it to match the algorithm shown in Listing 13.16.

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/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/OS-II is no longer dependent on compiler options. Thanks again Nicolas!

Testing a Port

Once you have a port of µ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.

There are a number of techniques you could use to test your port depending on your level of experience with embedded systems and processors in general. When I write a port, I generally follow the following four steps:

  1. Ensure that the code compiles, assembles and links
  2. Verify OSTaskStkInit() and OSStartHighRdy()
  3. Verify OSCtxSw()
  4. Verify OSIntCtxSw() and OSTickISR()

Once you complete the port, you need to compile, assemble and link it along with the µ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.

I generally setup a simple test directory as follows:

\SOFTWARE\uCOS-II\processor\compiler\TEST

where processor is the name of the processor or microcontroller for which you did the port, and compiler is the name of the compiler you used.

Table 13.2 shows the directories you will need to work with, along with the files found in those directories. In the TEST directory, you should have at least three file: TEST.C, INCLUDES.H and OS_CFG.H. Depending on the processor used, you may also need to have an interrupt vector table which I assumed would be called VECTORS.C but, it could certainly be called something else.

The TEST directory could also contain a MAKEFILE which specifies compiler, assembler and linker directives to build your project. A MAKEFILE assumes, of course, that you use a make utility. If your compiler provides an IDE (Integrated Development Environment), you may not have a MAKEFILE but instead, you could have project files which are specific to the IDE.

The port you did (refer to section 13.01) should be found in the following directory:

\SOFTWARE\uCOS-II\processor\compiler

Table 13.2, Files needed to test a Port

DirectoryFile
\SOFTWARE\uCOS-II\processor\compiler\TESTTEST.C
 OS_CFG.H
 INCLUDES.H
 VECTORS.C
 MAKEFILE or IDE project file(s)
\SOFTWARE\uCOS-II\processor\compilerOS_CPU_A.ASM
 OS_CPU_C.C
 OS_CPU.H
\SOFTWARE\uCOS-II\SOURCEOS_CORE.C
 OS_FLAG.C
 OS_MBOX.C
 OS_MEM.C
 OS_MUTEX.C
 OS_Q.C
 OS_SEM.C
 OS_TASK.C
 OS_TIME.C
 uCOS_II.H
 uCOS_II.H

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

Once you achieved a successful build, you are actually ready to start testing your port. As the title of this section suggest, this step will verify the proper operation of OSTaskStkInit() and OSStartHighRdy().

Testing with a source level debugger

If you have a source level debugger, you should be able to verify this step fairly quickly. I assume you already know how to use your debugger.

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/OS-II idle task: OS_TaskIdle(). We will step into the code until µ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.

For this test, you will need to temporarily modify three files OS_CFG.H, OS_CPU_C.C and TEST.C.

In OS_CFG.H, you need to disable the statistic task by setting OS_TASK_STAT_EN to 0. In TEST.C, you will need to add code to turn OFF the LED as shown in Listing 13.22. In OS_CPU_C.C, you need to modify OSTaskIdleHook() to toggle the LED as shown in the pseudocode of Listing 13.23.

The next step is to load the code in your target system and run it. If the LED doesnt toggle, youll need to find out whats wrong in either OSTaskStkInit() or OSStartHighRdy(). With such limited and primitive tools, the best you can do is carefully inspect your code until you find what you did wrong!

Verify OSCtxSw()

This should be an easy step because in the previous step, we verified that the stack frame of a task is correctly initialized by OSTaskStkInit(). For this test, we will create an application task and force a context switch back to the idle task. For this test, you need to ensure that you have correctly setup the software interrupt or TRAP to vector to OSCtxSw().

Testing with a Source Level Debugger

Start by modifying main() in TEST.C as shown in Listing 13.24. For sake of discussion, I decided to assume that the stack of your processor grows downwards from high to low memory and that 100 entries is sufficient stack space for the test task. Of course, you should modify this code according to your own processor requirements.You can now step into OSTimeDly(). The function OSTimeDly() will call OS_Sched() and OS_Sched() will in turn calls the assembly language function OSCtxSw(). In most cases, this is accomplished through a TRAP or software interrupt mechanism. In other words, if you setup the software interrupt or TRAP correctly, this instruction should cause the CPU to start executing OSCtxSw(). You can step through the code for OSCtxSw() and see the registers of TestTask() be saved onto its stack and the value of the registers for OS_TaskIdle() be loaded into the CPU. When the return from interrupt is executed (for the software interrupt or TRAP), you should be in OS_TaskIdle()!

If OSCtxSw() doesnt bring you into OS_TaskIdle() you will need to find out why and make the necessary corrections to OSCtxSw().

GO/noGO Testing

Modify main() in TEST.C as shown in Listing 13.25. I decided to assume that the stack of your processor grows downwards from high to low memory and that 100 entries is sufficient stack space for the test task.

Verify OSIntCtxSw() and OSTickISR()

This should be an easy step because OSIntCtxSw() is similar but simpler than OSCtxSw(). In fact, most of the code for OSIntCtxSw() can be borrowed from OSCtxSw(). For this test, you will need to setup an interrupt vector for the clock tick ISR. We will then initialize the clock tick and enable interrupts.

Start by modifying main() in TEST.C as shown in Listing 13.26.If the LED is not blinking and you are using a debugger, you can set a breakpoint in OSTickISR() and follow whats going on. I would also suggest trying to run the ISR without having it call OSIntExit(). In this case, you could simply have the ISR blink the LED (or another LED). If the LED is blinking then the problem is with OSIntCtxSw(). Again, because OSIntCtxSw() should have been derived from OSCtxSw(), I suspect that the problem is in the OSTickISR().

At this point, your port should work and you can now start adding application tasks. Have fun!

OSCtxSw()

void OSCtxSw(void)

FileCalled from 
OS_CPU_A.ASMOS_TASK_SW()Always needed

This function is called to perform a task level context switch. Generally, this function is invoked via a software interrupt instruction (also called a TRAP instruction). The pseudocode for this function is shown below.

Arguments

NONE

Return Value

NONE

Notes/Warnings

Interrupts are disabled when this function is called.

Some compilers will allow you to create software interrupts (or traps) directly in C and thus, you could place this function in OS_CPU_C.C. In some cases, the compiler also requires that you declare the prototype for this function differently. In this case, you can define the #define constant OS_ISR_PROTO_EXT in your INCLUDES.H. This allows you to delare OSCtxSw() differently. In other words, you are not forced to use the void OSCtxSw(void) prototype.

Example

NONE

OSInitHookBegin()

void OSInitHookBegin(void)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSInit()OS_CPU_HOOKS_EN == 1

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/OS-II know anything about such additional initialization.

Arguments

NONE

Return Value

NONE

Notes/Warnings

NONE

Example

NONE

OSInitHookEnd()

void OSInitHookEnd(void)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSInit()OS_CPU_HOOKS_EN == 1

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/OS-II know anything about such additional initialization.

Arguments

NONE

Return Value

NONE

Notes/Warnings

NONE

Example

NONE

OSIntCtxSw()

void OSIntCtxSw(void)

FileCalled from 
OS_CPU_A.ASMOSIntExit()Always needed

This function is called from OSIntExit() when OSIntExit() determines that there is a higher priority task to execute because of an ISR. The pseudocode for this function is shown below.

Arguments

NONE

Return Value

NONE

Notes/Warnings

Interrupts are disabled when this function is called.

Example

NONE

OSStartHighRdy()

void OSStartHighRdy(void)

FileCalled from 
OS_CPU_A.ASMOSStart()Always needed

This function is called from OSStart() to start the highest priority task that you created prior to you calling OSStart(). The pseudocode for this function is shown below.

Arguments

NONE

Return Value

NONE

Notes/Warnings

Interrupts are disabled when this function is called.

Example

NONE

OSTaskCreateHook()

void OSTaskCreateHook(OS_TCB *ptcb)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSTaskCreate() and OSTaskCreateExt()OS_CPU_HOOKS_EN == 1

This function is called whenever a task is created, after a TCB has been allocated and initialized and after the stack frame of the task is initialized. OSTaskCreateHook() allows you to extend the functionality of the task creation function with your own features. For example, you can initialize and store the contents of floating-point registers, MMU registers or anything else that can be associated with a task. Typically, you would store this additional information in memory allocated by your application. You should note that OSTaskCreateHook() is called immediately after another hook function called OSTCBInitHook(). In other words, either of these functions can be used to initialize the TCB. However, you ought to use OSTCBInitHook() for TCB related items and OSTaskCreateHook() for other task related items. You could also use OSTaskCreateHook() to trigger an oscilloscope or a logic analyzer or to set a breakpoint.

Arguments

ptcb

is a pointer to the task control block of the task created.

Return Value

NONE

Notes/Warnings

Interrupts are enabled when this function is called. Because of this, you might need to call OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() to protect critical sections inside OSTaskCreateHook().

Example

This example assumes that you created a task using OSTaskCreateExt() because it expects to have the .OSTCBExtPtr field in the tasks OS_TCB contain a pointer to storage for floating-point registers.

OSTaskDelHook()

void OSTaskDelHook(OS_TCB *ptcb)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSTaskDel()OS_CPU_HOOKS_EN == 1

This function is called whenever you delete a task by calling OSTaskDel(). You can thus dispose of memory you have allocated through the task create hook, OSTaskCreateHook(). OSTaskDelHook() is called just before the TCB is removed from the TCB chain. You can also use OSTaskCreateHook() to trigger an oscilloscope or a logic analyzer or to set a breakpoint.

Arguments

ptcb

is a pointer to the task control block of the task being deleted.

Return Value

NONE

Notes/Warnings

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)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COS_TaskIdle()OS_CPU_HOOKS_EN == 1

This function is called by the idle task (OS_TaskIdle()) when there are no other higher priority task ready to run. OSTaskIdleHook() can be used to force the CPU in low power mode for battery operated products to conserve energy when none of your tasks need to be serviced.

Arguments

NONE

Return Value

NONE

Notes/Warnings

OSTaskIdleHook() is called with interrupts enabled.

Example

OSTaskStatHook()

void OSTaskStatHook(void)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSTaskStat()OS_CPU_HOOKS_EN == 1

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

Arguments

NONE

Return Value

NONE

Notes/Warnings

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);

FileCalled from 
OS_CPU_C.COSTaskCreate() or OSTaskCreateExt()Always needed

This function is called by either OSTaskCreate() or OSTaskCreateExt() to initialize the stack frame of a task. Generally speaking, the stack frame is made to look las if an interrupt just occurred and all the CPU registers were saved onto it. The pseudocode for this function is shown below.

Arguments

task

is a pointer to the task code (i.e., the address of the function you want to declare as a task).

pdata

is a pointer to a user supplied data area that will be passed to the task when the task first executes. Sometimes, the compiler will pass pdata into registers while other compilers will pass pdata on the stack. You will need to consult your compiler documentation for the actual method used.

ptos

is a pointer to the top of stack. It is assumed that ptos points to a 'free' entry on the task stack. If OS_STK_GROWTH is set to 1 then ptos will contain the HIGHEST valid address of the stack. Similarly, if OS_STK_GROWTH is set to 0, ptos will contains the LOWEST valid address of the stack.

opt

specifies options that can be used to alter the behavior of OSTaskStkInit(). See uCOS_II.H for OS_TASK_OPT_???.

Return Value

A pointer to the new top-of-stack.

Notes/Warnings

Interrupts are enabled when this function is called.

Example

NONE

OSTaskSwHook()

void OSTaskSwHook(void)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSCtxSw() and OSIntCtxSw()OS_CPU_HOOKS_EN == 1

This function is called whenever a context switch is performed. The global variable OSTCBHighRdy points to the TCB of the task that will get the CPU, and OSTCBCur points to the TCB of the task being switched out. OSTaskSwHook() is called just after saving the tasks registers and after saving the stack pointer into the current tasks TCB. You can use this function to save/restore the contents of floating-point registers or MMU registers, to keep track of task execution time and of how many times the task has been switched-in, and more. OSTaskSwHook() is also called by OSStartHighRdy(). Because of this, you need to verify the flag OSRunning in OSTaskSwHook() so you dont perform any action as you would when a task is switched-out (see the example).

Arguments

NONE

Return Value

NONE

Notes/Warnings

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

OSTCBInitHook()

void OSTCBInitHook(OS_TCB *ptcb)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COS_TCBInit()OS_CPU_HOOKS_EN == 1

This function is called whenever a task is created, after a TCB has been allocated and initialized and when the stack frame of the task is initialized. OSTCBInitHook() allows you to extend the functionality of the TCB creation function with your own features. For example, you can initialize and store the contents of floating-point registers, MMU registers or anything else that can be associated with a task. Typically, you would store this additional information in memory allocated by your application. You should note that OSTCBInitHook() is called immediately before OSTaskCreateHook(). In other words, either of these functions can be used to initialize the TCB. However, you ought to use OSTCBInitHook() for TCB related items and OSTaskCreateHook() for other task related items.

Arguments

ptcb

is a pointer to the task control block of the task created.

Return Value

NONE

Notes/Warnings

Interrupts are enabled when this function is called. Because of this, you might need to call OS_ENTER_CRITICAL() and OS_EXIT_CRITICAL() to protect critical sections inside OSTCBInitHook().

Example

This example assumes that you created a task using OSTaskCreateExt() because it expects to have the .OSTCBExtPtr field in the tasks OS_TCB contain a pointer to storage for floating-point registers.

OSTickISR()

void OSTickISR(void)

FileCalled from 
OS_CPU_A.ASMTick InterruptAlways needed

When a tick interrupt occurs, the CPU needs to vector to this Interrupt Service Routine (ISR). The pseudocode for the ISR is shown below.

Arguments

NONE

Return Value

NONE

Notes/Warnings

  1. The interrupting device that causes OSTickISR() to be called should generally be setup to generate an interrupt every 10 to 100 mS.
  2. Some compilers will allow you to create ISRs directly in C and thus, you could place this function in OS_CPU_C.C. In some cases, the compiler also requires that you declare the prototype for this function differently. In this case, you can define the #define constant OS_ISR_PROTO_EXT in your INCLUDES.H. This allows you to delare OSTickISR() differently. In other words, you are not forced to use the void OSTickISR(void) prototype.

Example

NONE

OSTimeTickHook()

void OSTimeTickHook(void)

FileCalled fromCode enabled in OS_CPU_C.C if
OS_CPU_C.COSTimeTick()OS_CPU_HOOKS_EN == 1

This function is called by OSTimeTick(), which in turn is called whenever a clock tick occurs. OSTimeTickHook() is called immediately upon entering OSTimeTick(), to allow execution of time-critical code in your application. You can also use this function to trigger an oscilloscope for debugging, trigger a logic analyzer, or establish a breakpoint for an emulator.

Arguments

NONE

Return Value

NONE

Notes/Warnings

OSTimeTick() is generally called by an ISR, so the execution time of the tick ISR is increased by the code you provide in this function. Interrupts may or may not be enabled when OSTimeTickHook() is called, depending on how the processor port has been implemented. If interrupts are disabled, this function affects interrupt latency.

Example

  • No labels