![]() |
|
||||
Writing Device Drivers for LynxOS |
Device Driver Basics
This chapter covers the basic concepts of LynxOS device drivers. The topics covered are:
- Device driver overview,
- Components of a LynxOS device driver,
- Kernel support functions overview, and
- Summary of the development and installation process under LynxOS.
This chapter covers topics applicable to the development of device drivers under LynxOS and is not intended to be an introduction to device driver development. The reader is assumed to be familiar with the concepts of driver development within a kernel environment.
What is a Device Driver?
The device driver is a software interface between the OS and hardware that hides the implementation specifics of the hardware from the OS. It provides the mechanism for the kernel to communicate with a particular type of device. Typically, these communication requests are to transfer data to and from the device or to control the device in some manner. The device driver provides the predefined and consistent interface for the kernel to make these requests. The following figure diagrams the LynxOS device driver model.
LynxOS Device Driver Interface Model![]()
The device driver is linked into the kernel and interfaces directly with the controller card of a particular piece of hardware (drives, printers and modems, for example). An application can request access to devices using LynxOS I/O-related system calls such as open(), read(), or write().
The kernel invokes the appropriate routines within the device driver code to handle the I/O requests of an application. In addition, a device driver can also be invoked in response to file system operations, interrupts, timeouts or bus errors or a change in the process using the device.
Types of Device Drivers
Device drivers are classified as either block or character types. The primary difference between the two types is that block type device drivers use a fixed size data format when transferring data and character type drivers do not.
Block type device drivers are well suited for storage devices like disk and tape drives and off-board RAM or shared memory. Block device driver characteristics include:
- Data transfers in multiples of a fixed sized buffer (512 bytes for example),
- Kernel-buffered data transfer requests,
- Concept of position on a device, and
- File system support.
Character type drivers typically support data transfer devices such as serial and parallel ports, clocks and timers, network adapters, and A/D or D/A convertors. Character type device driver characteristics include:
- Operate on arbitrarily sized data structures,
- No kernel buffering
- May or may not have concept of position on device.
Most block type devices are supported by both a block and a character type device driver. The character type driver is a counterpart to the block driver code to support the character (also known as raw) data transfer capability of the block device (CDROM drive, for example).
Device Drivers and Devices
The terms device driver and driver are used synonymously throughout this document. Device and hardware are used interchangeably and refer to a physical device except within the context of installation. Within this context, device installation refers to the process of loading a device driver and creating a device node under LynxOS for access to the device's controller. This process is covered in "Installation and Debugging". Hardware installation is used to refer to the act of installing a physical device.
LynxOS Device Driver Components
A LynxOS device driver consists of code and supporting data structures. The device driver also has various installation attributes that categorize its existence within the LynxOS environment.
The device driver code is a program module that is incorporated into the kernel. It does not have a main() routine. A basic device driver is composed of a set of entry point functions, an interrupt service routine, and kernel threads. (Although not an absolute requirement by the device driver interface specification, the interrupt service routine and kernel threads are essential to sustaining the hard real-time requirements of LynxOS.) Additionally, a device driver may include a timeout handler, a bus error handler, and one or more shared resources such as semaphores, buffers, and queues (refer to "Other Components" for more information).
The supporting data structures for a device driver consists of:
These data structures are implemented as C struct data types. The device information and statics data structures are memory buffers used by the device driver routines. The dldd data structure is only used with dynamically installed device drivers and provide the mechanism for registering the entry point function names with the kernel.
The installation attributes of a device driver become known when the driver code and device are installed into LynxOS. These attributes characterize how the device driver is incorporated into the kernel (statically or dynamically) and how the hardware it supports is accessed by the kernel (driver ID, device ID, and device nodes). The driver ID, device ID, and device node name are specific to the actual driver installation. The device driver has no knowledge of these attributes. Driver IDs, device IDs, and device node names are covered later in the chapter in "Referencing Device Drivers".
A diagram of the LynxOS device driver is shown in the following figure.
lynxos Device Driver Components![]()
Note that the device information structure is external to the driver code. This data structure is instantiated independently of the device driver itself. The kernel passes the address of the device information structure to the driver when the device is installed. This data structure is described in the section "Device Information Data Structure" and the process of instantiating this structure is described in "Installation and Debugging"
Entry Point Functions
The entry point functions are the main access points into the device driver. The entry point function interface is predefined and the developer must supply the code that interacts with the hardware. This section provides a brief overview of the entry point functions. They are described in detail in "Entry Point Functions" The table below summarizes the entry point functions.
The install() and uninstall() functions provide a mechanism for initializing and removing a device driver. Well known I/O-related system calls map to the following entry point functions: open(), close(), read(), write(), ioctl(), and select() (see next figure). The strategy() entry point is used to provide block-oriented I/O scheduling of reads or writes to a block device. The mmap() entry point is used to support data mapped to memory.
Mapping to Entry Point Functions![]()
The set of entry points required for device drivers varies from device to device. The inherent characteristics, attributes, and purpose of the hardware determine which entry points are needed. However, for entry point routines not implemented, an empty routine, or one that simply returns the system defined constant, OK, should be provided.
Naming Convention
An identifier should be prepended to the standard entry point function names to uniquely identify them. The identifier chosen is entirely up to the developer, however, it is recommend that a convention be selected that identifies the intended device. For example, to name entry point functions for a device driver that supports the Xyz SCSI controller, XyzSCSI_install(), XyxSCSI_open(), XyzSCSI_read() and so on, can be used.
Data Structures
The data structures, device information and statics, are used in conjunction with the device driver code to provide a mechanism to pass device-specific information to the driver and to provide the driver routines with a shared memory area to place information about the state or status of the device and the driver. The dldd data structure is used to pass the entry point function names to the kernel for dynamically installed device drivers.
The device information and statics data structures are discussed in abstract terms because their structures are not predefined and their purposes vary. Concrete examples are provided to illustrate their usage. However, keep in mind that their field structure is determined by the particular device they are supporting and the developer is free to define them as appropriate.
Device Information Data Structure
The device information data structure is used to pass hardware-specific parameters to the device driver. These parameters (typically, configuration parameters) are essential to the proper functioning of the device driver code but would limit the driver's range of applicability if hardcoded. These parameters are typically unknown until the hardware is actually installed. They include items such as I/O address, IRQ level, and available resources.
Other uses of the device information structure include:
- Specifying specific operational characteristics of a controller card.
- Accessing special capabilities of the device.
- Accessing a specific port on a multifunction controller.
- Supporting multiple instances of a controller within the same system.
The device information data structure exists within the kernel address space. The kernel passes the address of this data structure to the install() entry point function. The device information data structure is deallocated when the device is uninstalled. An example of this data structure is shown below.
The developer is free to choose the structure name and make the field declarations appropriate to the hardware's attributes. An empty data structure is also allowable. The data structure definition is placed into a header file and is incorporated into the device driver module using the compiler #include directive.
With the values contained in the device information structure, the install() entry point can access and initialize the hardware. The process of initializing and instantiating the device information data structure, installing devices, and the mechanism used to pass the initialized structure to the driver is discussed in "Installation and Debugging"
Statics Data Structure
The statics data structure is a memory buffer commonly shared by the functions of the device driver. It is dynamically allocated and initialized by the install() entry point function. Its address is returned to the kernel by the install() entry point. The kernel passes the address of the statics structure to other entry point functions.
The developer is free to choose the structure name and make the field declarations appropriate to the requirements of the device driver. An empty data structure is also allowable. The statics data structure definition can be placed into a header file and incorporated into the device driver module using the compiler #include directive or placed within the driver code module. An example statics structure is shown below.
dldd Data Structure
The dldd data structure supports dynamic installation of device drivers. If used, the dldd structure is initialized within the driver code module. This data structure cannot exist in a device driver that is to be statically installed.
The names of the entry point functions are assigned to the fields of this structure and subsequently passed to the kernel when the device driver is installed. The dldd data structure is defined in <dldd.h>. The variable name associated with this data structure must be entry_points. For example:
dev_open, dev_close, dev_read, dev_write,
dev_select, dev_ioctl, dev_install,
dev_uninstall, dev_mmap
The dev_mmap field is used only by the mem and zero device drivers. All others can omit mmap.
For block-type drivers, dev_read is replaced with dev_strategy, and dev_write is replaced with NULL. The name of the data structure is block_entry_points. Any unused entry point function names can be replaced with NULL.
Handling Interrupts
Interrupts are external hardware signals delivered to the processor to indicate the occurrence of a specific event. Interrupts may signify:
- Completion of an operation
- Device has data available
- Device is ready for input or a command
- Device has changed status
Interrupt handlers are functions created by the developer to be part of the device driver code. They can be written to service the interrupt directly or can be used in conjunction with a kernel thread that can more effectively handle post interrupt processing in a scheduled and prioritized manner. Because interrupt handlers have the highest run priority, the minimization of the length of each is paramount. Interrupt handling is covered in more detail in "Interrupt and Timeout Handling"
Interrupts and Real-Time Response
In a normal system, interrupts have a higher priority than any task. A task, regardless of its priority, is interrupted if an interrupt is pending (unless the interrupts have been disabled). The result could mean that a low priority interrupt could interrupt a task that is executing with real-time constraints.
Using kernel threads, delays of this sort are significantly reduced. Instead of the interrupt service routine doing all the servicing of the interrupt, a kernel thread is used to perform the function previously performed by the interrupt routine.
Because the kernel thread is running at the application's priority (actually, at half a priority level higher), it is scheduled according to process priority and not hardware priority. This ensures that the interrupt service time is kept to a minimum and the task response time is kept short. The use of kernel threads for servicing interrupts is covered in detail in "Kernel Threads and Priority Tracking"
Kernel Threads
To off-load processing from interrupt-based sections of a device driver, LynxOS offers a feature known as kernel threads, also referred to as system threads. Kernel threads are defined as independently schedulable entities which reside in the kernel's virtual address space. They closely resemble processes but do not have the memory overhead associated with processes.
Although kernel threads have independent stack and register areas, the kernel threads share both text and data segments with the kernel. Each kernel thread has a priority associated with it, which is used by the operating system to schedule it. Kernel threads can be used to improve the interrupt and task response times considerably. Thus, they are often used in device drivers.
Priority tracking is the method used to dynamically determine the kernel thread's priority. The kernel thread assumes the same priority as the highest-priority application which it is currently servicing.
Kernel threads and priority tracking are covered in detail in "Kernel Threads and Priority Tracking"
Other Components
Other components of a device driver include:
These components may not be necessary in simple device drivers but may have a major role in the proper operation of more complex ones.
Shared Resources
Shared resources include memory objects such as semaphores, buffers, and queues. Semaphores are instrumental in the implementation of synchronization. They can be used as mutexes to protect critical code regions, as counters to manage shared resources, and as the gating object for event synchronization. (See "Synchronization") Buffers and queues are data objects shared by the functions of the device drivers. These data objects must be instantiated using special system calls that allocate the appropriate type of kernel memory ("Memory Management").
Timeout Handler
Timeouts are interrupts called by the clock interrupt handler. Timeouts can be used to generate interrupts at precise intervals of 10 millisecond granularity. LynxOS provides the system function timeout() to set up timeout handlers. Timeout handling is covered in "Interrupt and Timeout Handling"
Error Handler
An error handler is useful because it can change the default system behavior should a bus error occur. By default, LynxOS displays a message that a problem has occurred and attempts to halt the system. In most situations, system halts caused by bus errors can be avoided by implementing an error handler. More information on bus error handling can be found in "Handling Bus Errors".
LynxOS Kernel Support Functions
The kernel support functions available to a device driver fall into the following categories:
Memory management functions are available for the allocation and deallocation of memory objects that the device driver uses and for validating memory pointers passed to the device driver routines. Memory management functions are covered in detail in "Memory Management"
Synchronization mechanisms which include mutual exclusion, disabling interrupts and preemption, and shared resource management are covered in "Synchronization"
Exception handling is implemented using interrupt service routines, timeout handlers and bus error handlers. These topics are covered in "Interrupt and Timeout Handling" and "Installation and Debugging"
Device Driver Development and Installation
The development process consists of defining and coding the required entry point routines and supporting functions and defining the necessary device information and statics structures and shared data resources. The table "Summary of Device Driver Components" summarized the components to be considered in the development of a device driver. An example device driver is provided in Appendix C. Other examples can be found in /sys/devices.
Installation consists of choosing an installation method, dynamic or static, then performing the necessary steps, based on the method chosen, to incorporate the device driver into the LynxOS kernel. Device driver installation is covered in "Installation and Debugging"
Referencing Device Drivers
The LynxOS kernel maintains a set of tables to keep track of installed drivers and devices. Each device driver is assigned a driver ID and each installed device a device ID.
The kernel assigns driver IDs when the device code module is loaded. Each driver ID is unique. For block-type device drivers, the block driver and its character counterpart receive different driver IDs.
Device IDs are assigned when the device is installed. A device is installed when its device information block (See "Device Information Data Structure".) is loaded into the OS.
Major and Minor Device Designations
Each installed device has a major device and minor device component associated with it and each of these components has its own ID. The major and minor device IDs are also referred to as major and minor numbers.
The major device component is essentially the instantiation of the device information block for the device. The minor device component is used in a number of ways and one or more minor devices can exist for each major device. It is also possible for a device driver to support multiple major devices.
Major devices generally refer to a single controller card. Minor devices commonly refer to a single channel (sub-device) on a controller card but may also refer to different modes of dealing with the major device.
The major device correlates to the ID assigned to the device when it is installed by the LynxOS devinstall command or by the cdv_install() or bdv_install() system calls. These routines associate a major device to a specific driver and loads the device information block for the device. The following figure shows the relationship between drivers and major and minor devices.
Major and Minor Devices![]()
Some examples of minor device usage include:
Minor devices are defined within the device driver code and are initialized by way of the open() entry point function. The meaning of the minor device is interpreted only by the device driver code. The minor device number is assigned by the developer. The minor device number is only necessary if the major device supports multiple minor devices. For hardware in which a minor device is not applicable, zero is used as the minor device number.
Referencing Driver and Device IDs Under LynxOS
Drivers and devices in LynxOS can be referenced by used their identification numbers. The LynxOS commands drivers and devices are used to list installed device drivers and devices.
Drivers
Drivers are referenced using a unique driver identification number. This number is assigned automatically during kernel configuration. Drivers supporting raw (character) and block interfaces have separate driver identification numbers for each interface. The drivers command displays the drivers currently installed in the system and their unique driver identification numbers.
Following is a sample output of the drivers command.
Sample drivers Command Output
Devices
Each device is identified by a pair of major/minor numbers. LynxOS automatically assigns the major numbers during kernel generation. Character and block interfaces for the same device are indicated by different major numbers.
To view major devices installed on the system, use the devices command.
A sample output of the devices command is shown below. The id column of contains the major number of the device.
Sample devices Command OutputMinor devices are identified by the minor device number. These numbers may be used to indicate devices with different attributes. Minor device numbers are only necessary if there are multiple minor devices per major device. The meaning of the minor device number is selected and interpreted only by the device driver. The kernel does not attach a special meaning to the minor number. For example, different device drivers use the minor device number in different ways: device type, SCSI target ID (e.g., a SCSI disk controller driver), or a partition (e.g., an IDE disk controller driver).
Application Access to Devices and Drivers
Like UNIX, LynxOS is designed so that devices and drivers appear as special device files in the file system. Applications can access devices and drivers using the special device files. These files usually reside in the /dev directory (although they can be put anywhere) and are viewable, like other files, through the ls l command.
The device special files are named the same way as regular files and are identified by the device type (character (c) or block (b)) in the first character of the first column of the listing. Special device files have a file size of 0; however, they do occupy an inode and take up directory space for their name.
Below is a sample listing of the /dev directory using the ls -l command
(a heading as been added for clarity). The size column shows the major and minor numbers of the devices, respectively. Special device files are created with the mknod utility.
Sample /dev Directory Listing
Mapping Device Names to Driver Names
The following method can be used to map a device name to a driver:
- Use the ls l command on the /dev directory to obtain the listing of all the device names in the system. Determine the major and minor numbers associated with the device name. For example, in the example above, the device com1 would be a character device with a major device number of 6 and a minor device number of 0.
- Use the devices command to get a listing of all the devices in the system. The value in the id column corresponds to the major device number obtained above. If there is more than one entry with the same ID, the device type (character or block) eliminates any ambiguity. After locating the entry for the driver in question, look in the driver column for the driver ID. For example, in the sample drivers output above, com1 has a driver ID of 8.
- Use the drivers command to get a listing of all the drivers in the system. With the driver ID obtained in the above step, obtain the name of the driver. For com1, the driver name is serial, which is the driver with ID 8 as shown in the sample drivers listing above.
![]() LynuxWorks, Inc. 855 Branham Lane East San Jose, CA 95138 http://www.lynuxworks.com 1.800.255.5969 |
![]() |
![]() |
![]() |
![]() |