![]() |
|
||||
Writing Device Drivers for LynxOS |
Porting UNIX Drivers to LynxOS
This appendix discusses the similarities and differences between device drivers under LynxOS and UNIX. It is intended to serve two main purposes:
- To provide some guidelines to developers wishing to port existing UNIX drivers to LynxOS in order to reuse existing code.
- To provide a pedagogical stepping stone for developers who are already experienced with UNIX drivers.
The material that follows describes a feature of a UNIX device driver and points out the corresponding feature in a LynxOS device driver. This appendix supplements the more detailed coverage of LynxOS device drivers that can be found in previous chapters of this manual. Certain LynxOS features not used in a UNIX driver are, consequently, mentioned very briefly only. The versions of the UNIX kernel referred to in this chapter are, for the most part, SVR3.2 and SVR4.
Kernel and Driver Design Goals
A frequently asked question is whether it would be possible to achieve source-level or even binary compatibility between UNIX and LynxOS drivers. While this--with some effort--might be technically feasible, the result would probably not be acceptable for designers of real-time systems.
This is because the fundamental differences in the design goals of LynxOS as compared to the UNIX kernel. The latter was designed for multi-user time-sharing systems, while LynxOS was designed specifically for hard real-time systems. These differences in design goals influence the choice of kernel data structures and algorithms, including those used in device drivers.
The differences are also seen in the services provided by the kernel to device drivers. The LynxOS kernel provides many services that meet specific requirements of real-time systems. These features would not be found in a UNIX driver. On the contrary, a UNIX driver may use some services that would result in a detrimental effect on a real-time performance.
Another significant difference is preemptability. The UNIX kernel was originally written to be uninterruptible, though some UNIX kernels now exist that are preemptive to some extent. The LynxOS kernel, including device drivers, is fully preemptive. This has a major influence on the way a driver is written.
Different design goals can also be noted at the level of the drivers themselves. UNIX drivers are generally designed to make the most efficient use of I/O devices, thereby maximizing throughput. This goal leads to the use of specific driver techniques such as the chaining of I/O requests, processing of interrupts within an interrupt handler, and the starting of the next I/O operation from within the interrupt handler. In contrast, a LynxOS driver must be designed to have a minimal impact on real-time performance, respecting the relative priorities of the tasks that are using the devices. The way in which interrupts are handled is probably the largest difference between a UNIX and a LynxOS driver.
Given these differences, both at the kernel and driver level, it is clear that in order to respect real-time demands, a port is preferred in providing compatibility.
Porting Strategy
Porting a UNIX driver can be broadly divided into three stages as follows:
The first stage allows the developer to reach a point where a working LynxOS driver can be tested for functionality. While enabling the re-use of a driver in a relatively short time, this initial port does not take advantage of the real-time aspects of LynxOS, and the driver could have a detrimental effect on the system response time. In order for the driver to conform to the real-time characteristics of LynxOS, the implementation of Stage Two is absolutely necessary. The features in Stage Three are optional but may be advantageous in certain situations.
Driver Structure
Overall Structure
A LynxOS and a UNIX driver are quite similar in overall structure. Each consists of a number of entry points, including an initialization routine and an interrupt handler. A LynxOS driver has, in addition, one or more kernel threads.
LynxOS Driver UNIX Driver Initialization Initialization Entry points Entry points Interrupt handler Interrupt handler Kernel threads
Global Variables
A UNIX driver typically makes widespread use of global variables, which is the most common way for the driver entry points to share information. A LynxOS driver can and should be written without the use of any global variables. The LynxOS kernel provides an elegant means to communicate driver state between entry points. Use of this mechanism is essential to allow dynamic install and uninstall of a driver.
Major and Minor Device Numbers
There is an important difference in the way UNIX and LynxOS use major device numbers. Under UNIX, the major device number is used to distinguish between different drivers. The minor number distinguishes between different devices controlled by the same driver. Under LynxOS, each driver has a unique driver ID, though this number is never used by the driver code. Different devices controlled by the same driver are identified by different major numbers (as opposed to the minor number in UNIX). The use of the minor device number is defined entirely by the driver. LynxOS driver IDs and major numbers are allocated automatically during a kernel build.
Driver Interface with Kernel
The interface between the UNIX kernel and a driver is defined by the driver service calls, the init entry point, and the interrupt handler.
Driver Service Calls
The services provided by a kernel to device drivers can be grouped into several functional classes:
Memory Management
This section describes the functions used for allocating memory and for translating memory addresses.
Memory Allocation
Functions used for the allocation of memory for the driver's internal use are as follows:
LynxOS UNIX sysbrk, sysfree, get1page, free1page, alloc_cmem, free_cmem kmem_alloc, kmem_freeThe functions sysbrk and sysfree are the nearest equivalent to UNIX kmem_alloc and kmem_free. The UNIX function kmem_alloc can sleep while waiting for free space. The LynxOS functions never sleep, instead, they return SYSERR if the memory request cannot be satisfied immediately.
Address Translation
The functions required for converting virtual to physical addresses are as follows:
LynxOS UNIX User virtual to physical Kernel virtual to physicalNote that mmchain returns a kernel virtual address. To convert this to a physical address, the constant PHYSBASE must be subtracted.
Synchronization
In non-preemptive UNIX kernels, synchronization is a fairly straightforward matter. But in a fully preemptive kernel such as LynxOS it is much more complex. This can represent a significant portion of the porting effort. For more information, see "Synchronization"
DMA Transfers and Raw I/O
Setting up DMA transfers requires the following kernel services:
- Memory locking
- Split transfer into physically contiguous pieces
- Virtual to physical address translation
The following code fragments illustrate typical SVR4 driver code for performing a DMA transfer to user space.
UNIX
The key functions in the previous code fragment are:
LynxOS
The key functions in the previous code fragment are:
Faults in and locks pages. Converts virtual address range to list of kernel virtual addresses. These are converted to physical addresses using PHYSBASE Unlocks memory pages.Note that whereas UNIX uses the block interface (strategy entry point) for raw I/O, LynxOS uses the character interface read and write entry points.
Block Input/Output
strategy Entry Point
Both UNIX and LynxOS block drivers have a strategy entry point that is called by the kernel's block buffering I/O subsystem to perform transfers to block devices.
LynxOS UNIXAs with other entry points, the LynxOS strategy routine is passed the address of the device's statics structure as the first argument.
buf Structure
This data structure defines the buffers that are used to hold the data blocks from a block device. In LynxOS, this structure is of type struct buf_entry. The correspondence between the fields is shown below.
LynxOS struct buf_entry UNIX struct bufThe symbolic constants used to specify bits in the b_flags field are shown below.
LynxOS UNIX
Block I/O Support Routines
UNIX provides a number of support routines for block device drivers.
Suspend, waiting for I/O completion Wakeup process and release buffer Put buffer back on free listThe following code fragment shows how these routines are typically used in the strategy entry point and interrupt handler of a UNIX driver.
UNIX
LynxOS does not provide the biowait or biodone routines, but the code to implement the required functionality is straightforward, as shown below.
LynxOS
Driver Debugging
UNIX
Print message on system console (uses polling) Print message on user terminal (uses driver)
LynxOS
Print message on debug console (uses polling) Print message on system console (uses driver)
Initialization Routine
Although both UNIX and LynxOS drivers have an initialization routine, the way in which they are used differs in some important ways. By convention, the UNIX routine is called xxxinit, in LynxOS xxxinstall.
UNIX
- Initialization is called once during bootup.
- Initializes all hardware and software.
- Device-specific information is kept in statically allocated structures.
- Maximum number of supported devices is hardcoded.
- Limited number of configuration parameters
LynxOS
- Initialization routine is called for every major device.
- Device structure allocated dynamically.
- Number of supported devices not limited.
- User-defined configuration parameters
Probing for Devices
One of the tasks usually performed by the initialization routine is to test for the presence of a device. UNIX drivers must handle bus errors. In LynxOS this is handled automatically. Typical UNIX and LynxOS code is illustrated below:
UNIX init Routine
LynxOS install Routine
Interrupt Handling
In SystemV, the details of a device's interrupt capabilities are defined statically in configuration files external to the driver. The name of the interrupt handler is xxx_intr, where xxx_ is the specified driver prefix.
Because LynxOS supports dynamic driver installation and deinstallation, attaching and detaching an interrupt handler is done within the driver code using the functions iointset() and iointcl(). This is done in the install() and uninstall() entry points. The device's interrupt vector is normally passed to the install routine in the device information structure.
For x86
U Structure
Unlike most UNIX kernels, LynxOS does not have a U structure. The following paragraphs discuss the most commonly used members of this structure and how the equivalent functionality is implemented in a LynxOS driver.
u_base, u_count, u_offset
Older versions of UNIX used these fields to specify the details of a data transfer in the read/write entry points. The driver modifies these during the course of the transfer. The return value received by the application is the initial u_count value minus its final value. More recent implementations of UNIX have replaced them with a uio structure.
In a LynxOS driver, the user buffer address and size are passed directly as arguments to the driver entry point. An important difference from UNIX is that the value returned to the application is the value returned by the driver entry point. The seek position on the device is specified by the field position in the file structure. The driver is responsible for setting this at the end of a transfer.
u_fmode
This field holds the file mode flags. Its main use is in the read/write entry points to test for non-blocking I/O. It is also used to test, for example, if an application is trying to read from a device opened in write only mode.
In LynxOS, the file mode is held in the access_mode field of the file structure.
u_error
This field contains an error code, which is copied to the application's errno variable.
A LynxOS driver specifies an error code with the pseterr() function.
u_segflg
This field indicates whether a data transfer is to or from user or kernel space. It is necessary to know this because the user process and kernel have separate virtual address spaces.
In LynxOS, the user process and kernel exist within the same virtual space, so this functionality is not required.
u_procp
This field is a pointer used to process table entry for the current process. UNIX device drivers seldom need to access this field explicitly. In LynxOS, each process is identified by a unique job number which can be accessed in the driver top-half routines to provide similar functionality. The function getpid() can also be used to find the process ID number.
u_tsav, u_nofault
These are used for trapping bus errors, typically in the init() routine.
In the install routine of a LynxOS driver, bus errors are handled automatically. Elsewhere in a driver, the routines noreco() and recoset() must be used to catch bus errors.
Reentrance and Synchronization
Critical Code Regions
Accesses to shared data structures and hardware registers must be serialized. The synchronization mechanisms used in a UNIX driver depend very much on whether the driver is preemptive. SVR4 driver code is not preemptive, though synchronous preemption is possible if a driver calls sleep(). Drivers written for such kernels only need to synchronize with the interrupt level routines. This is done with the spln and splx functions. The LynxOS equivalent of these functions are disable() and restore() although there is an important difference. The LynxOS functions disable and restore all interrupts, but interrupt nesting is not possible.
Drivers under LynxOS are fully preemptive. Appropriate synchronization must be added to make the driver reentrant.
Event Synchronization
This type of synchronization involves waiting for an event (buffer free, transfer complete, data ready, and so on) to occur.
The UNIX sleep() function specifies a priority, which is assigned to the process when it wakes up. LynxOS uses fixed scheduling priorities. A task priority can only be changed on request from the user application. Both UNIX sleep and LynxOS swait() use an argument to specify how signals are handled during the time the task is blocked. It is difficult to find an exact correspondence in behavior in all cases.
UNIX sleep Priority
Another important difference is that wakeup() is stateless. It can only wake tasks that are blocked on the event at the time that wakeup() is called. On the other hand, ssignal() has a counter associated with it. This difference can have an influence on driver design. More care is needed with synchronization in the stateless case. Though this problem is normally solved by the fact that a UNIX driver is not preemptive.
Driver Interface with User Applications
The driver interface with the application covers the following topics:
Entry Points
There are a number of general remarks that can be made that apply to all entry points.
- In a LynxOS driver the first argument to all entry points is a pointer to the statics structure allocated by the install routine.
- LynxOS does not use the UNIX cred_t credentials structure.
- In LynxOS, the device number is passed only to the open entry point. Other entry points can access the device number and access the mode in the file structure.
Major and Minor Device Numbers
As discussed above, there is an important difference in the way UNIX and LynxOS use the device numbers. Typically, a UNIX driver uses (part of) the minor number to index into an array containing state variables for each device, as illustrated below.
UNIX
This code is unnecessary in the LynxOS driver because the address the controller's status block is passed as an argument to the entry points.
LynxOS
open/close
UNIX LynxOSAs shown in the listing above:
- LynxOS passes the device number to open, like SVR3. SVR4 passes a pointer to the device number.
- LynxOS does not have an equivalent of the otyp field.
- The LynxOS kernel only calls the close entry point on the last close of a device.
read/write
LynxOS UNIXThe UNIX uio structure specifies a list of user buffers. Earlier UNIX kernels used the clist data structure for character storage.
In a LynxOS driver, the user buffer is specified by buff and count. The entry point is called once for each buffer in scatter/gather I/O (readv/writev). LynxOS does not use the clist data structure.
The following code fragments compare typical write entry point logic used to transmit all user data. Note that in LynxOS, the driver is responsible for positioning the seek pointer (f->position). Another important difference is that the UNIX driver returns the number of bytes not transmitted.
LynxOS UNIX
ioctl
LynxOS UNIXIf arg is a pointer, the LynxOS driver must check the validity of the address with rbounds() and wbounds().
select
LynxOS UNIX
Accessing User Space
UNIX
The currently executing user process and the kernel may have separate virtual address spaces. In this case, kernel service routines are used to transfer data to and from user space. These routines usually handle invalid user addresses.
LynxOS
The current user process and the kernel exist in same virtual space. The kernel (including drivers) can access the whole of the virtual space. Therefore, drivers can transfer data to and from user space directly using a pointer.
The following code fragments illustrate how data might be transferred from user space in an ioctl entry point.
LynxOS UNIX
Returning Errors to User Application
UNIX
Earlier versions used the u_error field in the u structure. SVR4 uses the entry point return value.
LynxOS
Uses the pseterr function and return the value SYSERR.
The following code fragments illustrate how a driver returns the error EIO:
LynxOS UNIX SVR3 UNIX SVR4
LynxOS Kernel Threads
When using kernel threads, interrupt processing is performed by a preemptive, prioritized task. This is essential in order to maintain deterministic system response times. Using the UNIX interrupt architecture, where all interrupt processing is done in the interrupt handler itself, will lead to a degradation of the system's real-time performance.
Dynamic Installation
LynxOS supports the dynamic installation and deinstallation of drivers. This greatly facilitates the driver development and debugging phases as a kernel rebuild and reboot is not necessary each time the driver is modified. If the port has been done correctly, the only addition required to support dynamic installation is the declaration of the entry_points structure.
POSIX Programming Model
The LynxOS implementation of the POSIX.1 and POSIX.1b features permit much simpler driver design for supporting asynchronous I/O, non-blocking I/O, and synchronous I/O multiplexing and polling.
Asynchronous I/O
The complexity of handling asynchronous transfers is hidden from the application and driver developer. The POSIX API provides services to the application developer, and the driver sees only synchronous requests. Therefore, code to handle asynchronous transfers can be removed from a UNIX driver if the LynxOS version is only intended for use with POSIX conforming applications.
Synchronous I/O Multiplexing and Polling
This functionality is provided by the select system call at the application level and the select entry point in a driver. The POSIX standard does not define a select function. So, if the LynxOS driver is only intended for use with POSIX conforming applications, the select entry point can be removed.
![]() LynuxWorks, Inc. 855 Branham Lane East San Jose, CA 95138 http://www.lynuxworks.com 1.800.255.5969 |
![]() |
![]() |
![]() |
![]() |