4

I/O System



4.1    Introduction

The VxWorks I/O system is designed to present a simple, uniform, device-independent interface to any kind of device, including:

  • character-oriented devices such as terminals or communications lines
  • random-access block devices such as disks
  • virtual devices such as intertask pipes and sockets
  • monitor and control devices such as digital and analog I/O devices
  • network devices that give access to remote devices

The VxWorks I/O system provides standard C libraries for both basic and buffered I/O. The basic I/O libraries are UNIX-compatible; the buffered I/O libraries are ANSI C-compatible. Internally, the VxWorks I/O system has a unique design that makes it faster and more flexible than most other I/O systems. These are important attributes in a real-time system.

This chapter first describes the nature of files and devices, and the user view of basic and buffered I/O. The next section discusses the details of some specific devices. The third section is a detailed discussion of the internal structure of the VxWorks I/O system. The final sections describe PCMCIA and PCI support.

Figure 4-1:   Overview of the VxWorks I/O System

The diagram in Figure 4-1 illustrates the relationships between the different elements of the VxWorks I/O system. All of these elements are discussed in this chapter, except for file system routines (which are dealt with in 5. Local File Systems), and the network elements (which are covered in the VxWorks Network Programmer's Guide).


*      
NOTE: In Figure 4-1, the dotted arrow line indicates that the CBIO block device is an optional path between File System Routines and Driver Routines.



4.2    Files, Devices, and Drivers

In VxWorks, applications access I/O devices by opening named files. A file can refer to one of two things:

  • An unstructured "raw" device such as a serial communications channel or an intertask pipe.

  • A logical file on a structured, random-access device containing a file system.

Consider the following named files:

/usr/myfile

/pipe/mypipe

/tyCo/0

The first refers to a file called myfile, on a disk device called /usr. The second is a named pipe (by convention, pipe names begin with /pipe). The third refers to a physical serial channel. However, I/O can be done to or from any of these in the same way. Within VxWorks, they are all called files, even though they refer to very different physical objects.

Devices are handled by program modules called drivers. In general, using the I/O system does not require any further understanding of the implementation of devices and drivers. Note, however, that the VxWorks I/O system gives drivers considerable flexibility in the way they handle each specific device. Drivers conform to the conventional user view presented here, but can differ in the specifics. See 4.7 Devices in VxWorks.

Although all I/O is directed at named files, it can be done at two different levels: basic and buffered. The two differ in the way data is buffered and in the types of calls that can be made. These two levels are discussed in later sections.

4.2.1   Filenames and the Default Device

A filename is specified as a character string. An unstructured device is specified with the device name. In the case of file system devices, the device name is followed by a filename. Thus, the name /tyCo/0 might name a particular serial I/O channel, and the name DEV1:/file1 probably indicates the file file1 on the DEV1: device.

When a filename is specified in an I/O call, the I/O system searches for a device with a name that matches at least an initial substring of the filename. The I/O function is then directed at this device.

If a matching device name cannot be found, then the I/O function is directed at a default device. You can set this default device to be any device in the system, including no device at all, in which case failure to match a device name returns an error. You can obtain the current default path by using ioDefPathGet( ). You can set the default path by using ioDefPathSet( ).

Non-block devices are named when they are added to the I/O system, usually at system initialization time. Block devices are named when they are initialized for use with a specific file system. The VxWorks I/O system imposes no restrictions on the names given to devices. The I/O system does not interpret device or filenames in any way, other than during the search for matching device and filenames.

It is useful to adopt some naming conventions for device and filenames: most device names begin with a slash (/), except non-NFS network devices and VxWorks DOS devices (dosFs).

By convention, NFS-based network devices are mounted with names that begin with a slash. For example:

/usr

Non-NFS network devices are named with the remote machine name followed by a colon. For example:

host:

The remainder of the name is the filename in the remote directory on the remote system.

File system devices using dosFs are often named with uppercase letters and/or digits followed by a colon. For example:

DEV1:


*      
NOTE: Filenames and directory names on dosFs devices are often separated by backslashes (\). These can be used interchangeably with forward slashes (/).


*      
CAUTION: Because device names are recognized by the I/O system using simple substring matching, a slash (/) should not be used alone as a device name.



4.3    Basic I/O

Basic I/O is the lowest level of I/O in VxWorks. The basic I/O interface is source-compatible with the I/O primitives in the standard C library. There are seven basic I/O calls, shown in Table 4-1.

Table 4-1:   Basic I/O Routines   


Call
Description

creat( )
Creates a file.
delete( )
Deletes a file.
open( )
Opens a file. (Optionally, creates a file.)
close( )
Closes a file.
read( )
Reads a previously created or opened file.
write( )
Writes to a previously created or opened file.
ioctl( )
Performs special control functions on files.

4.3.1   File Descriptors

At the basic I/O level, files are referred to by a file descriptor, or fd. An fd is a small
integer returned by a call to open( ) or creat( ). The other basic I/O calls take an fd as a parameter to specify the intended file.

A file descriptor is global to a system. For example, a task, A, that performs a write( ) on fd 7 will write to the same file (and device) as a task, B, that performs a write( ) on fd 7.

When a file is opened, an fd is allocated and returned. When the file is closed, the fd is deallocated. There are a finite number of fds available in VxWorks. To avoid exceeding the system limit, it is important to close fds that are no longer in use. The number of available fds is specified in the initialization of the I/O system.

By default, file descriptors are reclaimed only when the file is closed.

4.3.2   Standard Input, Standard Output, and Standard Error

Three file descriptors are reserved and have special meanings:

0 = standard input
1 = standard output
2 = standard error output

These fds are never returned as the result of an open( ) or creat( ), but serve rather as indirect references that can be redirected to any other open fd.

These standard fds are used to make tasks and modules independent of their actual I/O assignments. If a module sends its output to standard output (fd = 1), then its output can be redirected to any file or device, without altering the module.

VxWorks allows two levels of redirection. First, there is a global assignment of the three standard fds. Second, individual tasks can override the global assignment of these fds with their own assignments that apply only to that task.

Global Redirection

When VxWorks is initialized, the global assignments of the standard fds are directed, by default, to the system console. When tasks are spawned, they initially have no task-specific fd assignments; instead, they use the global assignments.

The global assignments can be redirected using ioGlobalStdSet( ). The parameters to this routine are the global standard fd to be redirected, and the fd to direct it to.

For example, the following call sets global standard output (fd = 1) to be the open file with a file descriptor of fileFd:

ioGlobalStdSet (1, fileFd);

All tasks in the system that do not have their own task-specific redirection write standard output to that file thereafter. For example, the task tRlogind calls ioGlobalStdSet( ) to redirect I/O across the network during an rlogin session.

Task-Specific Redirection

The assignments for a specific task can be redirected using the routine ioTaskStdSet( ). The parameters to this routine are the task ID (0 = self) of the task with the assignments to be redirected, the standard fd to be redirected, and the fd to direct it to. For example, a task can make the following call to write standard output to fileFd:

ioTaskStdSet (0, 1, fileFd);

All other tasks are unaffected by this redirection, and subsequent global redirections of standard output do not affect this task.

4.3.3   Open and Close

Before I/O can be performed to a device, an fd must be opened to that device by invoking the open( ) routine (or creat( ), as discussed in the next section). The arguments to open( ) are the filename, the type of access, and, when necessary, the mode:

 fd = open ("name", flagsmode);

The possible access flags are shown in Table 4-2.

Table 4-2:   File Access Flags   


Flag
Hex Value
Description

O_RDONLY
0
Opens for reading only.
O_WRONLY
1
Opens for writing only.
O_RDWR
2
Opens for reading and writing.
O_CREAT
200
Creates a new file.
O_TRUNC
400
Truncates the file.


*      
WARNING: While the third parameter to open( ) is usually optional for other operating systems, it is required for the VxWorks implementation of open( ). When the third parameter is not appropriate for any given call, it should be set to zero. Note that this can be an issue when porting software from UNIX to VxWorks.

The mode parameter is used in the following special cases to specify the mode (permission bits) of a file or to create subdirectories:

  • In general, you can open only preexisting devices and files with open( ). However, with NFS network and dosFs devices, you can also create files with open( ) by OR'ing O_CREAT with one of the access flags. For NFS devices, open( ) requires the third parameter specifying the mode of the file:

fd = open ("name", O_CREAT | O_RDWR, 0644);

  • With both dosFs and NFS devices, you can use the O_CREAT option to create a subdirectory by setting mode to FSTAT_DIR. Other uses of the mode parameter with dosFs devices are ignored.

The open( ) routine, if successful, returns a file descriptor (fd). This fd is then used in subsequent I/O calls to specify that file. The fd is a global identifier that is not task specific. One task can open a file, and then any other tasks can use the resulting fd (for example, pipes). The fd remains valid until close( ) is invoked with that fd:

close (fd);

At that point, I/O to the file is flushed (completely written out) and the fd can no longer be used by any task. However, the same fd number can again be assigned by the I/O system in any subsequent open( ).

When a task exits or is deleted, the files opened by that task are not automatically closed unless the task owns the files, because fds are not task-specific by default. Thus, it is recommended that tasks explicitly close all files when they are no longer required. As stated previously, there is a limit to the number of files that can be open at one time.

4.3.4   Create and Delete

File-oriented devices must be able to create and delete files as well as open existing files.

The creat( ) routine directs a file-oriented device to make a new file on the device and return a file descriptor for it. The arguments to creat( ) are similar to those of open( ) except that the filename specifies the name of the new file rather than an existing one; the creat( ) routine returns an fd identifying the new file.

fd = creat ("name", flag);

The delete( ) routine deletes a named file on a file-oriented device:

delete ("name");

Do not delete files while they are open.

With non-file-system oriented device names, creat( ) acts exactly like open( ); however, delete( ) has no effect.

4.3.5   Read and Write

After an fd is obtained by invoking open( ) or creat( ), tasks can read bytes from a file with read( ) and write bytes to a file with write( ). The arguments to read( ) are the fd, the address of the buffer to receive input, and the maximum number of bytes to read:

nBytes = read (fd, &buffermaxBytes);

The read( ) routine waits for input to be available from the specified file, and returns the number of bytes actually read. For file-system devices, if the number of bytes read is less than the number requested, a subsequent read( ) returns 0 (zero), indicating end-of-file. For non-file-system devices, the number of bytes read can be less than the number requested even if more bytes are available; a subsequent read( ) may or may not return 0. In the case of serial devices and TCP sockets, repeated calls to read( ) are sometimes necessary to read a specific number of bytes. (See the reference entry for fioRead( ) in fioLib). A return value of ERROR (-1) indicates an unsuccessful read.

The arguments to write( ) are the fd, the address of the buffer that contains the data to be output, and the number of bytes to be written:

actualBytes = write (fd, &buffernBytes);

The write( ) routine ensures that all specified data is at least queued for output before returning to the caller, though the data may not yet have been written to the device (this is driver dependent). write( ) returns the number of bytes written; if the number returned is not equal to the number requested, an error has occurred.

4.3.6   File Truncation

It is sometimes convenient to discard part of the data in a file. After a file is open for writing, you can use the ftruncate( ) routine to truncate a file to a specified size. Its arguments are an fd and the desired length of the file:

status = ftruncate (fdlength);

If it succeeds in truncating the file, ftruncate( ) returns OK. If the size specified is larger than the actual size of the file, or if the fd refers to a device that cannot be truncated, ftruncate( ) returns ERROR, and sets errno to EINVAL.

The ftruncate( ) routine is part of the POSIX 1003.1b standard, but this implementation is only partially POSIX-compliant: creation and modification times may not be updated. This call is supported only by dosFsLib, the DOS-compatible file system library. The routine is provided by the INCLUDE_POSIX_FTRUNCATE component.

4.3.7   I/O Control

The ioctl( ) routine is an open-ended mechanism for performing any I/O functions that do not fit the other basic I/O calls. Examples include determining how many bytes are currently available for input, setting device-specific options, obtaining information about a file system, and positioning random-access files to specific byte positions. The arguments to the ioctl( ) routine are the fd, a code that identifies the control function requested, and an optional function-dependent argument:

result = ioctl (fdfunctionarg);

For example, the following call uses the FIOBAUDRATE function to set the baud rate of a tty device to 9600:

status = ioctl (fd, FIOBAUDRATE, 9600);

The discussion of specific devices in 4.7 Devices in VxWorks summarizes the ioctl( ) functions available for each device. The ioctl( ) control codes are defined in ioLib.h. For more information, see the reference entries for specific device drivers or file systems.

4.3.8   Pending on Multiple File Descriptors: The Select Facility

The VxWorks select facility provides a UNIX- and Windows-compatible method for pending on multiple file descriptors. The library selectLib provides both task-level support, allowing tasks to wait for multiple devices to become active, and device driver support, giving drivers the ability to detect tasks that are pended while waiting for I/O on the device. To use this facility, the header file selectLib.h must be included in your application code.

Task-level support not only gives tasks the ability to simultaneously wait for I/O on multiple devices, but it also allows tasks to specify the maximum time to wait for I/O to become available. An example of using the select facility to pend on multiple file descriptors is a client-server model, in which the server is servicing both local and remote clients. The server task uses a pipe to communicate with local clients and a socket to communicate with remote clients. The server task must respond to clients as quickly as possible. If the server blocks waiting for a request on only one of the communication streams, it cannot service requests that come in on the other stream until it gets a request on the first stream. For example, if the server blocks waiting for a request to arrive in the socket, it cannot service requests that arrive in the pipe until a request arrives in the socket to unblock it. This can delay local tasks waiting to get their requests serviced. The select facility solves this problem by giving the server task the ability to monitor both the socket and the pipe and service requests as they come in, regardless of the communication stream used.

Tasks can block until data becomes available or the device is ready for writing. The select( ) routine returns when one or more file descriptors are ready or a timeout has occurred. Using the select( ) routine, a task specifies the file descriptors on which to wait for activity. Bit fields are used in the select( ) call to specify the read and write file descriptors of interest. When select( ) returns, the bit fields are modified to reflect the file descriptors that have become available. The macros for building and manipulating these bit fields are listed in Table 4-3.

Table 4-3:   Select Macros   


Macro
Function

FD_ZERO
Zeroes all bits.
FD_SET
Sets the bit corresponding to a specified file descriptor.
FD_CLR
Clears a specified bit.
FD_ISSET
Returns 1 if the specified bit is set; otherwise returns 0.

Applications can use select( ) with any character I/O devices that provide support for this facility (for example, pipes, serial devices, and sockets). For information on writing a device driver that supports select( ), see Implementing select( ).

Example 4-1:   The Select Facility

/* selServer.c - select example  
 * In this example, a server task uses two pipes: one for normal-priority  
 * requests, the other for high-priority requests. The server opens both  
 * pipes and blocks while waiting for data to be available in at least one  
 * of the pipes.  
 */ 
 
#include "vxWorks.h" 
#include "selectLib.h" 
#include "fcntl.h" 
 
#define MAX_FDS 2 
#define MAX_DATA 1024 
#define PIPEHI   "/pipe/highPriority" 
#define PIPENORM "/pipe/normalPriority" 
 
/************************************************************************ 
* selServer - reads data as it becomes available from two different pipes  
* 
* Opens two pipe fds, reading from whichever becomes available. The  
* server code assumes the pipes have been created from either another  
* task or the shell. To test this code from the shell do the following: 
*  -> ld < selServer.o 
*  -> pipeDevCreate ("/pipe/highPriority", 5, 1024) 
*  -> pipeDevCreate ("/pipe/normalPriority", 5, 1024) 
*  -> fdHi = open   ("/pipe/highPriority", 1, 0) 
*  -> fdNorm = open ("/pipe/normalPriority", 1, 0) 
*  -> iosFdShow 
*  -> sp selServer 
*  -> i 
 
* At this point you should see selServer's state as pended. You can now 
* write to either pipe to make the selServer display your message. 
*  -> write fdNorm, "Howdy", 6 
*  -> write fdHi, "Urgent", 7 
*/ 
 
STATUS selServer (void) 
    { 
    struct fd_set readFds;      /* bit mask of fds to read from */ 
    int      fds[MAX_FDS];      /* array of fds on which to pend */ 
    int      width;             /* number of fds on which to pend */ 
    int      i;                 /* index for fd array */ 
    char     buffer[MAX_DATA];  /* buffer for data that is read */
/* open file descriptors */ 
    if ((fds[0] = open (PIPEHI, O_RDONLY, 0)) == ERROR) 
        { 
        close (fds[0]); 
        return (ERROR); 
        } 
    if ((fds[1] = open (PIPENORM, O_RDONLY, 0)) == ERROR) 
        { 
        close (fds[0]); 
        close (fds[1]); 
        return (ERROR); 
        } 
/* loop forever reading data and servicing clients */ 
    FOREVER 
        { 
        /* clear bits in read bit mask */ 
        FD_ZERO (&readFds); 
 
/* initialize bit mask */ 
        FD_SET (fds[0], &readFds); 
        FD_SET (fds[1], &readFds); 
        width = (fds[0] > fds[1]) ? fds[0] : fds[1]; 
        width++; 
 
/* pend, waiting for one or more fds to become ready */ 
    if (select (width, &readFds, NULL, NULL, NULL) == ERROR) 
        { 
        close (fds[0]); 
        close (fds[1]); 
        return (ERROR); 
        } 
 
/* step through array and read from fds that are ready */ 
    for (i=0; i< MAX_FDS; i++) 
        { 
        /* check if this fd has data to read */ 
        if (FD_ISSET (fds[i], &readFds)) 
            { 
            /* typically read from fd now that it is ready */ 
            read (fds[i], buffer, MAX_DATA); 
            /* normally service request, for this example print it */ 
            printf ("SELSERVER Reading from %s: %s\n",  
                    (i == 0) ? PIPEHI : PIPENORM, buffer); 
            } 
        } 
    } 
    }


4.4    Buffered I/O: stdio

The VxWorks I/O library provides a buffered I/O package that is compatible with the UNIX and Windows stdio package, and provides full ANSI C support. Configure VxWorks with the ANSI Standard component bundle to provide buffered I/O support.


*      
NOTE: The implementation of printf( ), sprintf( ), and sscanf( ), traditionally considered part of the stdio package, is part of a different package in VxWorks. These routines are discussed in 4.5 Other Formatted I/O.

4.4.1   Using stdio

Although the VxWorks I/O system is efficient, some overhead is associated with each low-level call. First, the I/O system must dispatch from the device-independent user call (read( ), write( ), and so on) to the driver-specific routine for that function. Second, most drivers invoke a mutual exclusion or queuing mechanism to prevent simultaneous requests by multiple users from interfering with each other.

This overhead is quite small because the VxWorks primitives are fast. However, an application processing a single character at a time from a file incurs that overhead for each character if it reads each character with a separate read( ) call:

n = read (fd, &char, 1);

To make this type of I/O more efficient and flexible, the stdio package implements a buffering scheme in which data is read and written in large chunks and buffered privately. This buffering is transparent to the application; it is handled automatically by the stdio routines and macros. To access a file with stdio, a file is opened with fopen( ) instead of open( ) (many stdio calls begin with the letter f):

fp = fopen ("/usr/foo", "r");

The returned value, a file pointer (or fp) is a handle for the opened file and its associated buffers and pointers. An fp is actually a pointer to the associated data structure of type FILE (that is, it is declared as FILE *). By contrast, the low-level I/O routines identify a file with a file descriptor (fd), which is a small integer. In fact, the FILE structure pointed to by the fp contains the underlying fd of the open file.

An already open fd can be associated belatedly with a FILE buffer by calling fdopen( ):

fp = fdopen (fd, "r");

After a file is opened with fopen( ), data can be read with fread( ), or a character at a time with getc( ), and data can be written with fwrite( ), or a character at a time with putc( ).

The routines and macros to get data into or out of a file are extremely efficient. They access the buffer with direct pointers that are incremented as data is read or written by the user. They pause to call the low-level read or write routines only when a read buffer is empty or a write buffer is full.


*      
WARNING: The stdio buffers and pointers are private to a particular task. They are not interlocked with semaphores or any other mutual exclusion mechanism, because this defeats the point of an efficient private buffering scheme. Therefore, multiple tasks must not perform I/O to the same stdio FILE pointer at the same time.

The FILE buffer is deallocated when fclose( ) is called.

4.4.2   Standard Input, Standard Output, and Standard Error

As discussed in 4.3 Basic I/O, there are three special file descriptors (0, 1, and 2) reserved for standard input, standard output, and standard error. Three corresponding stdio FILE buffers are automatically created when a task uses the standard file descriptors, stdin, stdout, and stderr, to do buffered I/O to the standard fds. Each task using the standard I/O fds has its own stdio FILE buffers. The FILE buffers are deallocated when the task exits.



4.5    Other Formatted I/O

This section describes additional formatting routines and facilites.

4.5.1   Special Cases: printf( ), sprintf( ), and sscanf( )

The routines printf( ), sprintf( ), and sscanf( ) are generally considered to be part of the standard stdio package. However, the VxWorks implementation of these routines, while functionally the same, does not use the stdio package. Instead, it uses a self-contained, formatted, non-buffered interface to the I/O system in the library fioLib. Note that these routines provide the functionality specified by ANSI; however, printf( ) is not buffered.

Because these routines are implemented in this way, the full stdio package, which is optional, can be omitted from a VxWorks configuration without sacrificing their availability. Applications requiring printf-style output that is buffered can still accomplish this by calling fprintf( ) explicitly to stdout.

While sscanf( ) is implemented in fioLib and can be used even if stdio is omitted, the same is not true of scanf( ), which is implemented in the usual way in stdio.

4.5.2   Additional Routines: printErr( ) and fdprintf( )

Additional routines in fioLib provide formatted but unbuffered output. The routine printErr( ) is analogous to printf( ) but outputs formatted strings to the standard error fd (2). The routine fdprintf( ) outputs formatted strings to a specified fd.

4.5.3   Message Logging

Another higher-level I/O facility is provided by the library logLib, which allows formatted messages to be logged without having to do I/O in the current task's context, or when there is no task context. The message format and parameters are sent on a message queue to a logging task, which then formats and outputs the message. This is useful when messages must be logged from interrupt level, or when it is desirable not to delay the current task for I/O or use the current task's stack for message formatting (which can take up significant stack space). The message is displayed on the console unless otherwise redirected at system startup using logInit( ) or dynamically using logFdSet( ).



4.6    Asynchronous Input/Output

Asynchronous Input/Output (AIO) is the ability to perform input and output operations concurrently with ordinary internal processing. AIO enables you to de-couple I/O operations from the activities of a particular task when these are logically independent.

The benefit of AIO is greater processing efficiency: it permits I/O operations to take place whenever resources are available, rather than making them await arbitrary events such as the completion of independent operations. AIO eliminates some of the unnecessary blocking of tasks that is caused by ordinary synchronous I/O; this decreases contention for resources between input/output and internal processing, and expedites throughput.

The VxWorks AIO implementation meets the specification in the POSIX 1003.1b standard. Include AIO in your VxWorks configuration with the INCLUDE_POSIX_AIO and INCLUDE_POSIX_AIO_SYSDRV components. The second configuration constant enables the auxiliary AIO system driver, required for asynchronous I/O on all current VxWorks devices.

4.6.1   The POSIX AIO Routines

The VxWorks library aioPxLib provides POSIX AIO routines. To access a file asynchronously, open it with the open( ) routine, like any other file. Thereafter, use the file descriptor returned by open( ) in calls to the AIO routines. The POSIX AIO routines (and two associated non-POSIX routines) are listed in Table 4-4.

The default VxWorks initialization code calls aioPxLibInit( ) automatically when the POSIX AIO component is included in VxWorks with INCLUDE_POSIX_AIO.

The aioPxLibInit( ) routine takes one parameter, the maximum number of lio_listio( ) calls that can be outstanding at one time. By default this parameter is MAX_LIO_CALLS. When the parameter is 0 (the default), the value is taken from AIO_CLUST_MAX (defined in installDir/target/h/private/aioPxLibP.h).

The AIO system driver, aioSysDrv, is initialized by default with the routine aioSysInit( ) when both INCLUDE_POSIX_AIO and INCLUDE_POSIX_AIO_SYSDRV are included in VxWorks. The purpose of aioSysDrv is to provide request queues independent of any particular device driver, so that you can use any VxWorks device driver with AIO.

Table 4-4:   Asynchronous Input/Output Routines   


Function
Description

aioPxLibInit( )
Initializes the AIO library (non-POSIX).
aioShow( )
Displays the outstanding AIO requests (non-POSIX).1
aio_read( )
Initiates an asynchronous read operation.
aio_write( )
Initiates an asynchronous write operation.
aio_listio( )
Initiates a list of up to LIO_MAX asynchronous I/O requests.
aio_error( )
Retrieves the error status of an AIO operation.
aio_return( )
Retrieves the return status of a completed AIO operation.
aio_cancel( )
Cancels a previously submitted AIO operation.
aio_suspend( )
Waits until an AIO operation is done, interrupted, or timed out.

1:  This function is not built into the Tornado shell. To use it from the Tornado shell, VxWorks must be configured with the INCLUDE_POSIX_AIO_SHOW component. When you invoke the function, its output is sent to the standard output device.

The routine aioSysInit( ) takes three parameters: the number of AIO system tasks to spawn, and the priority and stack size for these system tasks. The number of AIO system tasks spawned equals the number of AIO requests that can be handled in parallel. The default initialization call uses three constants: MAX_AIO_SYS_TASKS, AIO_TASK_PRIORITY, and AIO_TASK_STACK_SIZE.

When any of the parameters passed to aioSysInit( ) is 0, the corresponding value is taken from AIO_IO_TASKS_DFLT, AIO_IO_PRIO_DFLT, and AIO_IO_STACK_DFLT (all defined in installDir/target/h/aioSysDrv.h).

Table 4-5 lists the names of the constants, and shows the constants used within initialization routines when the parameters are left at their default values of 0, and where these constants are defined.

Table 4-5:   AIO Initialization Functions and Related Constants   


Initialization
Function
Configuration Parameter
Def.
Value
Header File Constant used when arg = 0
Def.
Value
Header File
(all in installDir/target

aioPxLibInit( )
MAX_LIO_CALLS
0
AIO_CLUST_MAX
100
h/private/aioPxLibP.h
aioSysInit( )
MAX_AIO_SYS_TASKS
0
AIO_IO_TASKS_DFLT
2
h/aioSysDrv.h
  
AIO_TASK_PRIORITY
0
AIO_IO_PRIO_DFLT
50
h/aioSysDrv.h
  
AIO_TASK_STACK_SIZE
0
AIO_IO_STACK_DFLT
0x7000
h/aioSysDrv.h

4.6.2   AIO Control Block

Each of the AIO calls takes an AIO control block (aiocb) as an argument to describe the AIO operation. The calling routine must allocate space for the control block, which is associated with a single AIO operation. No two concurrent AIO operations can use the same control block; an attempt to do so yields undefined results.

The aiocb and the data buffers it references are used by the system while performing the associated request. Therefore, after you request an AIO operation, you must not modify the corresponding aiocb before calling aio_return( ); this function frees the aiocb for modification or reuse.

The aiocb structure is defined in aio.h. It contains the following fields:

aio_fildes

aio_offset

aio_buf

aio_nbytes

aio_reqprio

aio_sigevent

aio_lio_opcode

aio_sys_p

For full definitions and important additional information, see the reference entry for aioPxLib.


*      
CAUTION: If a routine allocates stack space for the aiocb, that routine must call aio_return( ) to free the aiocb before returning.

4.6.3   Using AIO

The routines aio_read( ), aio_write( ), or lio_listio( ) initiate AIO operations. The last of these, lio_listio( ), allows you to submit a number of asynchronous requests (read and/or write) at one time. In general, the actual I/O (reads and writes) initiated by these routines does not happen immediately after the AIO request. For this reason, their return values do not reflect the outcome of the actual I/O operation, but only whether a request is successful--that is, whether the AIO routine is able to put the operation on a queue for eventual execution.

After the I/O operations themselves execute, they also generate return values that reflect the success or failure of the I/O. There are two routines that you can use to get information about the success or failure of the I/O operation: aio_error( ) and aio_return( ). You can use aio_error( ) to get the status of an AIO operation (success, failure, or in progress), and aio_return( ) to obtain the return values from the individual I/O operations. Until an AIO operation completes, its error status is EINPROGRESS. To cancel an AIO operation, call aio_cancel( ).

AIO with Periodic Checks for Completion

The following code uses a pipe for the asynchronous I/O operations. The example creates the pipe, submits an AIO read request, verifies that the read request is still in progress, and submits an AIO write request. Under normal circumstances, a synchronous read to an empty pipe blocks and the task does not execute the write, but in the case of AIO, we initiate the read request and continue. After the write request is submitted, the example task loops, checking the status of the AIO requests periodically until both the read and write complete. Because the AIO control blocks are on the stack, we must call aio_return( ) before returning from aioExample( ).

Example 4-2:   Asynchronous I/O

/* aioEx.c - example code for using asynchronous I/O */ 
 
/* includes */ 
 
#include "vxWorks.h" 
#include "aio.h" 
#include "errno.h" 
 
/* defines */ 
 
#define BUFFER_SIZE 200 
 
/************************************************************************ 
* aioExample - use AIO library * This example shows the basic functions of the AIO library. 
* RETURNS: OK if successful, otherwise ERROR. 
*/ 
 
STATUS aioExample (void) 
    { 
    int       fd; 
    static char   exFile [] = "/pipe/1stPipe"; 
    struct aiocb  aiocb_read; /* read aiocb */ 
    struct aiocb  aiocb_write; /* write aiocb */ 
    static char *  test_string = "testing 1 2 3"; 
    char      buffer [BUFFER_SIZE]; /* buffer for read aiocb */ 
 
pipeDevCreate (exFile, 50, 100); 
 
if ((fd = open (exFile, O_CREAT | O_TRUNC | O_RDWR, 0666)) == 
    ERROR) 
    { 
    printf ("aioExample: cannot open %s errno 0x%x\n", exFile, errno); 
    return (ERROR); 
    } 
 
printf ("aioExample: Example file = %s\tFile descriptor = %d\n", 
      exFile, fd);  
 
/* initialize read and write aiocbs */ 
bzero ((char *) &aiocb_read, sizeof (struct aiocb)); 
bzero ((char *) buffer, sizeof (buffer)); 
    aiocb_read.aio_fildes = fd; 
    aiocb_read.aio_buf = buffer; 
    aiocb_read.aio_nbytes = BUFFER_SIZE; 
    aiocb_read.aio_reqprio = 0; 
 
bzero ((char *) &aiocb_write, sizeof (struct aiocb)); 
    aiocb_write.aio_fildes = fd; 
    aiocb_write.aio_buf = test_string; 
    aiocb_write.aio_nbytes = strlen (test_string); 
    aiocb_write.aio_reqprio = 0; 
 
/* initiate the read */ 
if (aio_read (&aiocb_read) == -1) 
    printf ("aioExample: aio_read failed\n"); 
 
/* verify that it is in progress */ 
if (aio_error (&aiocb_read) == EINPROGRESS) 
    printf ("aioExample: read is still in progress\n"); 
 
/* write to pipe - the read should be able to complete */ 
    printf ("aioExample: getting ready to initiate the write\n"); 
    if (aio_write (&aiocb_write) == -1) 
        printf ("aioExample: aio_write failed\n");  
 
/* wait til both read and write are complete */ 
    while ((aio_error (&aiocb_read) == EINPROGRESS) ||  
           (aio_error (&aiocb_write) == EINPROGRESS)) 
        taskDelay (1); 
 
/* print out what was read */ 
    printf ("aioExample: message = %s\n", buffer); 
 
/* clean up */ 
    if (aio_return (&aiocb_read) == -1) 
        printf ("aioExample: aio_return for aiocb_read failed\n"); 
    if (aio_return (&aiocb_write) == -1) 
        printf ("aioExample: aio_return for aiocb_write failed\n"); 
 
close (fd); 
    return (OK); 
    }

Alternatives for Testing AIO Completion

A task can determine whether an AIO request is complete in any of the following ways:

  • Check the result of aio_error( ) periodically, as in the previous example, until the status of an AIO request is no longer EINPROGRESS.

  • Use aio_suspend( ) to suspend the task until the AIO request is complete.

  • Use signals to be informed when the AIO request is complete.

The following example is similar to the preceding aioExample( ), except that it uses signals for notification that the write operation has finished. If you test this from the shell, spawn the routine to run at a lower priority than the AIO system tasks to assure that the test routine does not block completion of the AIO request.

Example 4-3:   Asynchronous I/O with Signals

/* aioExSig.c - example code for using signals with asynchronous I/O */
/* includes */
#include "vxWorks.h" 
#include "aio.h" 
#include "errno.h"
/* defines */
#define BUFFER_SIZE   200 
#define LIST_SIZE    1 
#define EXAMPLE_SIG_NO  25 /* signal number */
/* forward declarations */ 
 
void mySigHandler (int sig, struct siginfo * info, void * pContext); 
 
/************************************************************************ 
* aioExampleSig - use AIO library. 
* 
* This example shows the basic functions of the AIO library. 
* Note if this is run from the shell it must be spawned. Use: 
*  -> sp aioExampleSig 
* 
* RETURNS: OK if successful, otherwise ERROR. 
*/
STATUS aioExampleSig (void) 
    { 
    int           fd; 
    static char          exFile [] = "/pipe/1stPipe"; 
    struct aiocb         aiocb_read;        /* read aiocb */ 
    static struct aiocb  aiocb_write;       /* write aiocb */ 
    struct sigaction     action;            /* signal info */ 
    static char *        test_string = "testing 1 2 3"; 
    char          buffer [BUFFER_SIZE];     /* aiocb read buffer */
pipeDevCreate (exFile, 50, 100); 
 
if ((fd = open (exFile, O_CREAT | O_TRUNC| O_RDWR, 0666)) == ERROR) 
    { 
    printf ("aioExample: cannot open %s errno 0x%x\n", exFile, errno); 
    return (ERROR); 
    } 
 
printf ("aioExampleSig: Example file = %s\tFile descriptor = %d\n",  
         exFile, fd); 
/* set up signal handler for EXAMPLE_SIG_NO */
    action.sa_sigaction = mySigHandler; 
    action.sa_flags = SA_SIGINFO; 
    sigemptyset (&action.sa_mask); 
    sigaction (EXAMPLE_SIG_NO, &action, NULL);
/* initialize read and write aiocbs */
    bzero ((char *) &aiocb_read, sizeof (struct aiocb)); 
    bzero ((char *) buffer, sizeof (buffer)); 
    aiocb_read.aio_fildes = fd; 
    aiocb_read.aio_buf = buffer; 
    aiocb_read.aio_nbytes = BUFFER_SIZE; 
    aiocb_read.aio_reqprio = 0;
bzero ((char *) &aiocb_write, sizeof (struct aiocb)); 
    aiocb_write.aio_fildes = fd; 
    aiocb_write.aio_buf = test_string; 
    aiocb_write.aio_nbytes = strlen (test_string); 
    aiocb_write.aio_reqprio = 0;
/* set up signal info */
    aiocb_write.aio_sigevent.sigev_signo = EXAMPLE_SIG_NO; 
    aiocb_write.aio_sigevent.sigev_notify = SIGEV_SIGNAL; 
    aiocb_write.aio_sigevent.sigev_value.sival_ptr =  
                        (void *) &aiocb_write;
/* initiate the read */
   if (aio_read (&aiocb_read) == -1) 
        printf ("aioExampleSig: aio_read failed\n");
/* verify that it is in progress */
    if (aio_error (&aiocb_read) == EINPROGRESS) 
        printf ("aioExampleSig: read is still in progress\n");
/* write to pipe - the read should be able to complete */
    printf ("aioExampleSig: getting ready to initiate the write\n"); 
    if (aio_write (&aiocb_write) == -1) 
        printf ("aioExampleSig: aio_write failed\n");
/* clean up */
    if (aio_return (&aiocb_read) == -1) 
        printf ("aioExampleSig: aio_return for aiocb_read failed\n"); 
    else 
        printf ("aioExampleSig: aio read message = %s\n", 
                  aiocb_read.aio_buf);
close (fd); 
    return (OK); 
    }
void mySigHandler  
    ( 
    int        sig, 
    struct siginfo * info, 
    void *      pContext 
    ) 
 
    { 
    /* print out what was read */ 
    printf ("mySigHandler: Got signal for aio write\n"); 
 
    /* write is complete so let's do cleanup for it here */ 
    if (aio_return (info->si_value.sival_ptr) == -1) 
        { 
        printf ("mySigHandler: aio_return for aiocb_write failed\n"); 
        printErrno (0); 
        } 
    }


4.7    Devices in VxWorks

The VxWorks I/O system is flexible, allowing specific device drivers to handle the seven I/O functions. All VxWorks device drivers follow the basic conventions outlined previously, but differ in specifics; this section describes those specifics.

Table 4-6:   Drivers Provided with VxWorks   


Module
Driver Description

ttyDrv
Terminal driver
ptyDrv
Pseudo-terminal driver
pipeDrv
Pipe driver
memDrv
Pseudo memory device driver
nfsDrv
NFS client driver
netDrv
Network driver for remote file access
ramDrv
RAM driver for creating a RAM disk
scsiLib
SCSI interface library
-
Other hardware-specific drivers

4.7.1   Serial I/O Devices (Terminal and Pseudo-Terminal Devices)

VxWorks provides terminal and pseudo-terminal device drivers (tty and pty drivers). The tty driver is for actual terminals; the pty driver is for processes that simulate terminals. These pseudo terminals are useful in applications such as remote login facilities.

VxWorks serial I/O devices are buffered serial byte streams. Each device has a ring buffer (circular buffer) for both input and output. Reading from a tty device extracts bytes from the input ring. Writing to a tty device adds bytes to the output ring. The size of each ring buffer is specified when the device is created during system initialization.


*      
NOTE: For the remainder of this section, the term tty is used to indicate both tty and pty devices

tty Options

The tty devices have a full range of options that affect the behavior of the device. These options are selected by setting bits in the device option word using the ioctl( ) routine with the FIOSETOPTIONS function (see I/O Control Functions). For example, to set all the tty options except OPT_MON_TRAP:

status = ioctl (fd, FIOSETOPTIONS, OPT_TERMINAL & ~OPT_MON_TRAP);

Table 4-7 is a summary of the available options. The listed names are defined in the header file ioLib.h. For more detailed information, see the reference entry for tyLib.

Table 4-7:   Tty Options   


Library
Description

OPT_LINE
Selects line mode. (See Raw Mode and Line Mode.)
OPT_ECHO
Echoes input characters to the output of the same channel.
OPT_CRMOD
Translates input RETURN characters into NEWLINE (\n); translates output NEWLINE into RETURN-LINEFEED.
OPT_TANDEM
Responds software flow control characters CTRL+Q and CTRL+S (XON and XOFF).
OPT_7_BIT
Strips the most significant bit from all input bytes.
OPT_MON_TRAP
Enables the special ROM monitor trap character, CTRL+X by default.
OPT_ABORT
Enables the special target shell abort character, CTRL+C by default. (Only useful if the target shell is configured into the system; see 6. Target Tools in this manual for details.)
OPT_TERMINAL
Sets all of the above option bits.
OPT_RAW
Sets none of the above option bits.

Raw Mode and Line Mode

A tty device operates in one of two modes: raw mode (unbuffered) or line mode. Raw mode is the default. Line mode is selected by the OPT_LINE bit of the device option word (see tty Options).

In raw mode, each input character is available to readers as soon as it is input from the device. Reading from a tty device in raw mode causes as many characters as possible to be extracted from the input ring, up to the limit of the user's read buffer. Input cannot be modified except as directed by other tty option bits.

In line mode, all input characters are saved until a NEWLINE character is input; then the entire line of characters, including the NEWLINE, is made available in the ring at one time. Reading from a tty device in line mode causes characters up to the end of the next line to be extracted from the input ring, up to the limit of the user's read buffer. Input can be modified by the special characters CTRL+H (backspace), CTRL+U (line-delete), and CTRL+D (end-of-file), which are discussed in Tty Special Characters.

Tty Special Characters

The following special characters are enabled if the tty device operates in line mode, that is, with the OPT_LINE bit set:

  • The backspace character, by default CTRL+H, causes successive previous characters to be deleted from the current line, up to the start of the line. It does this by echoing a backspace followed by a space, and then another backspace.

  • The line-delete character, by default CTRL+U, deletes all the characters of the current line.

  • The end-of-file (EOF) character, by default CTRL+D, causes the current line to become available in the input ring without a NEWLINE and without entering the EOF character itself. Thus if the EOF character is the first character typed on a line, reading that line returns a zero byte count, which is the usual indication of end-of-file.

The following characters have special effects if the tty device is operating with the corresponding option bit set:

  • The software flow control characters CTRL+Q and CTRL+S (XON and XOFF). Receipt of a CTRL+S input character suspends output to that channel. Subsequent receipt of a CTRL+Q resumes the output. Conversely, when the VxWorks input buffer is almost full, a CTRL+S is output to signal the other side to suspend transmission. When the input buffer is empty enough, a CTRL+Q is output to signal the other side to resume transmission. The software flow control characters are enabled by OPT_TANDEM.

  • The ROM monitor trap character, by default CTRL+X. This character traps to the ROM-resident monitor program. Note that this is drastic. All normal VxWorks functioning is suspended, and the computer system is controlled entirely by the monitor. Depending on the particular monitor, it may or may not be possible to restart VxWorks from the point of interruption.1 The monitor trap character is enabled by OPT_MON_TRAP.

  • The special target shell abort character, by default CTRL+C. This character restarts the target shell if it gets stuck in an unfriendly routine, such as one that has taken an unavailable semaphore or is caught in an infinite loop. The target shell abort character is enabled by OPT_ABORT.

The characters for most of these functions can be changed using the tyLib routines shown in Table 4-8.

Table 4-8:   Tty Special Characters   


Character
Description
Modifier

CTRL+H
backspace (character delete)
tyBackspaceSet( )
CTRL+U
line delete
tyDeleteLineSet( )
CTRL+D
EOF (end of file)
tyEOFSet( )
CTRL+C
target shell abort
tyAbortSet( )
CTRL+X
trap to boot ROMs
tyMonitorTrapSet( )
CTRL+S
output suspend
N/A
CTRL+Q
output resume
N/A

I/O Control Functions

The tty devices respond to the ioctl( ) functions in Table 4-9, defined in ioLib.h. For more information, see the reference entries for tyLib, ttyDrv, and ioctl( ).

Table 4-9:   I/O Control Functions Supported by tyLib   


Function
Description

FIOBAUDRATE
Sets the baud rate to the specified argument.
FIOCANCEL
Cancels a read or write.
FIOFLUSH
Discards all bytes in the input and output buffers.
FIOGETNAME
Gets the filename of the fd.
FIOGETOPTIONS
Returns the current device option word.
FIONREAD
Gets the number of unread bytes in the input buffer.
FIONWRITE
Gets the number of bytes in the output buffer.
FIOSETOPTIONS
Sets the device option word.


*      
CAUTION: To change the driver's hardware options (for example, the number of stop bits or parity bits), use the ioctl( ) function SIO_HW_OPTS_SET. Because this command is not implemented in most drivers, you may need to add it to your BSP serial driver, which resides in installDir/target/src/drv/sio. The details of how to implement this command depend on your board's serial chip. The constants defined in the header file installDir/target/h/sioLib.h provide the POSIX definitions for setting the hardware options.

4.7.2   Pipe Devices

Pipes are virtual devices by which tasks communicate with each other through the I/O system. Tasks write messages to pipes; these messages can then be read by other tasks. Pipe devices are managed by pipeDrv and use the kernel message queue facility to bear the actual message traffic.

Creating Pipes

Pipes are created by calling the pipe create routine:

status = pipeDevCreate ("/pipe/name", maxMsgs, maxLength);

The new pipe can have at most maxMsgs messages queued at a time. Tasks that write to a pipe that already has the maximum number of messages queued are blocked until a message is dequeued. Each message in the pipe can be at most maxLength bytes long; attempts to write longer messages result in an error.

Writing to Pipes from ISRs

VxWorks pipes are designed to allow ISRs to write to pipes in the same way as task-level code. Many VxWorks facilities cannot be used from ISRs, including output to devices other than pipes. However, ISRs can use pipes to communicate with tasks, which can then invoke such facilities. ISRs write to a pipe using the write( ) call. Tasks and ISRs can write to the same pipes. However, if the pipe is full, the message is discarded because the ISRs cannot pend. ISRs must not invoke any I/O function on pipes other than write( ). For more informationon ISRs, see 2.6 Interrupt Service Code: ISRs.

I/O Control Functions

Pipe devices respond to the ioctl( ) functions summarized in Table 4-10. The functions listed are defined in the header file ioLib.h. For more information, see the reference entries for pipeDrv and for ioctl( ) in ioLib.

Table 4-10:   I/O Control Functions Supported by pipeDrv   


Function
Description

FIOFLUSH
Discards all messages in the pipe.
FIOGETNAME
Gets the pipe name of the fd.
FIONMSGS
Gets the number of messages remaining in the pipe.
FIONREAD
Gets the size in bytes of the first message in the pipe.

4.7.3   Pseudo Memory Devices

The memDrv driver allows the I/O system to access memory directly as a pseudo-I/O device. Memory location and size are specified when the device is created. This feature is useful when data must be preserved between boots of VxWorks or when sharing data between CPUs. This driver does not implement a file system, unlike ramDrv. The ramDrv driver must be given memory over which it has absolute control; whereas memDrv provides a high-level method of reading and writing bytes in absolute memory locations through I/O calls.

Installing the Memory Driver

The driver is initialized automatically by the system with memDrv( ) when the INCLUDE_USR_MEMDRV component is included in the VxWorks kernel domain. The call for device creation must be made from the kernel domain:

STATUS memDevCreate 
    (char * name, char * base, int length)

Memory for the device is an absolute memory location beginning at base. The length parameter indicates the size of the memory. For additional information on the memory driver, see the entries for memDrv( ), memDevCreate( ), and memDev CreateDir( ) in the VxWorks API Reference, as well as the entry for the host tool memdrvbuild in the Tornado Tools section of the online Tornado Tools Reference.

I/O Control Functions

The memory driver responds to the ioctl( ) functions summarized in Table 4-11. The functions listed are defined in the header file ioLib.h.

Table 4-11:   I/O Control Functions Supported by memDrv   


Function
Description

FIOSEEK
Sets the current byte offset in the file.
FIOWHERE
Returns the current byte position in the file.

For more information, see the reference entries for memDrv and for ioctl( ) in ioLib.

4.7.4   Network File System (NFS) Devices

Network File System (NFS) devices allow files on remote hosts to be accessed with the NFS protocol. The NFS protocol specifies both client software, to read files from remote machines, and server software, to export files to remote machines.

The driver nfsDrv acts as a VxWorks NFS client to access files on any NFS server on the network. VxWorks also allows you to run an NFS server to export files to other systems; see VxWorks Network Programmer's Guide: File Access Applications.

Using NFS devices, you can create, open, and access remote files exactly as though they were on a file system on a local disk. This is called network transparency.

Mounting a Remote NFS File System from VxWorks

Access to a remote NFS file system is established by mounting that file system locally and creating an I/O device for it using nfsMount( ). Its arguments are (1) the host name of the NFS server, (2) the name of the host file system, and (3) the local name for the file system.

For example, the following call mounts /usr of the host mars as /vxusr locally:

nfsMount ("mars", "/usr", "/vxusr");

This creates a VxWorks I/O device with the specified local name (/vxusr, in this example). If the local name is specified as NULL, the local name is the same as the remote name.

After a remote file system is mounted, the files are accessed as though the file system were local. Thus, after the previous example, opening the file /vxusr/foo opens the file /usr/foo on the host mars.

The remote file system must be exported by the system on which it actually resides. However, NFS servers can export only local file systems. Use the appropriate command on the server to see which file systems are local. NFS requires authentication parameters to identify the user making the remote access. To set these parameters, use the routines nfsAuthUnixSet( ) and nfsAuthUnixPrompt( ).

To include NFS client support, use the INCLUDE_NFS component.

The subject of exporting and mounting NFS file systems and authenticating access permissions is discussed in more detail in VxWorks Network Programmer's Guide: File Access Applications. See also the reference entries nfsLib and nfsDrv, and the NFS documentation from Sun Microsystems.

I/O Control Functions for NFS Clients

NFS client devices respond to the ioctl( ) functions summarized in Table 4-12. The functions listed are defined in ioLib.h. For more information, see the reference entries for nfsDrv and for ioctl( ) in ioLib.

Table 4-12:   I/O Control Functions Supported by nfsDrv   


Function
Description

FIOFSTATGET
Gets file status information (directory entry data).
FIOGETNAME
Gets the filename of the fd.
FIONREAD
Gets the number of unread bytes in the file.
FIOREADDIR
Reads the next directory entry.
FIOSEEK
Sets the current byte offset in the file.
FIOSYNC
Flushes data to a remote NFS file.
FIOWHERE
Returns the current byte position in the file.

4.7.5   Non-NFS Network Devices

VxWorks also supports network access to files on a remote host through the Remote Shell protocol (RSH) or the File Transfer Protocol (FTP). These implementations of network devices use the driver netDrv. When a remote file is opened using RSH or FTP, the entire file is copied into local memory. As a result, the largest file that can be opened is restricted by the available memory. Read and write operations are performed on the memory-resident copy of the file. When closed, the file is copied back to the original remote file if it was modified.

In general, NFS devices are preferable to RSH and FTP devices for performance and flexibility, because NFS does not copy the entire file into local memory. However, NFS is not supported by all host systems.

Creating Network Devices

To access files on a remote host using either RSH or FTP, a network device must first be created by calling the routine netDevCreate( ). The arguments to netDevCreate( ) are (1) the name of the device, (2) the name of the host the device accesses, and (3) which protocol to use: 0 (RSH) or 1 (FTP).

For example, the following call creates an RSH device called mars: that accesses the host mars. By convention, the name for a network device is the remote machine's name followed by a colon (:).

netDevCreate ("mars:", "mars", 0);

Files on a network device can be created, opened, and manipulated as if on a local disk. Thus, opening the file mars:/usr/foo actually opens /usr/foo on host mars.

Note that creating a network device allows access to any file or device on the remote system, while mounting an NFS file system allows access only to a specified file system.

For the files of a remote host to be accessible with RSH or FTP, permissions and user identification must be established on both the remote and local systems. Creating and configuring network devices is discussed in detail in VxWorks Network Programmer's Guide: File Access Applications and in the reference entry for netDrv.

I/O Control Functions

RSH and FTP devices respond to the same ioctl( ) functions as NFS devices except for FIOSYNC and FIOREADDIR. The functions are defined in the header file ioLib.h. For more information, see the reference entries for netDrv and ioctl( ).

4.7.6   CBIO Interface

The core cached block I/O (CBIO) component, INCLUDE_CBIO_MAIN, provides an interface for file systems such as dosFs and rawFs. It is required for implementing these file systems.

The core CBIO component also provides generic services for other CBIO components that provide optional functionality. The optional CBIO components are:

INCLUDE_CBIO_DCACHE

INCLUDE_CBIO_DPART

INCLUDE_CBIO_RAMDISK

These components are discussed in the following sections as well as in the cbioLib, dcacheCbio, dpartCbio, and ramDiskCbio entries in the VxWorks API Reference.

CBIO Disk Cache

The CBIO disk cache is designed to make rotational media disk I/O more efficient, as well as to automatically detect changes in disks.

Disk I/O Efficiency

The disk cache reduces the number of disk accesses and accelerates disk read and write operations by means of the following techniques:

  • Most Recently Used (MRU) disk blocks are stored in RAM, which results in the most frequently accessed data being retrieved from memory rather than from disk.

  • Reading data from disk is performed in large units, relying on the read-ahead feature, one of the disk cache's tunable parameters.

  • Write operations are optimized because they occur to memory first. Then, updating the disk happens in an orderly manner, by delayed write, another tunable parameter.

Overall, the main performance advantage arises from a dramatic reduction in the amount of time spent seeking by the disk drive, thus maximizing the time available for the disk to read and write actual data. In other words, you get efficient use of the disk drive's available throughput.

The disk cache offers a number of operational parameters that can be tuned by the user to suit a particular file system workload pattern, for example, delayed write, read ahead, and bypass threshold.

The technique of delaying writes to disk means that if the system is turned off unexpectedly, updates that have not yet been written to the disk are lost. To minimize the effect of a possible crash, the disk cache periodically updates the disk. Modified blocks of data are not kept in memory more than a specified period of time.

By specifying a small update period, the possible worst-case loss of data from a crash is the sum of changes possible during that specified period. For example, it is assumed that an update period of two seconds is sufficiently large to effectively optimize disk writes, yet small enough to make the potential loss of data a reasonably minor concern. It is possible to set the update period to zero, in which case, all updates are flushed to disk immediately.

The disk cache allows you to negotiate between disk performance and memory consumption: The more memory allocated to the disk cache, the higher the "hit ratio" observed, which means increasingly better performance of file system operations.

Bypass Threshold

Another tunable parameter is the bypass threshold, which defines how much data constitutes a request large enough to justify bypassing the disk cache.

When significantly large read or write requests are made by the application, the disk cache is circumvented and there is a direct transfer of data between the disk controller and the user data buffer. The use of bypassing, in conjunction with support for contiguous file allocation and access (the FIOCONTIG ioctl( ) command and the DOS_O_CONTIG open( ) flag), should provide performance equivalent to that offered by the raw file system -- rawFs.

For details on all of the tunable parameters associated with the disk cache, see the entry for dcacheDevTune( ) in the VxWorks API Reference. For complete details on the disk cache, see the entry for dcacheCbio.

Detection of Disk Changes

The disk cache component also provides for automatic detection of disk changes. The detection process is based on two assumptions: one about the uniqueness of a disk, and a second about the time required to change a disk.

The first assumption is that the first block--that is, the boot block of each cartridge or diskette--is unique. This is typically the case because most media is pre-formatted during manufacturing, and the boot block contains a 32-bit volume serial ID that is set by the manufacturer as unique. The formatting utility in dosFs preserves the volume ID when one exists on the volume being formatted; when no valid ID is found on the disk it creates a new one based on the time that the formatting takes place.

The second assumption is that it takes at least two seconds to physically replace a disk. If a disk has been inactive for two seconds or longer, its boot block is verified against a previously stored boot block signature. If they do not match, the file system module is notified of the change, and in turn un-mounts the volume, marking all previously open file descriptors as obsolete. A new volume is then mounted, if a disk is in the drive. This can trigger an optional automatic consistency check that detects any structural inconsistencies resulting from a previous disk removal in the midst of a disk update (see the entry for dosFsDevCreate( ) in the VxWorks API Reference).


*      
CAUTION: The disk cache is designed to benefit rotational media devices and should not be used with RAM disks and TrueFFS disks.

CBIO Disk Partition Handler

It has become commonplace to share fixed disks and removable cartridges between VxWorks target systems and PCs running Windows. Therefore, dosFs provides support for PC-style disk partitioning.

Two modules provide partition management mechanisms:

dpartCbioLib

usrFdiskPartLib

The dpartCbioLib module handles partitions on both fixed and removable drives, by calling the user-supplied partition table decode routine. The routine is called whenever removable media has been changed. When initializing the system, you must define the maximum number of partitions you expect to exist on a particular drive, and then associate a logical device name with each partition. If you insert removable media having fewer partitions than the maximum number defined, then only the number of partitions configured on the removable media are accessible. Additional partitions previously defined and named cannot be accessed.

CBIO RAM Disk

In some applications it is desirable to use a file system to organize and access data although no disk or other traditional media is present. The CBIO RAM disk facility allows the use of a file system to access data stored in RAM memory. RAM disks can be created using volatile as well a non-volatile RAM.


*      
NOTE: The ramDiskCbio library implements a RAM disk using the CBIO interface; the ramDrv library implements a RAM disk using the BLK_DEV interface.

For more information, see the ramDiskCbio entries in the VxWorks API Reference.

I/O Control Functions for CBIO Devices

CBIO devices respond to the ioctl( ) functions summarized in Table 4-13.

Table 4-13:   I/O Control Functions Supported by CBIO Devices


Function
Description

CBIO_RESET
Resets the device.
CBIO_STATUS_CHK
Checks the device status.
CBIO_DEVICE_LOCK
Prevents disk removal.
CBIO_DEVICE_UNLOCK
Allows disk removal.
CBIO_DEVICE_EJECT
Dismounts the device.
CBIO_CACHE_FLUSH
Flushes dirty caches.
CBIO_CACHE_INVAL
Flushes and invalidates all.
CBIO_CACHE_NEWBLK
Allocates a scratch block.

4.7.7   Block Devices

A block device is a device that is organized as a sequence of individually accessible blocks of data. The most common type of block device is a disk. In VxWorks, the term block refers to the smallest addressable unit on the device. For most disk devices, a VxWorks block corresponds to a sector, although terminology varies.

Block devices in VxWorks have a slightly different interface than other I/O devices. Rather than interacting directly with the I/O system, block device support consists of low-level drivers that interact with a file system. The file system, in turn, interacts with the I/O system. This arrangement allows a single low-level driver to be used with various different file systems and reduces the number of I/O functions that must be supported in the driver. The internal implementation of low-level drivers for block devices is discussed in 4.9.4 Block Devices.

Block Device File Systems

For use with block devices, VxWorks is supplied with file system libraries compatible with the MS-DOS file systems. There are also libraries for a simple raw disk file system, for SCSI tape devices, for CD-ROM devices, and for flash memory devices. Use of these file systems is discussed in 5. Local File Systems in this manual (which discusses the Target Server File System as well). Also see the entries for dosFsLib, rawFsLib, tapeFsLib, cdromFsLib, and tffsDrv in the VxWorks API Reference.

Block Device RAM Disk Drivers

RAM drivers, as implemented in ramDrv, emulate disk devices but actually keep all data in memory. Memory location and "disk" size are specified when a RAM device is created by calling ramDevCreate( ). This routine can be called repeatedly to create multiple RAM disks.

Memory for the RAM disk can be pre-allocated and the address passed to ramDevCreate( ), or memory can be automatically allocated from the system memory pool using malloc( ).

After the device is created, a name and file system (for example, dosFs or rawFs) must be associated with it using the file system's device creation routine and format routine; for example, dosFsDevCreate( ) and dosFsVolFormat( ). Information describing the device is passed to the file system in a BLK_DEV structure. A pointer to this structure is returned by the RAM disk creation routine.

In the following example, a 200 KB RAM disk is created with automatically allocated memory, 512-byte sections, a single track, and no sector offset. The device is assigned the name DEV1: and initialized for use with dosFs.

BLK_DEV               *pBlkDev; 
DOS_VOL_DESC          *pVolDesc; 
pBlkDev = ramDevCreate (0, 512, 400, 400, 0); 
if (dosFsDevCreate ("DEV1:", pBlkDev, 20, NULL) == ERROR) 
{ 
    printErrno( ); 
}; 
 
/* if you want to format then do the following */ 
if (dosFsVolFormat ("DEV1:", 2, NULL) == ERROR) 
{ 
    printErrno( ); 
}

If the RAM disk memory already contains a disk image, the first argument to ramDevCreate( ) is the address in memory, and the formatting arguments must be identical to those used when the image was created. For example:

pBlkDev = ramDevCreate (0xc0000, 512, 400, 400, 0); 
if (dosFsDevCreate ("DEV1:", pBlkDev, 20, NULL) == ERROR) 
{ 
    printErrno( ); 
}

In this case, only dosFsDevCreate( ) must be used, because the file system already exists on the disk and does not require re-formatting. This procedure is useful if a RAM disk is to be created at the same address used in a previous boot of VxWorks. The contents of the RAM disk are then preserved.


*      
NOTE: The ramDiskCbio library implements a RAM disk using the CBIO interface; the ramDrv library implements a RAM disk using the BLK_DEV interface.

For more information on RAM disk drivers, see the reference entry for ramDrv. For more information on file systems, see 5. Local File Systems.

SCSI Drivers

SCSI is a standard peripheral interface that allows connection with a wide variety of hard disks, optical disks, floppy disks, tape drives, and CD-ROM devices. SCSI block drivers are compatible with the dosFs libraries, and offer several advantages for target configurations. They provide:

  • local mass storage in non-networked environments
  • faster I/O throughput than Ethernet networks

The SCSI-2 support in VxWorks supersedes previous SCSI support, although it offers the option of configuring the original SCSI functionality, now known as SCSI-1. With SCSI-2 enabled, the VxWorks environment can still handle SCSI-1 applications, such as file systems created under SCSI-1. However, applications that directly used SCSI-1 data structures defined in scsiLib.h may require modifications and recompilation for SCSI-2 compatibility.

The VxWorks SCSI implementation consists of two modules, one for the device-independent SCSI interface and one to support a specific SCSI controller. The scsiLib library provides routines that support the device-independent interface; device-specific libraries provide configuration routines that support specific controllers. There are also additional support routines for individual targets in sysLib.c.

Configuring SCSI Drivers

Components associated with SCSI drivers are listed in Table 4-14.

Table 4-14:   SCSI and Related Components


Component
Description

INCLUDE_SCSI
Includes SCSI interface.
INCLUDE_SCSI2
Includes SCSI-2 extensions.
INCLUDE_SCSI_DMA
Enables DMA for SCSI.
INCLUDE_SCSI_BOOT
Allows booting from a SCSI device.
SCSI_AUTO_CONFIG
Auto-configures and locates all targets on a SCSI bus.
INCLUDE_DOSFS
Includes the DOS file system.
INCLUDE_TAPEFS
Includes the tape file system.
INCLUDE_CDROMFS
Includes CD-ROM file system support.

To include SCSI-1 functionality in VxWorks, use the INCLUDE_SCSI component. To include SCSI-2 functionality, you must use INCLUDE_SCSI2 in addition to INCLUDE_SCSI.

Auto-configuration, DMA, and booting from a SCSI device are defined appropriately for each BSP. If you need to change these settings, see the reference for sysScsiConfig( ) and the source file installDir/target/src/config/usrScsi.c.


*      
CAUTION: Including SCSI-2 in your VxWorks image can significantly increase the image size.

Configuring the SCSI Bus ID

Each board in a SCSI-2 environment must define a unique SCSI bus ID for the SCSI initiator. SCSI-1 drivers, which support only a single initiator at a time, assume an initiator SCSI bus ID of 7. However, SCSI-2 supports multiple initiators, up to eight initiators and targets at one time. Therefore, to ensure a unique ID, choose a value in the range 0-7 to be passed as a parameter to the driver's initialization routine (for example, ncr710CtrlInitScsi2( )) by the sysScsiInit( ) routine in sysScsi.c. For more information, see the reference entry for the relevant driver initialization routine. If there are multiple boards on one SCSI bus, and all of these boards use the same BSP, then different versions of the BSP must be compiled for each board by assigning unique SCSI bus IDs.

ROM Size Adjustment for SCSI Boot

If the INCLUDE_SCSI_BOOT component is included, larger ROMs may be required for some boards.

Structure of the SCSI Subsystem

The SCSI subsystem supports libraries and drivers for both SCSI-1 and SCSI-2. It consists of the following six libraries which are independent of any SCSI controller:

scsiLib
routines that provide the mechanism for switching SCSI requests to either the SCSI-1 library (scsi1Lib) or the SCSI-2 library (scsi2Lib), as configured by the board support package (BSP).

scsi1Lib
SCSI-1 library routines and interface, used when only INCLUDE_SCSI is used (see Configuring SCSI Drivers).

scsi2Lib
SCSI-2 library routines and all physical device creation and deletion routines.

scsiCommonLib
commands common to all types of SCSI devices.

scsiDirectLib
routines and commands for direct access devices (disks).

scsiSeqLib
routines and commands for sequential access block devices (tapes).

Controller-independent support for the SCSI-2 functionality is divided into scsi2Lib, scsiCommonLib, scsiDirectLib, and scsiSeqLib. The interface to any of these SCSI-2 libraries can be accessed directly. However, scsiSeqLib is designed to be used in conjunction with tapeFs, while scsiDirectLib works with dosFs and rawFs. Applications written for SCSI-1 can be used with SCSI-2; however, SCSI-1 device drivers cannot.

VxWorks targets using SCSI interface controllers require a controller-specific device driver. These device drivers work in conjunction with the controller-independent SCSI libraries, and they provide controller configuration and initialization routines contained in controller-specific libraries. For example, the Western Digital WD33C93 SCSI controller is supported by the device driver libraries wd33c93Lib, wd33c93Lib1, and wd33c93Lib2. Routines tied to SCSI-1 (such as wd33c93CtrlCreate( )) and SCSI-2 (such as wd33c93CtrlCreateScsi2( )) are segregated into separate libraries to simplify configuration. There are also additional support routines for individual targets in sysLib.c.

Booting and Initialization

After VxWorks is built with SCSI support, the system startup code initializes the SCSI interface by executing sysScsiInit( ) and usrScsiConfig( ) when INCLUDE_SCSI is included in VxWorks. The call to sysScsiInit( ) initializes the SCSI controller and sets up interrupt handling. The physical device configuration is specified in usrScsiConfig( ), which is in installDir/target/src/config/usrScsi.c. The routine contains an example of the calling sequence to declare a hypothetical configuration, including:

  • definition of physical devices with scsiPhysDevCreate( )
  • creation of logical partitions with scsiBlkDevCreate( )
  • specification of a file system with dosFsDevCreate( ).

If you are not using SCSI_AUTO_CONFIG, modify usrScsiConfig( ) to reflect your actual configuration. For more information on the calls used in this routine, see the reference entries for scsiPhysDevCreate( ), scsiBlkDevCreate( ), dosFsDevCreate( ), and dosFsVolFormat( ).

Device-Specific Configuration Options

The SCSI libraries have the following default behaviors enabled:

  • SCSI messages
  • disconnects
  • minimum period and maximum REQ/ACK offset
  • tagged command queuing
  • wide data transfer

Device-specific options do not need to be set if the device shares this default behavior. However, if you need to configure a device that diverges from these default characteristics, use scsiTargetOptionsSet( ) to modify option values. These options are fields in the SCSI_OPTIONS structure, shown below. SCSI_OPTIONS is declared in scsi2Lib.h. You can choose to set some or all of these option values to suit your particular SCSI device and application.

typedef struct                    /* SCSI_OPTIONS - programmable options */ 
    { 
    UINT     selTimeOut;          /* device selection time-out (us)       */ 
    BOOL     messages;            /* FALSE => do not use SCSI messages    */ 
    BOOL     disconnect;          /* FALSE => do not use disconnect       */ 
    UINT8     maxOffset;          /* max sync xfer offset (0 => async.)   */ 
    UINT8     minPeriod;          /* min sync xfer period (x 4 ns)        */ 
    SCSI_TAG_TYPE tagType;        /* default tag type                     */ 
    UINT     maxTags;             /* max cmd tags available (0 => untag   */ 
    UINT8     xferWidth;          /* wide data trnsfr width in SCSI units */ 
    } SCSI_OPTIONS;

There are numerous types of SCSI devices, each supporting its own mix of SCSI-2 features. To set device-specific options, define a SCSI_OPTIONS structure and assign the desired values to the structure's fields. After setting the appropriate fields, call scsiTargetOptionsSet( ) to effect your selections. Example 4-5 illustrates one possible device configuration using SCSI_OPTIONS.

Call scsiTargetOptionsSet( ) after initializing the SCSI subsystem, but before initializing the SCSI physical device. For more information about setting and implementing options, see the reference entry for scsiTargetOptionsSet( ).


*      
WARNING: Calling scsiTargetOptionsSet( ) after the physical device has been initialized may lead to undefined behavior.

The SCSI subsystem performs each SCSI command request as a SCSI transaction. This requires the SCSI subsystem to select a device. Different SCSI devices require different amounts of time to respond to a selection; in some cases, the selTimeOut field may need to be altered from the default.

If a device does not support SCSI messages, the boolean field messages can be set to FALSE. Similarly, if a device does not support disconnect/reconnect, the boolean field disconnect can be set to FALSE.

The SCSI subsystem automatically tries to negotiate synchronous data transfer parameters. However, if a SCSI device does not support synchronous data transfer, set the maxOffset field to 0. By default, the SCSI subsystem tries to negotiate the maximum possible REQ/ACK offset and the minimum possible data transfer period supported by the SCSI controller on the VxWorks target. This is done to maximize the speed of transfers between two devices. However, speed depends upon electrical characteristics, like cable length, cable quality, and device termination; therefore, it may be necessary to reduce the values of maxOffset or minPeriod for fast transfers.

The tagType field defines the type of tagged command queuing desired, using one of the following macros:

  • SCSI_TAG_UNTAGGED
  • SCSI_TAG_SIMPLE
  • SCSI_TAG_ORDERED
  • SCSI_TAG_HEAD_OF_QUEUE

For more information about the types of tagged command queuing available, see the ANSI X3T9-I/O Interface Specification Small Computer System Interface (SCSI-2).

The maxTags field sets the maximum number of command tags available for a particular SCSI device.

Wide data transfers with a SCSI target device are automatically negotiated upon initialization by the SCSI subsystem. Wide data transfer parameters are always negotiated before synchronous data transfer parameters, as specified by the SCSI ANSI specification, because a wide negotiation resets any prior negotiation of synchronous parameters. However, if a SCSI device does not support wide parameters and there are problems initializing that device, you must set the xferWidth field to 0. By default, the SCSI subsystem tries to negotiate the maximum possible transfer width supported by the SCSI controller on the VxWorks target in order to maximize the default transfer speed between the two devices. For more information on the actual routine call, see the reference entry for scsiTargetOptionsSet( ).

SCSI Configuration Examples

The following examples show some possible configurations for different SCSI devices. Example 4-4 is a simple block device configuration setup. Example 4-5 involves selecting special options and demonstrates the use of scsiTargetOptionsSet( ). Example 4-6 configures a tape device and a tape file system. Example 4-7 configures a SCSI device for synchronous data transfer. Example 4-8 shows how to configure the SCSI bus ID. These examples can be embedded either in the usrScsiConfig( ) routine or in a user-defined SCSI configuration function.

Example 4-4:   Configuring SCSI Drivers

In the following example, usrScsiConfig( ) was modified to reflect a new system configuration. The new configuration has a SCSI disk with a bus ID of 4 and a Logical Unit Number (LUN) of 0 (zero). The disk is configured with a dosFs file system (with a total size of 0x20000 blocks) and a rawFs file system (spanning the remainder of the disk). The following usrScsiConfig( ) code reflects this modification.

/* configure Winchester at busId = 4, LUN = 0 */ 
 
if ((pSpd40 = scsiPhysDevCreate (pSysScsiCtrl, 4, 0, 0, NONE, 0, 0, 0)) 
        == (SCSI_PHYS_DEV *) NULL) 
    { 
    SCSI_DEBUG_MSG ("usrScsiConfig: scsiPhysDevCreate failed.\n"); 
    } 
else 
    { 
    /* create block devices - one for dosFs and one for rawFs */ 
 
    if (((pSbd0 = scsiBlkDevCreate (pSpd40, 0x20000, 0)) == NULL) || 
        ((pSbd1 = scsiBlkDevCreate (pSpd40, 0, 0x20000)) == NULL)) 
        { 
        return (ERROR); 
        } 
 
    /* initialize both dosFs and rawFs file systems */ 
 
    if ((dosFsDevInit ("/sd0/", pSbd0, NULL) == NULL) || 
        (rawFsDevInit ("/sd1/", pSbd1) == NULL)) 
        { 
        return (ERROR); 
        } 
    }

If problems with your configuration occur, insert the following lines at the beginning of usrScsiConfig( ) to obtain further information on SCSI bus activity.

#if FALSE 
scsiDebug = TRUE; 
scsiIntsDebug = TRUE; 
#endif

Do not declare the global variables scsiDebug and scsiIntsDebug locally. They can be set or reset from the shell; see the Tornado User's Reference: Shell for details.  

Example 4-5:   Configuring a SCSI Disk Drive with Asynchronous Data Transfer and No Tagged Command Queuing

In this example, a SCSI disk device is configured without support for synchronous data transfer and tagged command queuing. The scsiTargetOptionsSet( ) routine is used to turn off these features. The SCSI ID of this disk device is 2, and the LUN is 0:

int              which; 
SCSI_OPTIONS     option; 
int              devBusId; 
 
devBusId = 2; 
which = SCSI_SET_OPT_XFER_PARAMS | SCSI_SET_OPT_TAG_PARAMS; 
option.maxOffset = SCSI_SYNC_XFER_ASYNC_OFFSET;  
                                            /* => 0 defined in scsi2Lib.h */ 
option.minPeriod = SCSI_SYNC_XFER_MIN_PERIOD;  /* defined in scsi2Lib.h */ 
option.tagType = SCSI_TAG_UNTAGGED;      /* defined in scsi2Lib.h */ 
option.maxTag = SCSI_MAX_TAGS; 
 
if (scsiTargetOptionsSet (pSysScsiCtrl, devBusId, &option, which) == ERROR) 
    { 
    SCSI_DEBUG_MSG ("usrScsiConfig: could not set options\n", 0, 0, 0, 0,  
        0, 0); 
    return (ERROR); 
    } 
 
/* configure SCSI disk drive at busId = devBusId, LUN = 0 */ 
 
if ((pSpd20 = scsiPhysDevCreate (pSysScsiCtrl, devBusId, 0, 0, NONE, 0, 0,  
        0)) == (SCSI_PHYS_DEV *) NULL) 
    { 
    SCSI_DEBUG_MSG ("usrScsiConfig: scsiPhysDevCreate failed.\n"); 
    return (ERROR); 
    }

Example 4-6:   Working with Tape Devices

SCSI tape devices can be controlled using common commands from scsiCommonLib and sequential commands from scsiSeqLib. These commands use a pointer to a SCSI sequential device structure, SEQ_DEV, defined in seqIo.h. For more information on controlling SCSI tape devices, see the reference entries for these libraries.

This example configures a SCSI tape device whose bus ID is 5 and whose LUN is 0. It includes commands to create a physical device pointer, set up a sequential device, and initialize a tapeFs device.

/* configure Exabyte 8mm tape drive at busId = 5, LUN = 0 */ 
if ((pSpd50 = scsiPhysDevCreate (pSysScsiCtrl, 5, 0, 0, NONE, 0, 0, 0)) 
        == (SCSI_PHYS_DEV *) NULL) 
    { 
    SCSI_DEBUG_MSG ("usrScsiConfig: scsiPhysDevCreate failed.\n"); 
    return (ERROR); 
    } 
 
/* configure the sequential device for this physical device */ 
if ((pSd0 = scsiSeqDevCreate (pSpd50)) == (SEQ_DEV *) NULL) 
    { 
    SCSI_DEBUG_MSG ("usrScsiConfig: scsiSeqDevCreate failed.\n"); 
        return (ERROR); 
    } 
 
/* setup the tape device configuration */ 
pTapeConfig = (TAPE_CONFIG *) calloc (sizeof (TAPE_CONFIG), 1); 
pTapeConfig->rewind = TRUE;   /* this is a rewind device */ 
pTapeConfig->blkSize = 512;   /* uses 512 byte fixed blocks */ 
 
/* initialize a tapeFs device */ 
if (tapeFsDevInit ("/tape1", pSd0, pTapeConfig) == NULL) 
    { 
    return (ERROR); 
    } 
 
/* rewind the physical device using scsiSeqLib interface directly*/ 
if (scsiRewind (pSd0) == ERROR) 
    { 
    return (ERROR); 
    }

Example 4-7:   Configuring a SCSI Disk for Synchronous Data Transfer with Non-Default Offset and Period Values

In this example, a SCSI disk drive is configured with support for synchronous data transfer. The offset and period values are user-defined and differ from the driver default values. The chosen period is 25, defined in SCSI units of 4 ns. Thus, the period is actually 4 * 25 = 100 ns. The synchronous offset is chosen to be 2. Note that you may need to adjust the values depending on your hardware environment.

int                   which; 
SCSI_OPTIONS          option; 
int                   devBusId; 
 
devBusId = 2; 
 
    which = SCSI_SET_IPT_XFER_PARAMS; 
    option.maxOffset = 2; 
    option.minPeriod = 25; 
 
    if (scsiTargetOptionsSet (pSysScsiCtrl, devBusId &option, which) == 
        ERROR) 
        { 
        SCSI_DEBUG_MSG ("usrScsiConfig: could not set options\n",  
                         0, 0, 0, 0, 0, 0) 
        return (ERROR); 
        } 
 
    /* configure SCSI disk drive at busId = devBusId, LUN = 0 */ 
 
    if ((pSpd20 = scsiPhysDevCreate (pSysScsiCtrl, devBusId, 0, 0, NONE, 
                                     0, 0, 0)) == (SCSI_PHYS_DEV *) NULL) 
        { 
        SCSI_DEBUG_MSG ("usrScsiConfig: scsiPhysDevCreate failed.\n") 
        return (ERROR); 
        }

Example 4-8:   Changing the Bus ID of the SCSI Controller

To change the bus ID of the SCSI controller, modify sysScsiInit( ) in sysScsi.c. Set the SCSI bus ID to a value between 0 and 7 in the call to xxxCtrlInitScsi2( ), where xxx is the controller name. The default bus ID for the SCSI controller is 7.

Troubleshooting
  • Incompatibilities Between SCSI-1 and SCSI-2

Applications written for SCSI-1 may not execute for SCSI-2 because data structures in scsi2Lib.h, such as SCSI_TRANSACTION and SCSI_PHYS_DEV, have changed. This applies only if the application used these structures directly.

If this is the case, you can choose to configure only the SCSI-1 level of support, or you can modify your application according to the data structures in scsi2Lib.h. In order to set new fields in the modified structure, some applications may simply need to be recompiled, and some applications will have to be modified and then recompiled.

  • SCSI Bus Failure

If your SCSI bus hangs, it could be for a variety of reasons. Some of the more common are:

  • Your cable has a defect. This is the most common cause of failure.

  • The cable exceeds the cumulative maximum length of 6 meters specified in the SCSI-2 standard, thus changing the electrical characteristics of the SCSI signals.

  • The bus is not terminated correctly. Consider providing termination power at both ends of the cable, as defined in the SCSI-2 ANSI specification.

  • The minimum transfer period is insufficient or the REQ/ACK offset is too great. Use scsiTargetOptionsSet( ) to set appropriate values for these options.

  • The driver is trying to negotiate wide data transfers on a device that does not support them. In rejecting wide transfers, the device-specific driver cannot handle this phase mismatch. Use scsiTargetOptionsSet( ) to set the appropriate value for the xferWidth field for that particular SCSI device.

4.7.8   Sockets

In VxWorks, the underlying basis of network communications is sockets. A socket is an endpoint for communication between tasks; data is sent from one socket to another. Sockets are not created or opened using the standard I/O functions. Instead, they are created by calling socket( ), and connected and accessed using other routines in sockLib. However, after a stream socket (using TCP) is created and connected, it can be accessed as a standard I/O device, using read( ), write( ), ioctl( ), and close( ). The value returned by socket( ) as the socket handle is in fact an I/O system file descriptor (fd).

VxWorks socket routines are source-compatible with the BSD 4.4 UNIX socket functions and the Windows Sockets (Winsock 1.1) networking standard. Use of these routines is discussed in VxWorks Network Programmer's Guide: Networking APIs.



4.8    Differences Between VxWorks and Host System I/O

Most commonplace uses of I/O in VxWorks are completely source-compatible with I/O in UNIX and Windows. However, note the following differences:

  • Device Configuration..  

In VxWorks, device drivers can be installed and removed dynamically.

  • File Descriptors..  

In UNIX and Windows, fds are unique to each process. In VxWorks, fds are global entities, accessible by any task, except for standard input, standard output, and standard error (0, 1, and 2), which can be task specific.

  • I/O Control..  

The specific parameters passed to ioctl( ) functions can differ between UNIX and VxWorks.

  • Driver Routines..  

In UNIX, device drivers execute in system mode and cannot be preempted. In VxWorks, driver routines can be preempted because they execute within the context of the task that invoked them.



4.9    Internal Structure

The VxWorks I/O system differs from most I/O systems in the way that the work of performing user I/O requests is distributed between the device-independent I/O system and the device drivers themselves.

In many systems, the device driver supplies a few routines to perform low-level I/O functions such as reading a sequence of bytes from, or writing them to, character-oriented devices. The higher-level protocols, such as communications protocols on character-oriented devices, are implemented in the device-independent part of the I/O system. The user requests are heavily processed by the I/O system before the driver routines get control.

While this approach is designed to make it easy to implement drivers and to ensure that devices behave as much alike as possible, it has several drawbacks. The driver writer is often seriously hampered in implementing alternative protocols that are not provided by the existing I/O system. In a real-time system, it is sometimes desirable to bypass the standard protocols altogether for certain devices where throughput is critical, or where the device does not fit the standard model.

In the VxWorks I/O system, minimal processing is done on user I/O requests before control is given to the device driver. The VxWorks I/O system acts as a switch to route user requests to appropriate driver-supplied routines. Each driver can then process the raw user requests as appropriate to its devices. In addition, however, several high-level subroutine libraries are available to driver writers that implement standard protocols for both character- and block-oriented devices. Thu,s the VxWorks I/O system provides the best of both worlds: while it is easy to write a standard driver for most devices with only a few pages of device-specific code, driver writers are free to execute the user requests in nonstandard ways where appropriate.

There are two fundamental types of device: block and character (or non-block; see Figure 4-8). Block devices are used for storing file systems. They are random access devices where data is transferred in blocks. Examples of block devices include hard and floppy disks. Character devices are any device that does not fall in the block category. Examples of character devices include serial and graphical input devices, for example, terminals and graphics tablets.

As discussed in earlier sections, the three main elements of the VxWorks I/O system are drivers, devices, and files. The following sections describe these elements in detail. The discussion focuses on character drivers; however, much of it is applicable for block devices. Because block drivers must interact with VxWorks file systems, they use a slightly different organization; see 4.9.4 Block Devices.


*      
NOTE: This discussion is designed to clarify the structure of VxWorks I/O facilities and to highlight some considerations relevant to writing I/O drivers for VxWorks. It is not a complete text on writing a device driver. For detailed information on this subject, see the VxWorks BSP Developer's Guide.

Example 4-9 shows the abbreviated code for a hypothetical driver that is used as an example throughout the following discussions. This example driver is typical of drivers for character-oriented devices.

In VxWorks, each driver has a short, unique abbreviation, such as net or tty, which is used as a prefix for each of its routines. The abbreviation for the example driver is xx.

Example 4-9:   Hypothetical Driver

/* 
 * xxDrv - driver initialization routine 
 * xxDrv() init's the driver. It installs the driver via iosDrvInstall. 
 * It may allocate data structures, connect ISRs, and initialize hardware*/ 
 
STATUS xxDrv () 
  { 
  xxDrvNum = iosDrvInstall (xxCreat, 0, xxOpen, 0, xxRead, xxWrite, xxIoctl); 
  (void) intConnect (intvec, xxInterrupt, ...); 
  ... 
  }
/************************************************************************ 
 * xxDevCreate - device creation routine 
 * 
 * Called to add a device called <name> to be svced by this driver. Other 
 * driver-dependent arguments may include buffer sizes, device addresses. 
 * The routine adds the device to the I/O system by calling iosDevAdd. 
 * It may also allocate and initialize data structures for the device, 
 * initialize semaphores, initialize device hardware, and so on. 
 */ 
 
STATUS xxDevCreate (name, ...) 
  char * name; 
  ... 
  { 
  status = iosDevAdd (xxDev, name, xxDrvNum); 
  ... 
  } 
 
/* 
 * 
 * The following routines implement the basic I/O functions.  
 * The xxOpen() return value is meaningful only to this driver, 
 * and is passed back as an argument to the other I/O routines. 
 */ 
 
int xxOpen (xxDev, remainder, mode) 
  XXDEV * xxDev; 
  char * remainder; 
  int mode; 
  { 
  /* serial devices should have no file name part */ 
  
  if (remainder[0] != 0) 
    return (ERROR); 
  else 
    return ((int) xxDev); 
  } 
 
int xxRead (xxDev, buffer, nBytes) 
  XXDEV * xxDev; 
  char * buffer; 
  int nBytes; 
  ... 
int xxWrite (xxDev, buffer, nBytes) 
  ... 
int xxIoctl (xxDev, requestCode, arg) 
  ...
/* 
 * xxInterrupt - interrupt service routine 
 * 
 * Most drivers have routines that handle interrupts from the devices 
 * serviced by the driver. These routines are connected to the interrupts 
 * by calling intConnect (usually in xxDrv above). They can receive a 
 * single argument, specified in the call to intConnect (see intLib). 
*/ 
 
VOID xxInterrupt (arg) 
  ...

4.9.1   Drivers

A driver for a non-block device implements the seven basic I/O functions--creat( ), delete( ), open( ), close( ), read( ), write( ), and ioctl( )--for a particular kind of device. In general, this type of driver has routines that implement each of these functions, although some of the routines can be omitted if the functions are not operative with that device.

Drivers can optionally allow tasks to wait for activity on multiple file descriptors. This is implemented using the driver's ioctl( ) routine; see Implementing select( ).

A driver for a block device interfaces with a file system, rather than directly with the I/O system. The file system in turn implements most I/O functions. The driver need only supply routines to read and write blocks, reset the device, perform I/O control, and check device status. Drivers for block devices have a number of special requirements that are discussed in 4.9.4 Block Devices.

When the user invokes one of the basic I/O functions, the I/O system routes the request to the appropriate routine of a specific driver, as detailed in the following sections. The driver's routine runs in the calling task's context, as though it were called directly from the application. Thus, the driver is free to use any facilities normally available to tasks, including I/O to other devices. This means that most drivers have to use some mechanism to provide mutual exclusion to critical regions of code. The usual mechanism is the semaphore facility provided in semLib.

In addition to the routines that implement the seven basic I/O functions, drivers also have three other routines:

  • An initialization routine that installs the driver in the I/O system, connects to any interrupts used by the devices serviced by the driver, and performs any necessary hardware initialization. This routine is typically named xxDrv( ).

  • A routine to add devices that are to be serviced by the driver to the I/O system. This routine is typically named xxDevCreate( ).

  • Interrupt-level routines that are connected to the interrupts of the devices serviced by the driver.

The Driver Table and Installing Drivers

The function of the I/O system is to route user I/O requests to the appropriate routine of the appropriate driver. The I/O system does this by maintaining a table that contains the address of each routine for each driver. Drivers are installed dynamically by calling the I/O system internal routine iosDrvInstall( ). The arguments to this routine are the addresses of the seven I/O routines for the new driver. The iosDrvInstall( ) routine enters these addresses in a free slot in the driver table and returns the index of this slot. This index is known as the driver number and is used subsequently to associate particular devices with the driver.

Null (0) addresses can be specified for some of the seven routines. This indicates that the driver does not process those functions. For non-file-system drivers, close( ) and delete( ) often do nothing as far as the driver is concerned.

VxWorks file systems (such as dosFsLib) contain their own entries in the driver table, which are created when the file system library is initialized.

Figure 4-2:   Example - Driver Initialization for Non-Block Devices

Example of Installing a Driver

Figure 4-2 shows the actions taken by the example driver and by the I/O system when the initialization routine xxDrv( ) runs.

The driver calls iosDrvInstall( ), specifying the addresses of the driver's routines for the seven basic I/O functions. Then, the I/O system:

  1. Locates the next available slot in the driver table, in this case slot 2.
  1. Enters the addresses of the driver routines in the driver table.
  1. Returns the slot number as the driver number of the newly installed driver.

4.9.2   Devices

Some drivers are capable of servicing many instances of a particular kind of device. For example, a single driver for a serial communications device can often handle many separate channels that differ only in a few parameters, such as device address.

In the VxWorks I/O system, devices are defined by a data structure called a device header (DEV_HDR). This data structure contains the device name string and the driver number for the driver that services this device. The device headers for all the devices in the system are kept in a memory-resident linked list called the device list. The device header is the initial part of a larger structure determined by the individual drivers. This larger structure, called a device descriptor, contains additional device-specific data such as device addresses, buffers, and semaphores.

The Device List and Adding Devices

Non-block devices are added to the I/O system dynamically by calling the internal I/O routine iosDevAdd( ). The arguments to iosDevAdd( ) are the address of the device descriptor for the new device, the device's name, and the driver number of the driver that services the device. The device descriptor specified by the driver can contain any necessary device-dependent information, as long as it begins with a device header. The driver does not need to fill in the device header, only the device-dependent information. The iosDevAdd( ) routine enters the specified device name and the driver number in the device header and adds it to the system device list.

To add a block device to the I/O system, call the device initialization routine for the file system required on that device (dosFsDevCreate( ) or rawFsDevInit( )). The device initialization routine then calls iosDevAdd( ) automatically.

The routine iosDevFind( ) can be used to locate the device structure (by obtaining a pointer to the DEV_HDR, which is the first member of that structure) and to verify that a device name exists in the table. Following is an example using iosDevFind( ):

char *  pTail;                          /* pointer to tail of devName */ 
char devName[6] = "DEV1:";              /* name of device */ 
DOS_VOLUME_DESC *  pDosVolDesc;         /* first member is DEV_HDR */ 
    ... 
    pDosVolDesc = iosDevFind(devName, (char**)&pTail); 
    if (NULL == pDosVolDesc) 
        { 
        /* ERROR: device name does not exist and no default device */ 
        } 
    else 
        { 
        /*  
         * pDosVolDesc is a valid DEV_HDR pointer  
         * and pTail points to beginning of devName.  
         * Check devName against pTail to determine if it is 
         * the default name or the specified devName.  
         */ 
        }

Example of Adding Devices

In Figure 4-3, the example driver's device creation routine xxDevCreate( ) adds devices to the I/O system by calling iosDevAdd( ).

Figure 4-3:   Example - Addition of Devices to I/O System

4.9.3   File Descriptors

Several fds can be open to a single device at one time. A device driver can maintain additional information associated with an fd beyond the I/O system's device information. In particular, devices on which multiple files can be open at one time have file-specific information (for example, file offset) associated with each fd. You can also have several fds open to a non-block device, such as a tty; typically there is no additional information, and thus writing on any of the fds produces identical results.

The Fd Table

Files are opened with open( ) (or creat( )). The I/O system searches the device list for a device name that matches the filename (or an initial substring) specified by the caller. If a match is found, the I/O system uses the driver number contained in the corresponding device header to locate and call the driver's open routine in the driver table.

The I/O system must establish an association between the file descriptor used by the caller in subsequent I/O calls, and the driver that services it. Additionally, the driver must associate some data structure per descriptor. In the case of non-block devices, this is usually the device descriptor that was located by the I/O system.

The I/O system maintains these associations in a table called the fd table. This table contains the driver number and an additional driver-determined 4-byte value. The driver value is the internal descriptor returned by the driver's open routine, and can be any nonnegative value the driver requires to identify the file. In subsequent calls to the driver's other I/O functions (read( ), write( ), ioctl( ), and close( )), this value is supplied to the driver in place of the fd in the application-level I/O call.

Example of Opening a File

In Figure 4-4 and Figure 4-5, a user calls open( ) to open the file /xx0. The I/O system takes the following series of actions:

  1. It searches the device list for a device name that matches the specified filename (or an initial substring). In this case, a complete device name matches.
  1. It reserves a slot in the fd table and creates a new file descriptor object, which is used if the open is successful.
  1. It then looks up the address of the driver's open routine, xxOpen( ), and calls that routine. Note that the arguments to xxOpen( ) are transformed by the I/O system from the user's original arguments to open( ). The first argument to xxOpen( ) is a pointer to the device descriptor the I/O system located in the full filename search. The next parameter is the remainder of the filename specified by the user, after removing the initial substring that matched the device name. In this case, because the device name matched the entire filename, the remainder passed to the driver is a null string. The driver is free to interpret this remainder in any way it wants. In the case of block devices, this remainder is the name of a file on the device. In the case of non-block devices like this one, it is usually an error for the remainder to be anything but the null string. The third parameter is the file access flag, in this case O_RDONLY; that is, the file is opened for reading only. The last parameter is the mode, as passed to the original open( ) routine.
  1. It executes xxOpen( ), which returns a value that subsequently identifies the newly opened file. In this case, the value is the pointer to the device descriptor. This value is supplied to the driver in subsequent I/O calls that refer to the file being opened. Note that if the driver returns only the device descriptor, the driver cannot distinguish multiple files opened to the same device. In the case of non-block device drivers, this is usually appropriate.
  1. The I/O system then enters the driver number and the value returned by xxOpen( ) in the new file descriptor object. .

Figure 4-4:   Example: Call to I/O Routine open( ) [Part 1]

Again, the value entered in the fd object has meaning only for the driver, and is arbitrary as far as the I/O system is concerned.  

  1. Finally, it returns to the user the index of the slot in the fd table, in this case 3.  

Figure 4-5:   Example: Call to I/O Routine open( ) [Part 2]

  

Example of Reading Data from the File

In Figure 4-6, the user calls read( ) to obtain input data from the file. The specified fd is the index into the fd table for this file. The I/O system uses the driver number contained in the table to locate the driver's read routine, xxRead( ). The I/O system calls xxRead( ), passing it the identifying value in the fd table that was returned by the driver's open routine, xxOpen( ). Again, in this case the value is the pointer to the device descriptor. The driver's read routine then does whatever is necessary to read data from the device. The process for user calls to write( ) and ioctl( ) follow the same procedure. 

Figure 4-6:   Example: Call to I/O Routine read( )

Example of Closing a File

The user terminates the use of a file by calling close( ). As in the case of read( ), the I/O system uses the driver number contained in the fd table to locate the driver's close routine. In the example driver, no close routine is specified; thus no driver routines are called. Instead, the I/O system marks the slot in the fd table as being available. Any subsequent references to that fd cause an error. However, subsequent calls to open( ) can reuse that slot.

Implementing select( )

Supporting select( ) in your driver allows tasks to wait for input from multiple devices or to specify a maximum time to wait for the device to become ready for I/O. Writing a driver that supports select( ) is simple, because most of the functionality is provided in selectLib. You might want your driver to support select( ) if any of the following is appropriate for the device:

  • The tasks want to specify a timeout to wait for I/O from the device. For example, a task might want to time out on a UDP socket if the packet never arrives.

  • The driver supports multiple devices, and the tasks want to wait simultaneously for any number of them. For example, multiple pipes might be used for different data priorities.

  • The tasks want to wait for I/O from the device while also waiting for I/O from another device. For example, a server task might use both pipes and sockets.

To implement select( ), the driver must keep a list of tasks waiting for device activity. When the device becomes ready, the driver unblocks all the tasks waiting on the device.

For a device driver to support select( ), it must declare a SEL_WAKEUP_LIST structure (typically declared as part of the device descriptor structure) and initialize it by calling selWakeupListInit( ). This is done in the driver's xxDevCreate( ) routine. When a task calls select( ), selectLib calls the driver's ioctl( ) routine with the function FIOSELECT or FIOUNSELECT. If ioctl( ) is called with FIOSELECT, the driver must do the following:

  1. Add the SEL_WAKEUP_NODE (provided as the third argument of ioctl( )) to the SEL_WAKEUP_LIST by calling selNodeAdd( ).
  1. Use the routine selWakeupType( ) to check whether the task is waiting for data to read from the device (SELREAD) or if the device is ready to be written (SELWRITE).
  1. If the device is ready (for reading or writing as determined by selWakeupType( )), the driver calls the routine selWakeup( ) to make sure that the select( ) call in the task does not pend. This avoids the situation where the task is blocked but the device is ready.

If ioctl( ) is called with FIOUNSELECT, the driver calls selNodeDelete( ) to remove the provided SEL_WAKEUP_NODE from the wakeup list.

When the device becomes available, selWakeupAll( ) is used to unblock all the tasks waiting on this device. Although this typically occurs in the driver's ISR, it can also occur elsewhere. For example, a pipe driver might call selWakeupAll( ) from its xxRead( ) routine to unblock all the tasks waiting to write, now that there is room in the pipe to store the data. Similarly the pipe's xxWrite( ) routine might call selWakeupAll( ) to unblock all the tasks waiting to read, now that there is data in the pipe.

Example 4-10:   Driver Code Using the Select Facility

/* This code fragment shows how a driver might support select(). In this  
 * example, the driver unblocks tasks waiting for the device to become ready  
 * in its interrupt service routine. 
 */ 
 
/* myDrvLib.h - header file for driver */ 
 
typedef struct     /* MY_DEV */ 
    { 
    DEV_HDR     devHdr;                 /* device header */ 
    BOOL       myDrvDataAvailable;      /* data is available to read */ 
    BOOL       myDrvRdyForWriting;      /* device is ready to write */ 
    SEL_WAKEUP_LIST selWakeupList;      /* list of tasks pended in select */ 
    } MY_DEV;

/* myDrv.c - code fragments for supporting select() in a driver */ 
 
#include "vxWorks.h" 
#include "selectLib.h" 
 
/* First create and initialize the device */ 
 
STATUS myDrvDevCreate 
    ( 
    char *  name,                      /* name of device to create */ 
    ) 
    { 
    MY_DEV * pMyDrvDev;                /* pointer to device descriptor*/ 
    ... additional driver code ... 
 
    /* allocate memory for MY_DEV */ 
    pMyDrvDev = (MY_DEV *) malloc (sizeof MY_DEV); 
    ... additional driver code ... 
 
    /* initialize MY_DEV */ 
    pMyDrvDev->myDrvDataAvailable=FALSE 
    pMyDrvDev->myDrvRdyForWriting=FALSE 
 
    /* initialize wakeup list */ 
    selWakeupListInit (&pMyDrvDev->selWakeupList); 
    ... additional driver code ... 
    } 
 
/* ioctl function to request reading or writing */ 
 
STATUS myDrvIoctl 
    ( 
    MY_DEV * pMyDrvDev,                /* pointer to device descriptor */ 
    int      request,                  /* ioctl function */ 
    int      arg                       /* where to send answer */ 
    ) 
    { 
    ... additional driver code ... 
 
    switch (request) 
        { 
        ... additional driver code ... 
 
        case FIOSELECT: 
 
            /* add node to wakeup list */ 
 
            selNodeAdd (&pMyDrvDev->selWakeupList, (SEL_WAKEUP_NODE *) arg); 
 
            if (selWakeupType ((SEL_WAKEUP_NODE *) arg) == SELREAD  
                && pMyDrvDev->myDrvDataAvailable) 
                { 
                /* data available, make sure task does not pend */ 
                selWakeup ((SEL_WAKEUP_NODE *) arg); 
                } 
            if (selWakeupType ((SEL_WAKEUP_NODE *) arg) == SELWRITE  
                && pMyDrvDev->myDrvRdyForWriting) 
                { 
                /* device ready for writing, make sure task does not pend */ 
                selWakeup ((SEL_WAKEUP_NODE *) arg); 
                } 
            break; 
 
        case FIOUNSELECT: 
 
            /* delete node from wakeup list */ 
            selNodeDelete (&pMyDrvDev->selWakeupList, (SEL_WAKEUP_NODE *) arg); 
            break; 
 
            ... additional driver code ... 
        } 
    } 
 
/* code that actually uses the select() function to read or write */ 
 
void myDrvIsr 
    ( 
    MY_DEV * pMyDrvDev; 
    ) 
    { 
    ... additional driver code ... 
 
    /* if there is data available to read, wake up all pending tasks */ 
 
    if (pMyDrvDev->myDrvDataAvailable) 
        selWakeupAll (&pMyDrvDev->selWakeupList, SELREAD); 
 
    /* if the device is ready to write, wake up all pending tasks */ 
 
    if (pMyDrvDev->myDrvRdyForWriting) 
        selWakeupAll (&pMyDrvDev->selWakeupList, SELWRITE); 
    }

Cache Coherency

Drivers written for boards with caches must guarantee cache coherency. Cache coherency means data in the cache must be in sync, or coherent, with data in RAM. The data cache and RAM can get out of sync any time there is asynchronous access to RAM (for example, DMA device access or VMEbus access). Data caches are used to increase performance by reducing the number of memory accesses. Figure 4-7 shows the relationships between the CPU, data cache, RAM, and a DMA device.

Data caches can operate in one of two modes: writethrough and copyback. Write-through mode writes data to both the cache and RAM; this guarantees cache coherency on output but not input. Copyback mode writes the data only to the cache; this makes cache coherency an issue for both input and output of data.

Figure 4-7:   Cache Coherency

If a CPU writes data to RAM that is destined for a DMA device, the data can first be written to the data cache. When the DMA device transfers the data from RAM, there is no guarantee that the data in RAM was updated with the data in the cache. Thus, the data output to the device may not be the most recent--the new data may still be sitting in the cache. This data incoherence can be solved by making sure the data cache is flushed to RAM before the data is transferred to the DMA device.

If a CPU reads data from RAM that originated from a DMA device, the data read can be from the cache buffer (if the cache buffer for this data is not marked invalid) and not the data just transferred from the device to RAM. The solution to this data incoherence is to make sure that the cache buffer is marked invalid so that the data is read from RAM and not from the cache.

Drivers can solve the cache coherency problem either by allocating cache-safe buffers (buffers that are marked non-cacheable) or flushing and invalidating cache entries any time the data is written to or read from the device. Allocating cache-safe buffers is useful for static buffers; however, this typically requires MMU support. Non-cacheable buffers that are allocated and freed frequently (dynamic buffers) can result in large amounts of memory being marked non-cacheable. An alternative to using non-cacheable buffers is to flush and invalidate cache entries manually; this allows dynamic buffers to be kept coherent.

The routines cacheFlush( ) and cacheInvalidate( ) are used to manually flush and invalidate cache buffers. Before a device reads the data, flush the data from the cache to RAM using cacheFlush( ) to ensure the device reads current data. After the device has written the data into RAM, invalidate the cache entry with cacheInvalidate( ). This guarantees that when the data is read by the CPU, the cache is updated with the new data in RAM.

Example 4-11:   DMA Transfer Routine

/* This a sample DMA transfer routine. Before programming the device  
 * to output the data to the device, it flushes the cache by calling  
 * cacheFlush(). On a read, after the device has transferred the data, 
 * the cache entry must be invalidated using cacheInvalidate(). 
 */ 
 
#include "vxWorks.h" 
#include "cacheLib.h" 
#include "fcntl.h" 
#include "example.h" 
void exampleDmaTransfer   /* 1 = READ, 0 = WRITE */ 
    ( 
    UINT8 *pExampleBuf, 
    int exampleBufLen, 
    int xferDirection 
    ) 
    { 
    if (xferDirection == 1) 
        { 
        myDevToBuf (pExampleBuf); 
        cacheInvalidate (DATA_CACHE, pExampleBuf, exampleBufLen); 
        } 
 
    else 
        { 
        cacheFlush (DATA_CACHE, pExampleBuf, exampleBufLen); 
        myBufToDev (pExampleBuf); 
        } 
    }

It is possible to make a driver more efficient by combining cache-safe buffer allocation and cache-entry flushing or invalidation. The idea is to flush or invalidate a cache entry only when absolutely necessary. To address issues of cache coherency for static buffers, use cacheDmaMalloc( ). This routine initializes a CACHE_FUNCS structure (defined in cacheLib.h) to point to flush and invalidate routines that can be used to keep the cache coherent.

The macros CACHE_DMA_FLUSH and CACHE_DMA_INVALIDATE use this structure to optimize the calling of the flush and invalidate routines. If the corresponding function pointer in the CACHE_FUNCS structure is NULL, no unnecessary flush/invalidate routines are called because it is assumed that the buffer is cache coherent (hence it is not necessary to flush/invalidate the cache entry manually).

The driver code uses a virtual address and the device uses a physical address. Whenever a device is given an address, it must be a physical address. Whenever the driver accesses the memory, it must use the virtual address.

The device driver should use CACHE_DMA_VIRT_TO_PHYS to translate a virtual address to a physical address before passing it to the device. It may also use CACHE_DMA_PHYS_TO_VIRT to translate a physical address to a virtual one, but this process is time-consuming and non-deterministic, and should be avoided whenever possible.

Example 4-12:   Address-Translation Driver

/* The following code is an example of a driver that performs address 
 * translations. It attempts to allocate a cache-safe buffer, fill it, and  
 * then write it out to the device. It uses CACHE_DMA_FLUSH to make sure  
 * the data is current. The driver then reads in new data and uses  
 * CACHE_DMA_INVALIDATE to guarantee cache coherency. 
 */ 
 
#include "vxWorks.h" 
#include "cacheLib.h" 
#include "myExample.h" 
STATUS myDmaExample (void) 
    { 
    void * pMyBuf; 
    void * pPhysAddr; 
 
    /* allocate cache safe buffers if possible */ 
    if ((pMyBuf = cacheDmaMalloc (MY_BUF_SIZE)) == NULL) 
    return (ERROR);  
 
     fill buffer with useful information  
 
    /* flush cache entry before data is written to device */ 
    CACHE_DMA_FLUSH (pMyBuf, MY_BUF_SIZE); 
 
    /* convert virtual address to physical */ 
    pPhysAddr = CACHE_DMA_VIRT_TO_PHYS (pMyBuf); 
 
    /* program device to read data from RAM */ 
    myBufToDev (pPhysAddr); 
     wait for DMA to complete  
     ready to read new data  
 
    /* program device to write data to RAM */ 
    myDevToBuf (pPhysAddr); 
     wait for transfer to complete  
 
    /* convert physical to virtual address */ 
    pMyBuf = CACHE_DMA_PHYS_TO_VIRT (pPhysAddr); 
 
    /* invalidate buffer */ 
    CACHE_DMA_INVALIDATE (pMyBuf, MY_BUF_SIZE); 
     use data  
 
    /* when done free memory */ 
    if (cacheDmaFree (pMyBuf) == ERROR) 
        return (ERROR); 
    return (OK); 
    }

4.9.4   Block Devices

General Implementation

In VxWorks, block devices have a slightly different interface than other I/O devices. Rather than interacting directly with the I/O system, block device drivers interact with a file system. The file system, in turn, interacts with the I/O system. Direct access block devices have been supported since SCSI-1 and are used compatibly with dosFs and rawFs. In addition, VxWorks supports SCSI-2 sequential devices, which are organized so individual blocks of data are read and written sequentially. When data blocks are written, they are added sequentially at the end of the written medium; that is, data blocks cannot be replaced in the middle of the medium. However, data blocks can be accessed individually for reading throughout the medium. This process of accessing data on a sequential medium differs from that of other block devices.

Figure 4-8 shows a layered model of I/O for both block and non-block (character) devices. This layered arrangement allows the same block device driver to be used with different file systems, and reduces the number of I/O functions that must be supported in the driver.

A device driver for a block device must provide a means for creating a logical block device structure, a BLK_DEV for direct access block devices or a SEQ_DEV for sequential block devices. The BLK_DEV/SEQ_DEV structure describes the device in a generic fashion, specifying only those common characteristics that must be known to a file system being used with the device. Fields within the structures specify various physical configuration variables for the device--for example, block size, or total number of blocks. Other fields in the structures specify routines within the device driver that are to be used for manipulating the device (reading blocks, writing blocks, doing I/O control functions, resetting the device, and checking device status). The BLK_DEV/SEQ_DEV structures also contain fields used by the driver to indicate certain conditions (for example, a disk change) to the file system.

When the driver creates the block device, the device has no name or file system associated with it. These are assigned during the device initialization routine for the chosen file system (for example, dosFsDevInit( ) or tapeFsDevInit( )).

The low-level device driver for a block device is not installed in the I/O system driver table, unlike non-block device drivers. Instead, each file system in the VxWorks system is installed in the driver table as a "driver." Each file system has only one entry in the table, even though several different low-level device drivers can have devices served by that file system.

Figure 4-8:   Non-Block Devices vs. Block Devices

After a device is initialized for use with a particular file system, all I/O operations for the device are routed through that file system. To perform specific device operations, the file system in turn calls the routines in the specified BLK_DEV or SEQ_DEV structure.

A driver for a block device must provide the interface between the device and VxWorks. There is a specific set of functions required by VxWorks; individual devices vary based on what additional functions must be provided. The user manual for the device being used, as well as any other drivers for the device, is invaluable in creating the VxWorks driver.

The following sections describe the components necessary to build low-level block device drivers that adhere to the standard interface for VxWorks file systems.

Low-Level Driver Initialization Routine

The driver normally requires a general initialization routine. This routine performs all operations that are done one time only, as opposed to operations that must be performed for each device served by the driver. As a general guideline, the operations in the initialization routine affect the whole device controller, while later operations affect only specific devices.

Common operations in block device driver initialization routines include:

  • initializing hardware
  • allocating and initializing data structures
  • creating semaphores
  • initializing interrupt vectors
  • enabling interrupts

The operations performed in the initialization routine are entirely specific to the device (controller) being used; VxWorks has no requirements for a driver initialization routine.

Unlike non-block device drivers, the driver initialization routine does not call iosDrvInstall( ) to install the driver in the I/O system driver table. Instead, the file system installs itself as a "driver" and routes calls to the actual driver using the routine addresses placed in the block device structure, BLK_DEV or SEQ_DEV (see Device Creation Routine).

Device Creation Routine

The driver must provide a routine to create (define) a logical disk or sequential device. A logical disk device may be only a portion of a larger physical device. If this is the case, the device driver must keep track of any block offset values or other means of identifying the physical area corresponding to the logical device. VxWorks file systems always use block numbers beginning with zero for the start of a device. A sequential access device can be either of variable block size or fixed block size. Most applications use devices of fixed block size.

The device creation routine generally allocates a device descriptor structure that the driver uses to manage the device. The first item in this device descriptor must be a VxWorks block device structure (BLK_DEV or SEQ_DEV). It must appear first because its address is passed by the file system during calls to the driver; having the BLK_DEV or SEQ_DEV as the first item permits also using this address to identify the device descriptor.

The device creation routine must initialize the fields within the BLK_DEV or SEQ_DEV structure. The BLK_DEV fields and their initialization values are shown in Table 4-15.

Table 4-15:   Fields in the BLK_DEV Structure   


Field
Value

bd_blkRd
Address of the driver routine that reads blocks from the device.
bd_blkWrt
Address of the driver routine that writes blocks to the device.
bd_ioctl
Address of the driver routine that performs device I/O control.
bd_reset
Address of the driver routine that resets the device (NULL if none).
bd_statusChk
Address of the driver routine that checks disk status (NULL if none).
bd_removable
Value specifying whether or not the device is removable. TRUE if the device is removable (for example, a floppy disk); otherwise FALSE.
bd_nBlocks
Total number of blocks on the device.
bd_bytesPerBlk
Number of bytes per block on the device.
bd_blksPerTrack
Number of blocks per track on the device.
bd_nHeads
Number of heads (surfaces).
bd_retry
Number of times to retry failed reads or writes.
bd_mode
Device mode (write-protect status); generally set to O_RDWR.
bd_readyChanged
Value specifying whether or not the device ready status has changed. TRUE if the status has changed; initialize to TRUE to cause the disk to be mounted.

The SEQ_DEV fields and their initialization values are shown in Table 4-16

Table 4-16:   Fields in the SEQ_DEV Structure   


Field
Value

sd_seqRd
Address of the driver routine that reads blocks from the device.
sd_seqWrt
Address of the driver routine that writes blocks to the device.
sd_ioctl
Address of the driver routine that performs device I/O control.
sd_seqWrtFileMarks
Address of the driver routine that writes file marks to the device.
sd_rewind
Address of the driver routine that rewinds the sequential device.
sd_reserve
Address of the driver routine that reserves a sequential device.
sd_release
Address of the driver routine that releases a sequential device.
sd_readBlkLim
Address of the driver routine that reads the data block limits from the sequential device.
sd_load
Address of the driver routine that either loads or unloads a sequential device.
sd_space
Address of the driver routine that moves (spaces) the medium forward or backward to end-of-file or end-of-record markers.
sd_erase
Address of the driver routine that erases a sequential device.
sd_reset
Address of the driver routine that resets the device (NULL if none).
sd_statusChk
Address of the driver routine that checks sequential device status (NULL if none).
sd_blkSize
Block size of sequential blocks for the device. A block size of 0 means that variable block sizes are used.
sd_mode
Device mode (write protect status).
sd_readyChanged
Value for specifying whether or not the device ready status has changed. TRUE if the status has changed; initialize to TRUE to cause the sequential device to be mounted.
sd_maxVarBlockLimit
Maximum block size for a variable block.
sd_density
Density of sequential access media.

The device creation routine returns the address of the BLK_DEV or SEQ_DEV structure. This address is then passed during the file system device initialization call to identify the device.

Unlike non-block device drivers, the device creation routine for a block device does not call iosDevAdd( ) to install the device in the I/O system device table. Instead, this is done by the file system's device initialization routine.

Read Routine (Direct-Access Devices)

The driver must supply a routine to read one or more blocks from the device. For a direct access device, the read-blocks routine must have the following arguments and result:

STATUS xxBlkRd 
    ( 
    DEVICE *  pDev,       /* pointer to device descriptor */ 
    int       startBlk,   /* starting block to read */ 
    int       numBlks,    /* number of blocks to read */ 
    char *    pBuf        /* pointer to buffer to receive data */ 
    )


*      
NOTE: In this and following examples, the routine names begin with xx. These names are for illustration only, and do not have to be used by your device driver. VxWorks references the routines by address only; the name can be anything.

pDev
a pointer to the driver's device descriptor structure, represented here by the symbolic name DEVICE. (Actually, the file system passes the address of the corresponding BLK_DEV structure; these are equivalent, because the BLK_DEV is the first item in the device descriptor.) This identifies the device.

startBlk
the starting block number to be read from the device. The file system always uses block numbers beginning with zero for the start of the device. Any offset value used for this logical device must be added in by the driver.

numBlks
the number of blocks to be read. If the underlying device hardware does not support multiple-block reads, the driver routine must do the necessary looping to emulate this ability.

pBuf
the address where data read from the disk is to be copied.

The read routine returns OK if the transfer is successful, or ERROR if a problem occurs.

Read Routine (Sequential Devices)

The driver must supply a routine to read a specified number of bytes from the device. The bytes being read are always assumed to be read from the current location of the read/write head on the media. The read routine must have the following arguments and result:

STATUS xxSeqRd 
    ( 
    DEVICE *  pDev,         /* pointer to device descriptor */ 
    int       numBytes,     /* number of bytes to read */ 
    char *    buffer,       /* pointer to buffer to receive data */ 
    BOOL      fixed         /* TRUE => fixed block size */ 
    )
pDev
a pointer to the driver's device descriptor structure, represented here by the symbolic name DEVICE. (Actually, the file system passes the address of the corresponding SEQ_DEV structure; these are equivalent, because the SEQ_DEV structure is the first item in the device descriptor.) This identifies the device.

numBytes
the number of bytes to be read.

buffer
the buffer into which numBytes of data are read.

fixed
specifies whether the read routine reads fixed-size blocks from the sequential device or variable-sized blocks, as specified by the file system. If fixed is TRUE, fixed-size blocks are used.

The read routine returns OK if the transfer is completed successfully, or ERROR if a problem occurs.

Write Routine (Direct-Access Devices)

The driver must supply a routine to write one or more blocks to the device. The definition of this routine closely parallels that of the read routine. For direct-access devices, the write routine is as follows:

STATUS xxBlkWrt 
    ( 
    DEVICE *  pDev,     /* pointer to device descriptor */ 
    int       startBlk, /* starting block for write */ 
    int       numBlks,  /* number of blocks to write */ 
    char *    pBuf      /* ptr to buffer of data to write */ 
    )
pDev
a pointer to the driver's device descriptor structure.

startBlk
the starting block number to be written to the device.

numBlks
the number of blocks to be written. If the underlying device hardware does not support multiple-block writes, the driver routine must do the necessary looping to emulate this ability.

pBuf
the address of the data to be written to the disk.

The write routine returns OK if the transfer is successful, or ERROR if a problem occurs.

Write Routine (Sequential Devices)

The driver must supply a routine to write a specified number of bytes to the device. The bytes being written are always assumed to be written to the current location of the read/write head on the media. For sequential devices, the write routine is as follows:

STATUS xxWrtTape 
    ( 
    DEVICE *  pDev,       /* ptr to SCSI sequential device info */ 
    int       numBytes,   /* total bytes or blocks to be written */ 
    char *    buffer,     /* ptr to input data buffer      */ 
    BOOL      fixed       /* TRUE => fixed block size */ 
    )
pDev
a pointer to the driver's device descriptor structure.

numBytes
the number of bytes to be written.

buffer
the buffer from which numBytes of data are written.

fixed
specifies whether the write routine reads fixed-size blocks from the sequential device or variable-sized blocks, as specified by the file system. If fixed is TRUE, fixed-size blocks are used.

The write routine returns OK if the transfer is successful, or ERROR if a problem occurs.

I/O Control Routine

The driver must provide a routine that can handle I/O control requests. In VxWorks, most I/O operations beyond basic file handling are implemented through ioctl( ) functions. The majority of these are handled directly by the file system. However, if the file system does not recognize a request, that request is passed to the driver's I/O control routine.

Define the driver's I/O control routine as follows:

STATUS xxIoctl 
    ( 
    DEVICE *  pDev,       /* pointer to device descriptor */ 
    int        funcCode,  /* ioctl() function code */ 
    int        arg        /* function-specific argument */ 
    )
pDev
a pointer to the driver's device descriptor structure.

funcCode
the requested ioctl( ) function. Standard VxWorks I/O control functions are defined in the include file ioLib.h. Other user-defined function code values can be used as required by your device driver. The I/O control functions supported by dosFs, rawFs, and tapeFs are summarized in 5. Local File Systems in this manual.

arg
specific to the particular ioctl( ) function requested. Not all ioctl( ) functions use this argument.

The driver's I/O control routine typically takes the form of a multi-way switch statement, based on the function code. The driver's I/O control routine must supply a default case for function code requests it does not recognize. For such requests, the I/O control routine sets errno to S_ioLib_UNKNOWN_REQUEST and returns ERROR.

The driver's I/O control routine returns OK if it handled the request successfully; otherwise, it returns ERROR.

Device-Reset Routine

The driver usually supplies a routine to reset a specific device, but it is not required. This routine is called when a VxWorks file system first mounts a disk or tape, and again during retry operations when a read or write fails.

Declare the driver's device-reset routine as follows:

STATUS xxReset 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

When called, this routine resets the device and controller. Do not reset other devices, if it can be avoided. The routine returns OK if the driver succeeded in resetting the device; otherwise, it returns ERROR.

If no reset operation is required for the device, this routine can be omitted. In this case, the device-creation routine sets the xx_reset field in the BLK_DEV or SEQ_DEV structure to NULL.


*      
NOTE: In this and following examples, the names of fields in the BLK_DEV and SEQ_DEV structures are parallel except for the initial letters bd_ or sd_. In these cases, the initial letters are represented by xx_, as in the xx_reset field to represent both the bd_reset field and the sd_reset field.

Status-Check Routine

If the driver provides a routine to check device status or perform other preliminary operations, the file system calls this routine at the beginning of each open( ) or creat( ) on the device.

Define the status-check routine as follows:

STATUS xxStatusChk 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

The routine returns OK if the open or create operation can continue. If it detects a problem with the device, it sets errno to some value indicating the problem, and returns ERROR. If ERROR is returned, the file system does not continue the operation.

A primary use of the status-check routine is to check for a disk change on devices that do not detect the change until after a new disk is inserted. If the routine determines that a new disk is present, it sets the bd_readyChanged field in the BLK_DEV structure to TRUE and returns OK so that the open or create operation can continue. The new disk is then mounted automatically by the file system. (See Change in Ready Status.)

Similarly, the status check routine can be used to check for a tape change. This routine determines whether a new tape has been inserted. If a new tape is present, the routine sets the sd_readyChanged field in the SEQ_DEV structure to TRUE and returns OK so that the open or create operation can continue. The device driver should not be able to unload a tape, nor should a tape be physically ejected, while a file descriptor is open on the tape device.

If the device driver requires no status-check routine, the device-creation routine sets the xx_statusChk field in the BLK_DEV or SEQ_DEV structure to NULL.

Write-Protected Media

The device driver may detect that the disk or tape in place is write-protected. If this is the case, the driver sets the xx_mode field in the BLK_DEV or SEQ_DEV structure to O_RDONLY. This can be done at any time (even after the device is initialized for use with the file system). The file system respects the xx_mode field setting and does not allow writes to the device until the xx_mode field is changed to O_RDWR or O_WRONLY.

Change in Ready Status

The driver informs the file system whenever a change in the device's ready status is recognized. This can be the changing of a floppy disk, changing of the tape medium, or any other situation that makes it advisable for the file system to remount the disk.

To announce a change in ready status, the driver sets the xx_readyChanged field in the BLK_DEV or SEQ_DEV structure to TRUE. This is recognized by the file system, which remounts the disk during the next I/O initiated on the disk. The file system then sets the xx_readyChanged field to FALSE. The xx_readyChanged field is never cleared by the device driver.

Setting xx_readyChanged to TRUE has the same effect as calling the file system's ready-change routine (for example, calling ioctl( ) with the FIODISKCHANGE function code).

An optional status-check routine (see Status-Check Routine) can provide a convenient mechanism for asserting a ready-change, particularly for devices that cannot detect a disk change until after the new disk is inserted. If the status-check routine detects that a new disk is present, it sets xx_readyChanged to TRUE. This routine is called by the file system at the beginning of each open or create operation.

Write-File-Marks Routine (Sequential Devices)

The sequential driver must provide a routine that can write file marks onto the tape device. The write file marks routine must have the following arguments:

STATUS xxWrtFileMarks 
    ( 
    DEVICE *  pDev,         /* pointer to device descriptor */ 
    int       numMarks,     /* number of file marks to write */ 
    BOOL      shortMark     /* short or long file marks */ 
    )
pDev
a pointer to the driver's device descriptor structure.

numMarks
the number of file marks to be written sequentially.

shortMark
the type of file mark (short or long). If shortMark is TRUE, short marks are written.

The write file marks routine returns OK if the file marks are written correctly on the tape device; otherwise, it returns ERROR.

Rewind Routine (Sequential Devices)

The sequential driver must provide a rewind routine in order to rewind tapes in the tape device. The rewind routine is defined as follows:

STATUS xxRewind 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

When called, this routine rewinds the tape in the tape device. The routine returns OK if completion is successful; otherwise, it returns ERROR.

Reserve Routine (Sequential Devices)

The sequential driver can provide a reserve routine that reserves the physical tape device for exclusive access by the host that is executing the reserve routine. The tape device remains reserved until it is released by that host, using a release routine, or by some external stimulus.

The reserve routine is defined as follows:

STATUS xxReserve 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

If a tape device is reserved successfully, the reserve routine returns OK. However, if the tape device cannot be reserved or an error occurs, it returns ERROR.

Release Routine (Sequential Devices)

This routine releases the exclusive access that a host has on a tape device. The tape device is then free to be reserved again by the same host or some other host. This routine is the opposite of the reserve routine and must be provided by the driver if the reserve routine is provided.

The release routine is defined as follows:

STATUS xxReset 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

If the tape device is released successfully, this routine returns OK. However, if the tape device cannot be released or an error occurs, this routine returns ERROR.

Read-Block-Limits Routine (Sequential Devices)

The read-block-limits routine can poll a tape device for its physical block limits. These block limits are then passed back to the file system so the file system can decide the range of block sizes to be provided to a user.

The read-block-limits routine is defined as follows:

STATUS xxReadBlkLim 
    ( 
    DEVICE *  pDev,            /* pointer to device descriptor */ 
    int      *maxBlkLimit,     /* maximum block size for device */ 
    int      *minBlkLimit      /* minimum block size for device */ 
    )
pDev
a pointer to the driver's device descriptor structure.

maxBlkLimit
returns the maximum block size that the tape device can handle to the calling tape file system.

minBlkLimit
returns the minimum block size that the tape device can handle.

The routine returns OK if no error occurred while acquiring the block limits; otherwise, it returns ERROR.

Load/Unload Routine (Sequential Devices)

The sequential device driver must provide a load or unload routine in order to mount or unmount tape volumes from a physical tape device. Loading means that a volume is being mounted by the file system. This is usually done with an open( ) or a creat( ) call. However, a device should be unloaded or unmounted only when the file system wants to eject the tape volume from the tape device.

The load/unload routine is defined as follows:

STATUS xxLoad 
    ( 
    DEVICE *  pDev,  /* pointer to device descriptor */ 
    BOOL      load   /* load or unload device */ 
    )
pDev
a pointer to the driver's device descriptor structure.

load
a boolean variable that determines if the tape is loaded or unloaded. If load is TRUE, the tape is loaded. If load is FALSE, the tape is unloaded.

The load/unload routine returns OK if the load or unload operation ends successfully; otherwise, it returns ERROR.

Space Routine (Sequential Devices)

The sequential device driver must provide a space routine that moves, or spaces, the tape medium forward or backward. The amount of distance that the tape spaces depends on the kind of search that must be performed. In general, tapes can be searched by end-of-record marks, end-of-file marks, or other types of device-specific markers.

The basic definition of the space routine is as follows; however, other arguments can be added to the definition:

STATUS xxSpace 
    ( 
    DEVICE *  pDev,       /* pointer to device descriptor */ 
    int       count,      /* number of spaces */ 
    int       spaceCode   /* type of space */ 
    )
pDev
a pointer to the driver's device descriptor structure.

count
specifies the direction of search. A positive count value represents forward movement of the tape device from its current location (forward space); a negative count value represents a reverse movement (back space).

spaceCode
defines the type of space mark that the tape device searches for on the tape medium. The basic types of space marks are end-of-record and end-of-file. However, different tape devices may support more sophisticated kinds of space marks designed for more efficient maneuvering of the medium by the tape device.

If the device is able to space in the specified direction by the specified count and space code, the routine returns OK; if these conditions cannot be met, it returns ERROR.

Erase Routine (Sequential Devices)

The sequential driver must provide a routine that allows a tape to be erased. The erase routine is defined as follows:

STATUS xxErase 
    ( 
    DEVICE *  pDev  /* pointer to driver's device descriptor structure */ 
    )

The routine returns OK if the tape is erased; otherwise, it returns ERROR.

4.9.5   Driver Support Libraries

The subroutine libraries in Table 4-17 may assist in the writing of device drivers. Using these libraries, drivers for most devices that follow standard protocols can be written with only a few pages of device-dependent code. See the reference entry for each library for details.

Table 4-17:   VxWorks Driver Support Routines


Library
Description

errnoLib
Error status library
ftpLib
ARPA File Transfer Protocol library
ioLib
I/O interface library
iosLib
I/O system library
intLib
Interrupt support subroutine library
remLib
Remote command library
rngLib
Ring buffer subroutine library
ttyDrv
Terminal driver
wdLib
Watchdog timer subroutine library



4.10    PCMCIA

A PCMCIA card can be plugged into notebook computers to connect devices such as modems and external hard drives.2 VxWorks provides PCMCIA facilities for pcPentium, pcPentium2, pcPentium3 and BSPs and PCMCIA drivers that allow VxWorks running on these targets to support PCMCIA hardware.

PCMCIA support is at the PCMCIA Release 2.1 level. It does not include socket services or card services, which are not required by VxWorks. It does include chip drivers and libraries. The PCMCIA libraries and drivers are also available in source code form for VxWorks systems based on CPU architectures other than Intel Pentium.

To include PCMCIA support in your system, add the INCLUDE_PCMCIA component to the VxWorks kernel protection domain. For information about PCMCIA facilities, see the entries for pcmciaLib and pcmciaShow in the VxWorks API Reference.



4.11    Peripheral Component Interconnect: PCI

Peripheral Component Interconnect (PCI) is a bus standard for connecting peripherals to a PC, and is used in Pentium systems, among others. PCI includes buffers that de-couple the CPU from relatively slow peripherals, allowing them to operate asynchronously.

For information about PCI facilities, see the entries for pciAutoConfigLib, pciConfigLib, pciInitLib, and pciConfigShow in the VxWorks API Reference.


1:  It will not be possible to restart VxWorks if un-handled external interrupts occur during the boot countdown.

2:  PCMCIA stands for Personal Computer Memory Card International Association, and refers to both the association and the standards that it has developed.