Send feedback.
VxWorks and Embedded Linux
Updated 6 Mar 2004
An Overview Tutorial of the VxWorks® Real-Time Operating System
by Jim Collier

 

VxWorks is a popular real-time multi-tasking operating system for embedded microprocessor systems, designed by Wind River Systems of Alameda, CA.  Like Unix and Linux, VxWorks is generally compliant with the IEEE's POSIX (Portable Operating System Interface) standard, version 1003.1b.  The current release of VxWorks is version 5.4.  VxWorks projects are usually developed in the Tornado 2 Integrated Development Environment (IDE) which provides for specifying a configuration (e.g., the libraries with which a project is linked), project builds, and code testing.  Code is written is written C/C++ with segments, usually short, in the target processor's assembly language.  VxWorks runs on many target processors including, but not limited to the following processors: Motorola PowerPC, 68K and CPU32 cores; MIPS; ARM; Intel X86 (386 and up) and i960. SPARC/SPARCLite.

The VxWorks kernel wind is a proprietary product, outwardly similar to Unix and Linux for its multiprocessing capability.

The remainder of this tutorial introduces the following topics at some depth.  (Click on any line to go to that section.)
 

1. Introduction to VxWorks Programming
2. Board Support Package (BSP)
3. Tornado 2 Cross-Development System
Appendix  Tornado 2 File Tree
References

 

1. Introduction to VxWorks Programming

Some of the rudiments of VxWorks programming are presented here.  These do not appear in the same order presented in Ref 1, which should be read by any serious VxWorks programmer.  Rather they reenforce some of the material that can be found there, but appear in a top-down order, with basic concepts described first, followed by details.

1.1 Tasks
In VxWorks, the unit of execution is the task, corresponding to a Unix/Linux process.  Tasks may be spawned (i.e., created), deleted, resumed, suspended, and preempted (i.e., interrupted) by other tasks, or may be delayed by the task itself.  A task has its own set of context registers including stack.  The term thread, while not unknown in VxWorks jargon, does not exist in a formal sense as in other operating systems.  A thread, when the term is used, may be thought of as a sub-sequence of connected program steps inside a task, such as the steps the VxWorks kernel performs in spawning a task, or the sequence of instructions in an else-clause following an if-statement.  Tasks can communicate with each other in a manner similar to Interprocess Communications in Unix and Linux.

Tasks are in one of four states, diagrammed in Figure 1-1, adapted from Ref. 1.

Task states
Figure 1-1  Task State Diagram

A newly spawned task enters the state diagram through the suspended state.

Tasks may be scheduled for execution by assigning them priorities, ranging from 0 (higest priority) to 255.  Once started, that is, having entered the ready state in Figure 1, a task may execute to completion, or it may be assigned a fixed time slice in round-robin scheduling.  A task blocks (enters the pended state) while another task takes control.  A task may be prempted because it has consumed its time slice, or because another task with higher priority exists. Task priorities may be changed during execution.  A task may avoid being preempted by locking the scheduler while it has control.  (This does not prevent interrupts from occurring.)

A task may also enter the delayed state, for example while waiting a fixed time between reading samples within a task before processing them as a group, during which time another task may take control.  The delay is controlled by an external timer which runs independently of processing (combined with a tick counter maintained by the kernel) that awakens the delayed task and avoids having the task tie up resources with an index counter which would prevent another task from executing.

The suspended state is used to halt a task for debugging without loss of its context registers.

Several system tasks exist concurrently with user defined tasks.  These are the root task, named tUsrRoot; the logging task tLogTask; exception task tExcTask; and the network task tNetTask.

Intertask communication (corresponding to Unix/Linux Interprocess Communication) can occur through semaphores that provide interlocking and synchronization of tasks, and messaging that allow tasks to communicate events or data with each other.  Although semaphores and messaging are implemented with different kernel mechanisms, it is customary to treat them together.

