3.9   Internal Structure

The VxWorks I/O system is different from most in the way the work of performing user I/O requests is apportioned 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 inputting or outputting a sequence of bytes 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. Instead, 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. Thus the VxWorks I/O system gives you 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 3-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 3.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 Tornado BSP Developer's Kit User's Guide.

Example 3-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 3-9:  Hypothetical Driver

/************************************************************************* 
* xxDrv - driver initialization routine 
* 
* xxDrv() initializes 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 serviced 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) ...

3.9.1   Drivers

A driver for a non-block device implements the seven basic I/O functions--creat( ), remove( ), 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 3.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:

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 remove( ) often do nothing as far as the driver is concerned.

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

Example of Installing a Driver

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

[1] The driver calls iosDrvInstall( ), specifying the addresses of the driver's routines for the seven basic I/O functions.

The I/O system:

[2] Locates the next available slot in the driver table, in this case slot 2.

[3] Enters the addresses of the driver routines in the driver table.

[4] Returns the slot number as the driver number of the newly installed driver.

3.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 (dosFsDevInit( ), rt11FsDevInit( ), or rawFsDevInit( )). The device initialization routine then calls iosDevAdd( ) automatically.

Example of Adding Devices

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

3.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 file name (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 3-4 and Figure 3-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 file name (or an initial substring). In this case, a complete device name matches. 

[2] It reserves a slot in the fd table, which is used if the open is successful.

[3] 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 file name search. The next parameter is the remainder of the file name specified by the user, after removing the initial substring that matched the device name. In this case, because the device name matched the entire file name, 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 last parameter is the file access flag, in this case O_RDONLY; that is, the file is opened for reading only.

[4] 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.

[5] The I/O system then enters the driver number and the value returned by xxOpen( ) in the reserved slot in the fd table. Again, the value entered in the fd table has meaning only for the driver, and is arbitrary as far as the I/O system is concerned.

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

Example of Reading Data from the File

In Figure 3-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.

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:

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

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 incoherency 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 incoherency 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 3-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).

Some architectures allow the virtual address to be different from the physical address seen by the device; see 7.3 Virtual Memory Configuration in this manual. In this situation, 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 uses the virtual address. The driver translates the address using the following macros: CACHE_DMA_PHYS_TO_VIRT (to translate a physical address to a virtual one) and CACHE_DMA_VIRT_TO_PHYS (to translate a virtual address to a physical one).

Example 3-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); }

3.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, rt11Fs, 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 3-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( ), rt11FsDevInit( ) 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.

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:

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 3-14. The SEQ_DEV fields and their initialization values are shown in Table 3-15.  

Table 3-14:  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  
TRUE if the device is removable (for example, a floppy disk); FALSE otherwise. 
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  
TRUE if the device ready status has changed; initialize to TRUE to cause the disk to be mounted. 

  

Table 3-15:  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  
TRUE if the device ready 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-sized blocks from the sequential device or variable-sized blocks, as specified by the file system. If fixed is TRUE, then fixed sized 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-sized blocks from the sequential device or variable-sized blocks, as specified by the file system. If fixed is TRUE, then fixed sized 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 the dosFs, rt11Fs, rawFs, and tapeFs are summarized in 4. 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 
        )
pDev
a pointer to the 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 device descriptor */ 
        )
pDev
a pointer to the 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 you physically eject a tape, 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 checks this value and does not allow writes to the device until the xx_mode field is changed (to O_RDWR or O_WRONLY) or the file system's mode change routine (for example, dosFsModeChange( )) is called to change the mode. (The xx_mode field is changed automatically if the file system's mode change routine is used.)

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, dosFsReadyChange( )) or 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 device descriptor */ 
        )
pDev
a pointer to the 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 device descriptor */ 
        )
pDev
a pointer to the 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 device descriptor */ 
        )
pDev
a pointer to the 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/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 upon an open( ) or a creat( ). 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 device descriptor */ 
        )
pDev
a pointer to the driver's device descriptor structure.

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

3.9.5   Driver Support Libraries

The subroutine libraries in Table 3-16 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 3-16:  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