Semphores can be categorized as ordinary binary semaphores and a special class of binary semaphores called mutual exclusion semaphores.  Binary semaphores are used for task synchronization.  As implemented in VxWorks, a binary semaphore has two values: full and empty.  When full, it is available for a task.  When empty, it is unavailable.  A pending task proceeds by taking an available semaphore, which makes the semaphore empty or unavailable..  When the semaphore is no longer needed (because the task is about to return to the pending state), it gives the semaphore which makes it full or available for another task.   A mutual exclusion semaphore (also called a mutex) allow one task to have exclusive use of a resource while it is needed.

The difference between an ordinary binary semaphore and a mutex semaphore is in the way the semaphore is initialized.  For an ordinary binary semaphore, a task attempting to synchronize to an external event creates an empty semaphore.  When it reaches the point to be synchronized, it attempts to take a semaphore.  If unavailable, it waits at this point.  A second task which controls the synchronization event gives the semaphore when it is no longer needed.  The first task receives notification that the semaphore is available and proceeds.    For a mutex semaphore, a task wishing to block other tasks from a resouce first creates a full semaphore, and then takes the semaphore, making it unavailable to other tasks.  When it is no longer needed, it the task gives the semaphore, making the resource available.  A mutual exclusion semaphore must have matching "takes" and "gives".

These ideas can be illustrated with the following code segments.

Example of synchronization through binary semaphore

#include "vxWorks.h"
#include "semLib.h"

#define T_PRIORITY 50
 

SEM_ID syncExampleSem;    // named semaphore object

void initialize (void)
{

    // set up FIFO queue with emtpy binary semaphore
   

syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);

    // create task1
    taskSpawn ("task1", T_PRIORITY, 0, 10000, task1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

    // create task2
    taskSpawn ("task2", T_PRIORITY, 0, 10000, task2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

}

void task1 (void)
{
    // stay here until semaphore becomes available
    semTake (syncExampleSem, WAIT_FOREVER);
 

    // do something

}
 
 

void task2 (void)
{
    // do something
 
 

    // now let task1 execute
    semGive (synExampleSem);

}

Example of resource lockout through mutual exclusion semaphore

#include "vxWorks.h"
#include "semLib.h"

#define T_HI_PRIORITY 20
#define T_LO_PRIORITY 200

SEM_ID semMutex;    // named semaphore object

char alphabet[27];  // memory resource to have exclusive access

void initialize (void)

{
 

  //create binary semaphore which is initially full
  semMutex = semBCreate (SEM_Q_PRIORITY, SEM_FULL);

    // spawn high priority task
    taskSpawn ("hi_priority_task", T_HI_PRIORITY, 10000, tHiPriorityTask, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

    // spawn low priority task
    taskSpawn ("lo_priority_task", T_LO_PRIORITY, 10000, tLoPriorityTask, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

void tHiPriorityTask (void)
{

   int i;

    // enter critical region - any other tasks wanting access to alphabet[] should
    // wait for available semaphore
    semTake (semMutex, WAIT_FOREVER);

    // write alphabet to global array
    for (i= 0; i < 26; i++)
        alphabet[i] = 'A' + i;

    alphabet[i] = '\0';

    // leave critical region
    semGive (semMutex);

}

void tLoPriorityTask (void)
{
    // enter critical region
    semTake (semMutex, WAIT_FOREVER);

    // array members guaranteed stable while being read by this task
    printf ("alphabet= %s ", alphabet);

    // leave critical region
    semGive (semMutex);
}

A potential problem can occur in the second example.  Suppose a third task with medium priority, say 50, between the high and low priority tasks is also spawned, but doesn't require access to the mutually excluded region.  Assume that the high priority task is called repetitively from somewhere else, requiring it to enter and leave the critical region each time it's executed.  This medium priority task will preempt the low priority task, because of the difference in priorities.  If the third task is overly long, then the low priority task, which reads the alphabet sequence is delayed in releasing its access to the critical area, holding up execution of the high priority task, as illustrated schematically in Figure 1-2.  The medium priority task has effectively assumed higher priority than the high priority task.  This is an example of priority inversion.

Priority Inversion Problem
Figure 1-2  Priority Inversion Problem

The solution is to promote the low priority task temporarily to the same priority as the highest priority task which prevents the low priority task from being preempted by the medium priority task.  The low priority task creates a "inversion-safe" mutex semaphore which permits it to assume the temporarily higher priority.  Once it reaches this priority, it remains here until all mutex semaphores owned by the task have been released.

POSIX semaphores, not discussed here, may also be used in VxWorks programs.

1.2 Messaging

Closely related to the idea of semaphores is the concept of the message which passes data between tasks.  Messaging could be used to accomplish task interlocking as well, but setting up a message consumes more time than initializing a semaphore.  Messaging is useful for passing variables between asynchronous tasks.  VxWorks supplies seven functions for messaging: msgQCreate ( ), msgQDelete ( ), msgQSend ( ), msgQReceive ( ), msgQNumMsgs ( ), msgQShow ( ), msgQInfoGet ( ).      A message may be placed ahead of previous messages by sending it with the attribute MSG_PRI_URGENT.  As for semaphores, POSIX messages may also be used in VxWorks programs, and in fact, offer some capabilities that VxWorks messages don't possess.

The following fragment illustrates a common use for a message.  We will illustrate using VxWorks messges.  The procedure is similar for POSIX messages.  Task 1 has written data to a file whose name is not known to other tasks, and wishes to inform Task 2 that the data is available in the specified file.

#define MAX_MSGS 10
#define MAX_MSG_LEN 50
    task1 (void)
    {

      char strFileName [MAX_MSG_LEN];
       strcpy (strFileName, "./filename1");
       // write to file with name strFileName and fd (file descriptor) handle file1
           ...
       fclose (file1);

       // create message queue
       if ( (exampleMsgId = msgQCreate (MAX_MSGS, MAX_MSG_LEN, MSQ_PRI_NORMAL) ) == NULL)
          return ERROR;
       if (msgQSend (exampleMsgId, strFileName, sizeof (strFileName) + 1, WAIT_FOREVER) == ERROR)
          return ERROR;

     }

     task2 (void)
     {

    char msgBuf [MAX_MSG_LEN];

    // fetch message from queue
    if (msqQReceive (exampleMsgId, msgBuf, MAX_MSG_LEN, WAIT_FOREVER) == ERROR)
        return ERROR;
    // open file for reading
    ...

     }

There are two things to note.  The second task may query the message exampleMsgId before it has been initialized.  But since the object is known to exist (it is a global variable),  this does not cause an error.  Also note that the sender has closed the file before informing the receiver of its name.  This approach requires sending the file's fully qualified path name as a possibly long string, which consumes time at the sending and receiving ends.  The programmer could send the file's handle as message data, but this would require that the writing task keep the file open for reading by another task and leads to a new set of interlock problems which are best avoided.

1.3 Input/Output

In VxWorks, an input/output data stream in treated as a file regardless of the I/O device.   File I/O can be organized by block or by character.  Character devices include display terminals and external hardware devices such as A/D converters or real-time clocks.  Block devices include local or network disk drives.  File I/O can be real, as in the previous examples, or virtual as in the case of pipes and network sockets.  Pipes enable tasks to communicate with each other.  The pipe driver is called pipeDrv.  If the device stream extends across a netowork, it becomes a socket. The stream socket is similar in concept to the Unix or Windows TCP/IP socket.  (For TCP/IP, VxWorks uses Berkeley Software Distribution version. 4.x Unix socket functions.)

A file is handled by its file descriptor fd corresonding to a FILE structure in POSIX.  For each kind of file, there is a different kind of driver which permits the following I/O operations, summarized in the following.

1.3.1  Basic I/O Operations

creat (const char *name, int flag ) Create a file.
open (const char *name,  int flags, int mode) Open (and possibly create) a file.
close ( int fd) Close a file.
remove ( const char *name) Remove a file.
read ( int fd, char *buffer, size_t maxbytes) Read an existing file.
write (int fd, char *buffer, size_t nbytes ) Write to an existing file.
ioctl (int fd, int function, int arg ) Perform control operations on a file.
BASIC I/O OPERATIONS

Note: mode specifies the file's permissions.

The string name denotes what kind of device the file is.  When a file is created or opened, the I/O system searches through a list of device names, and physical file directories for at least a partial beginning match of the names and a file descriptor is returned if a match is established.  If no match is found, a default device can be specified, or if no default is specified (the more common case), then the I/O system reports an error.  Thereafter, the basic I/O operations specified previously are mapped to the specific file's I/O routines.  For instance, read ( ) and write ( ) result in low-level calls to xxread( ) and xxwrite ( ), which are defined in the device's driver in the case of physical devices or in the local file system routines in the case of disk files.

1.3.2  Devices and Files

Two kinds of databases are maintained for I/O operations: the Driver Table and the File Descriptor (FD) Table.  The Driver Table has entries for character devices, and the FD Table has entries for block devices.

Each record in the Driver Table corresponds to a device descriptor.  A device descriptor describes a specific device, for example a particular Ethernet port.  The Driver Table is built at boot by adding the device descriptors, which form a linked list.  The information in a device descriptor contains (a) the device name string, e.g., "/tyE2/0"; (b) the corresponding record number in the Driver Table, an integer assigned at the time the device is added (see below); (c) the names of device-specific routines which implement the seven basic I/O operations above.  These will have the same names as the operation prefixed by the device name, for example, ether3creat ( ) and ether3write ( ).  These low level functions are implented in a device driver file for the particular device.  It is important to note that a different driver exists for each physical device, which may use similar functions for another driver (of an identical device).  If Ether 1 and Ether 2 are the names of two devices, then after installation, there will be an implentation of ether1creat ( ), and so forth, and ether2creat ( ), et al.

The installation of a character device at boot is a two-step process.  A driver for the device is installed with iosDrvInstall ( ) which returns a record number
in the Driver Table where the driver was installed.  The device is added to the linked list of device descriptors (which contain the information in the
specific Driver Table record) with a call to iosDevAdd ( ).  Both operations must be completed before the device is installed.

The following table summarizes the type of I/O device (or file) and the library where its driver(s) is(are) defined.  If the type of device is present, then the device's library should be included in the VxWorks build.

 
FILE/DEVICE LIBRARY
Local File Systems
  MSDOS (16-bit FAT), dosFs   dosFsLib
  MSDOS (32-bit FAT), dosFs32   dosFs32Lib
  RT-11   rt11FsLib
  Raw File System, rawFs   rawFsLib
  Tape File System, tapeFs   tapeFsLib
  CD-ROM, cdromFs   cdromFsLib
Virtual Devices
  pipes   pipeDrv
  memory I/O (non-file)   memDrv
  RAM files   ramDrv
Network File System (NFS)   nfsDrv
Physical Devices  
  Terminal driver   ttyDrv
  Pseudo-terminal driver   ptyDrv
  SCSI   scsiLib
  sysLib.c

 

 

2.0 The Board Support Package (BSP)

The Board Support Package (BSP) contains the functionality specific to the user's hardware.  It does such things as initialize hardware, interrupts, clocks and timers, the mapping of memory spaces and memory sizing.  The BSP also contains handlers for external devices that exchange data with the processor, called device drivers.    The BSP provides the interface between the outside world and the kernel.

2.1  Creating a New BSP
Creating a BSP is a broad subject.  The steps in creating a BSP are recommended in Ref. 2, Chapter 3.

1. Generate a minimal VxWorks kernel
     a. Determine the development environment.  The user has several choices for the type of target image.  These are:

vxWorks
Execution on the target begins from RAM.  The trick is to load the image into RAM somehow.

vxWorks_rom
Image begins execution in ROM where text (i.e., code) and data segments are copied to RAM.

vxWorks_rom_nosym
Image executes from ROM, but the data segment is copied into RAM.

 

     b. Write pre-kernel initialization code.  This consists of modifying template files supplied by Wind River.  The files are:

Makefile The master make-file.  Contains CPU name, information about the host development platform GNU tool chain, top directory names, ROM addresses and RAM destination addresses.
bspname.h Definitions for serial interface, timer,  I/O device addresses, interrupt vectors and levels.
config.h Include-file names and definitions specific to board. 
romInit.s Beginning of execution at cold boot.  See Section 2.1 below.
sysLib.c Resets hardware to quiescent state with interrupts disabled.
sysALib.s Entry point for RAM-based images.
usrConfig.c See Section 2.1 below.

     c. Add simple drivers for timers, serial devices and interrupt controller.

     d. Start the target agent, that is, the portion of VxWorks in the target image that interfaces with the host development system.

     e. Add (or create) full device drivers.

     f.  Test the BSP.  The systematic approach, perhaps not observed as often as it should be, is to take a "build-out" approach.  Features are tested in the order they were added, with later features disabled at first.  The later features are enabled one by one and tested at each stage.  Ref. 2 Chapter 7 gives recommendations for the systematic design of a Validation Test Sequence using Tcl scripts.
 

2.2 The Boot Process
It is helpful to see what happens during a cold power-on boot.  Figure 2-1 is a block diagram of the boot sequence for a ROM image of the code, that is, one which does not contain the VxWorks debugging facilities, in other words, a production version.  Some simplifying assumptions have been made.  For instance, no consideration has been given to networking, which would normally occur in the memory initialization routine,  meminit (), called from usrConfig.c during usrRoot (), the root task.


Figure 2-1  VxWorks Application Boot Sequence (ROM image)

2.3  Device  Drivers

The device driver acts as an interface between the wind kernel and an external device.   A typical driver contains a handler for the interrupting device.  The handler will have functions that acknowledge the interrupt, bind the interrupt to a user-specified function, initialize the device, and disable the device.  Wind River supplies template drivers for (a) traditional and multi-mode serial drivers, (b) timers, (c) non-volatile memory (NVRAM), (d) the VMEbus, (e) interrupt controllers and (f) the PCI Bus, and (g) network interface drivers.  The last class of drivers are the most complex.

&nsbp;

3.0 The Tornado 2 Cross-Development System

Tornado 2 is the name given to the development system for VxWorks projects and development tools such as compilers and linkers.  Tornado includes an integrated development environment (IDE) program similar to Microsoft's Visual Studio for Visual C++ projects.  Source code may be written using the IDE's editor, or other text editor.  Programs may be compiled and projects built inside the IDE or from the command line.

Figure 3-1 (adapted from Ref. 3) shows the Tornado Development Environment.  The Logic Analyzer option is not discussed here.

Tornado Development System Environment
Figure 3-1   Tornado Development System

3.1 Editor
The development can take place inside the Tornado IDE or outside using commands.  The user may use any text editor.  The Tornado IDE editor, Code Warrior, EMACS, vi, and Microsoft's Visual Studio editor are all commonly used.

3.2 Project Facility
Whether the developer uses the IDE or command approaches, one IDE feature has been found indispensable by this user: the Project facility.   In the IDE, the Project facility information is displayed in the Workspace window.  (It is similar to the Workspace window in Microsoft's Visual Studio.)  This feature enables the developer to configure the project so that a loadable module is built from the required application files, BSP and pertinent libraries.  The facility "knows" which libraries to include (and just as important, which to exclude) given the requirements specified by the developer.   The Project facility also operates like the make file utility in other C compilers.  Any change which the user makes in any source file, whether an application file or a BSP file or a library file or inherited code is detected so that a load module rebuild requires only recompiling the changed source and relinking, a process requiring on the order of tens of seconds on modern development platforms even when the code has grown to its nearly complete size.  The Workspace window has three folders, or views: Files, VxWorks, and Builds.  Files are source files.  VxWorks refers to VxWorks components and libraries which will be present in a build.  The Build view shows which target object file is being built, whether that image is a simulated version, or downloadable version, and whether it contains VxWorks debugging facilities or whether it is a self-contained bootable application.

3.3 Target Server - Target Agent Back-End Link
The Target Server is set up by configuring it to have an interface that matches the Target Platform's Target Agent back end (see Figure 3-1).  The link may be an IP (Internet Protocol) connection, called RPC back end (RPC is a TCP/IP protocol) such as Ethernet; or a simple serial interface, called Serial back end with user-specifiable baud rate; or a Pipe back end; or Netrom back end.  This interface is built into the VxWorks image which is loaded onto the target.  The Target Agent back-end protocol will have been established in the Project facility, described previously, while the Target Server interface on the host side is configured separately.

Establishing the link between the Target Server and the Target Agent back end is a milestone in the development cycle, for it demonstrates that code is correctly built and loaded at the target.

3.4 WindSh Shell
Tornado's WindSh is a command shell that operates on the host computer and exchanges data over the Target Server-Target Agent link.  It permits peeking and poking target memory, and performing calculations using C operators and variables.  A common use for the shell is spawning tasks.  The task may be a part of the target code that executes an application, or it may be a particular function being tested.  The task may be suspended and resumed and deleted.  It may be set up to be called periodically.  Information about the state of tasks on the target can be queried.  Particular target database items can be printed, such as the list of devices, or symbols, or memory contents; the files in the target's file system can be listed.   Particularly useful functions performed by the shell are querrying the status of tasks and message queues and the status of network elements, if present.  The target firmware/software can be rebooted from the shell.

The shell has standard input, output, and error streams which can be redirected like POSIX-compliant file streams.

WindSh can be started from the Tornado IDE or by typing

                                           winsh targetname

on the command line, where targetname is the name of the target.

Virtually every VxWorks developer will interact with the shell and should read Chapter 6 of Ref. 3 which deals with the subject in detail.

3.5 CrossWind Debugger
The Tornado CrossWind debugger allows single stepping through a program with breakpoints specified by the user through a graphical interface.  It, along with the shell,  is an important tool in program debugging.  At a breakpoint, the user can examine memory locations, variables by name when in scope, the stack, and registers of the target processor.  The user can also examine disassembled C code.  CrossWind is actually an enhancement of the GNU debug utility, GDB.  The CrossWind window  has a command line where GDB commands can be used such as run, show, attach detach, kill, and break -- to name only a few -- are available.  (View GNU's online GDB reference.)   The debugger has a Tcl interface which can be used for scripting debugging setups.  For instance, the user might want the ability to break, examine memory, and resume at several specific points in the program being tested for changing hardware connections, or observing the effect of inserting different filters, say.

3.6 Browser

Tornado provides a somewhat overlooked display browser for examining the target status.  The browser can used to check the status of tasks, semaphores, message queues, and memory objects.  The browser shows CPU usage, stack usage and memory fragmentation dynamically, all of which help pinpoint problem areas that need changes in user code.

 

Appendix - Tornado 2 File Tree

Important parts of the Tornado System file tree are shown in the table below.  Subdirectory names are followed by a forward slash, which on NT platforms may be replaced by a backward slash.

tornado/

docs/ Online documentation
host/ Tools used on development host computer.
SETUP/
share/ Protocol definitiones shared by host and target software.
target/
config/
all/ System configuration information, maintained by the Project facility in 
Tornado IDE.
bsp_name/ Customization for a particular board named bsp_name.
h/ Header files
lib/ Processor-independent libraries and modules
src/ All .c-source files
config/ Source for VxWorks-supplied modules other than device drivers
demo/ Wind River-supplied demonstrations
drv/ Source code for device drivers
usr/ User-modifiable code
.wind/ tcl and initialization files used for customizing Tornado tools.

REFERENCES
 

1 VxWorks® Programmer's Guide, 5.4, Wind River Systems, Inc., 1999.  Alameda, CA 
2 Tornado BSP Developer's Kit for VxWorks® User's Guide Wind River Systems, Inc., 1999.  Alameda, CA
3. Tornado User's Guide.  Wind River Systems, Inc., 1999.  Alameda, CA.    Comments apply to both Windows and Unix versions.