|
| 以 PDF 格式下載這本書
Overview of SunOS Device Drivers
3
- This chapter gives an overview of SunOS device drivers. It discusses what a device driver is and the types of device drivers that SunOS supports. It also provides a general discussion of the routines that device drivers must implement and points out compiler-related issues.
What is a Device Driver?
- A device driver is a kernel module containing subroutines and data responsible for managing low-level I/O operations for a particular hardware device. Device drivers can also be software-only, emulating a device such as a RAM disk or a pseudo-terminal that only exists in software. Such device drivers are called pseudo device drivers and cannot perform functions requiring hardware (such as DMA).
- A device driver contains all the device-specific code necessary to communicate with a device and provides a standard I/O interface to the rest of the system. This interface protects the kernel from device specifics just as the system call interface protects application programs from platform specifics. Application programs and the rest of the kernel need little (if any) device-specific code to address the device. In this way, device drivers make the system more portable and easier to maintain.
Types of Device Drivers
- There are several kinds of device drivers, each handling a different kind of I/O. Block device drivers manage devices with physically addressable storage media, such as disks. All other devices are considered character devices. There are two types of character device drivers: standard character device drivers and STREAMS device drivers.
Block Device Drivers
- Devices that support a file system are known as block devices. Drivers written for these devices are known as block device drivers. Block device drivers take a file system request (in the form of a buf(9S) structure) and make the device transfer the specified block. The main interface to the file system is the strategy(9E) routine. See Chapter 9, "Drivers for Block Devices" for more information.
- Block device drivers can also provide a character driver interface that allows utility programs to bypass the file system and access the device directly. This device access is commonly referred to as the raw interface to a block device.
Standard Character Device Drivers
- Character device drivers normally perform I/O in a byte stream. They can also provide additional interfaces not present in block drivers, such as I/O control (ioctl(9E)) commands, memory mapping, and device polling. See Chapter 8, "Drivers for Character Devices" for more information.
Byte-Stream I/O
- The main job of any device driver is to perform I/O, and many character device drivers do what is called bytestream or character I/O. The driver transfers data to and from the device without using a specific device address. This is in contrast to block device drivers, where part of the file system request identifies a specific location on the device.
- The read(9E) and write(9E) entry points handle bytestream I/O for standard character drivers. See "I/O Request Handling" on page 153 for more information.
I/O Control
- Many devices have characteristics and behaviors that can be configured or tuned. The ioctl(2) system call and the ioctl(9E) driver entry point provide a mechanism for application programs to change and determine the status of a driver's configurable characteristics. The baud rate of a serial communications port, for example, is usually configurable in this way.
- The I/O control interface is open ended, allowing device drivers to define special commands for the device. The definition of the commands is entirely up to the driver and is restricted only by the requirements of the application programs using the device and the device itself.
- Certain classes of devices such as frame buffers or disks must support standard sets of I/O control requests. These standard I/O control interfaces are documented in the Solaris 2.4 Reference Manual AnswerBook. For example, fbio(7) documents the I/O controls that frame buffers must support, and dkio(7) documents standard disk I/O controls. See "Miscellaneous I/O Control" on page 165 for more information on I/O control.
-
Note - The I/O control commands in section 7 are not part of the Solaris 2.x DDI/DKI.
Memory Mapping
- For certain devices, such as frame buffers, it is more efficient for application programs to have direct access to device memory. Applications can map device memory into their address spaces using the mmap(2) system call. To support memory mapping, device drivers implement segmap(9E) and mmap(9E) entry points. See Chapter 11, "Device Context Management" for details.
- Drivers that define an mmap(9E) entry point usually do not define read(9E) and write(9E) entry points, since application programs perform I/O directly to the devices after calling mmap(2). See Chapter 11, "Device Context Management", for more information on I/O control.
Device Polling
- The poll(2) system call allows application programs to monitor or poll a set of file descriptors for certain conditions or events. poll(2) is used to find out whether data are available to be read from the file descriptors or whether data may be written to the file descriptors without delay. Drivers referred to by these file descriptors must provide support for the poll(2) system call by implementing a chpoll(9E) entry point.
- Drivers for communication devices such as serial ports should support polling since they are used by applications that require synchronous notification of changes in read and write status. Many communications devices, however, are better implemented as STREAMS drivers.
STREAMS Drivers
- STREAMS is a separate programming model for writing a character device. Devices that receive data asynchronously (such as terminal and network devices) are suited to a STREAMS implementation. STREAMS device drivers must provide the loading and autoconfiguration support described in Chapter 5, "Autoconfiguration." See the STREAMS Programmer's Guide for additional information on how to write STREAMS drivers.
Device Issues
Accessing Device Registers
- There are two common ways of accessing device registers: through memory-mapping and I/O ports. The preferred method depends on the device; it is not generally software-configurable. For example, SBus and VMEbus devices do not provide I/O ports, but some ISA, MCA, and EISA devices may provide both access methods.
Memory-mapped Access
- In memory-mapped access, device registers appear in memory address space and are treated as normal memory. Just as the driver needs a kernel virtual address to access physical memory, the driver also needs a virtual address to
- access any device registers. To get a virtual address, the driver must map the device registers into kernel virtual memory. The Solaris 2.x DDI/DKI provides this ability with the ddi_map_regs(9F) function.
-
-
volatile char *reg_addr;
ddi_map_regs(..., (caddr_t) ®_addr, ...);
- Once the registers are successfully mapped, they can be accessed as any other memory. The following example writes one byte to the first location mapped (hexadecimal notation is usually used when writing bits):
-
-
*reg_addr = 0x10;
I/O Port Access
- In I/O port access, the device registers appear in I/O address space. Each addressable element of the I/O address space is called an I/O port. Device registers are accessed through I/O port numbers, which are defined by the hardware. These port numbers can refer to 8, 16, or 32-bit registers. Reading from a port is accomplished with one of the inb(9F) family of routines. Writing to a port is performed with an outb(9F) routine.
- On x86 systems, device registers are typically accessed through I/O ports. Large buffers, on the other hand, are accessed using memory mapping.
Example Device Registers
- Most of the examples in this manual use a fictitious device that has an 8-bit command/status register (csr), followed by an 8-bit data register. The command/status register is so called because writes to it go to an internal command register, and reads from it are directed to an internal status register.
- The command register looks like this:
-

- The status register looks like this:
-

- Many drivers provide macros for the various bits in their registers to make the code more readable. The examples in this manual use the following names for the bits in the command register:
-
-
#define ENABLE_INTERRUPTS 0x10
#define CLEAR_INTERRUPT 0x08
#define START_TRANSFER 0x04
- For the bits in the status register, the following macros are used:
-
-
#define INTERRUPTS_ENABLED 0x10
#define INTERRUPTING 0x08
#define DEVICE_BUSY 0x04
#define DEVICE_ERROR 0x02
#define TRANSFER_COMPLETE 0x01
Device Register Structure
- Using pointer accesses to communicate with the device results in unreadable code. For example, the code that reads the data register when a transfer has completed might look like this:
-
-
if (*reg_addr & TRANSFER_COMPLETE) {
data = *(reg_addr + 1); /* read data */
}
- To make the code more readable, it is common to define a structure that matches the layout of the devices registers. In this case, the structure could look like this:
-
-
struct device_reg {
volatile u_char csr;
volatile u_char data;
};
- The driver then maps the registers into memory and refers to them through a pointer to the structure:
-
-
struct device_reg *regp;
...
ddi_map_regs(..., (caddr_t) ®p, ... );
...
- The code that reads the data register upon a completed transfer now looks like this:
-
-
if (regp->csr & TRANSFER_COMPLETE)
data = regp->data;
Structure Padding
- A device that has a one-byte command/status register followed by a four-byte data register might lead to the following structure layout:
-
-
struct device_reg {
u_char csr;
u_int data;
};
- The above structure is not correct, because the compiler places padding between the two fields. For example, the SPARC processor requires each type to be on its natural boundary, which is byte-alignment for the csr field, but four-byte alignment for the data field. This results in three unused bytes between the two fields. Using this structure, the driver would be three bytes off when accessing the data register.
-
Finding Padding The ANSI C offsetof(3C) macro may be used in a test program to determine the offset of each element in the structure. Knowing the offset and the size of each element, the location and size of any padding can be determined.
-
Code Example 3-1 Structure padding
-
-
#include <sys/types.h>
#include <stdio.h>
#include <stddef.h>
struct device_reg {
u_char csr;
u_int data;
};
-
-
int main(void)
{
printf("The offset of csr is %d, its size is %d.\n",
offsetof(struct device_reg, csr), sizeof (u_char));
printf("The offset of data is %d, its size is %d.\n",
offsetof(struct device_reg, data), sizeof (u_int));
return (0);
}
- Here is a sample compilation with SPARCompilers 2.0.1 and a subsequent run of the program:
-
test% cc -Xa c.c
test% a.out
The offset of csr is 0, its size is 1.
The offset of data is 4, its size is 4.
|
- Driver developers should be aware that padding is dependent not only on the processor but also on the compiler.
Driver Interfaces
- The kernel expects device drivers to provide certain routines that must perform certain operations; these routines are called entry points. This is similar to the requirement that application programs have a _start( ) entry point or that C applications have the more familiar main( ) routine.
Entry Points
- Each device driver defines a standard set of functions called entry points, which are defined in the Solaris 2.4 Reference Manual AnswerBook. Drivers for different types of devices have different sets of entry points according to the kinds of operations the devices perform. A driver for a memory-mapped character-oriented device, for example, supports an mmap(9E) entry point, while a block driver does not.
- Some operations are common to all drivers, such as the functions that are required for module loading (_init(9E), _info(9E), and _fini(9E)), and the required autoconfiguration entry points identify(9E), attach(9E), and getinfo(9E)). Drivers may also support the optional autoconfiguration entry
- points for probe(E) and detach(9E). All device drivers must support the entry point getinfo(9E). Most drivers have open(9E) and close(9E) entry points to control access to their devices. See Chapter 8, "Drivers for Character Devices," Chapter 9, "Drivers for Block Devices," and Chapter 5, "Autoconfiguration," for details about these entry points.
- Traditionally, all driver function and variable names have some prefix added to them. Usually, this is the name of the driver, such as xxopen() for the open(9E) routine of driver xx. In subsequent examples, xx is used as the driver prefix.
-
Note - In SunOS 5.x, only the loadable module routines must be visible outside the driver object module. Everything else can have the storage class static.
Loadable Module Routines
-
-
int _init(void);
int _info(struct modinfo *modinfop);
int _fini(void);
- All drivers must implement the _init(9E), _fini(9E) and _info(9E) entry points to load, unload and report information about the driver module. The driver is single-threaded when the kernel calls _init. No other thread will enter a driver routine until mod_install(9F) returns success.
- Any resources global to the device driver should be allocated in _init(9E) before calling mod_install(9F) and should be released in _fini(9E) after calling mod_remove(9F).
- These routines have kernel context.
-
Note - Drivers must use these names, and they must not be declared static, unlike the other entry points where the names and storage classes are up to the driver.
Autoconfiguration Routines
-
-
static int xxidentify(dev_info_t *dip);
static int xxprobe(dev_info_t *dip);
static int xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd);
-
-
static int xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd);
static int xxgetinfo(dev_info_t *dip,ddi_info_cmd_t infocmd,
void *arg, void **result);
- The driver is single-threaded on a per-device basis when the kernel calls these routines, with the exception of getinfo(9E). The kernel may be in a multithreaded state when calling getinfo(9E), which can occur at any time. No calls to attach(9E) will occur on the same device concurrently. However, calls to attach(9E) on different devices that the driver handles may occur concurrently.
- Any per-device resources should be allocated in attach(9E) and released in detach(9E). No resources global to the driver should be allocated in attach(9E).
- These routines have kernel context.
Block Driver Entry Points
-
-
int xxopen(dev_t *devp, int flag, int otyp, cred_t *credp);
int xxclose(dev_t dev, int flag, int otyp, cred_t *credp);
int xxstrategy(struct buf *bp);
int xxprint(dev_t dev, char *str);
int xxdump(dev_t dev, caddr_t addr, daddr_t blkno, int nblk);
int xxprop_op(dev_t dev, dev_info_t *dip, ddi_prop_op_t prop_op,
int mod_flags, char *name, caddr_t valuep,
int *length);
- These routines have kernel context.
Character Driver Entry Points
-
-
int xxopen(dev_t *devp, int flag, int otyp, cred_t *credp);
int xxclose(dev_t dev, int flag, int otyp, cred_t *credp);
int xxread(dev_t dev, struct uio *uiop, cred_t *credp);
int xxwrite(dev_t dev, struct uio *uiop, cred_t *credp);
int xxioctl(dev_t dev, int cmd, int arg, int mode,
cred_t *credp, int *rvalp);
int xxmmap(dev_t dev, off_t off, int prot);
-
-
int xxsegmap(dev_t dev, off_t off, struct as *asp,
caddr_t *addrp, off_t len, unsigned int prot,
unsigned int maxprot, unsigned int flags,
cred_t *credp);
int xxchpoll(dev_t dev, short events, int anyyet,
short *reventsp, struct pollhead **phpp);
int xxprop_op(dev_t dev, dev_info_t *dip,
ddi_prop_op_t prop_op, int mod_flags,
char *name, caddr_t valuep, int *length);
- With the exception of prop_op(9E), all these routines have user context. prop_op(9E) has kernel context.
Callback functions
- Some routines provide a callback mechanism. This is a way to schedule a function to be called when a condition is met. Typical conditions for which callback functions are set up include:
-
- When a transfer has completed
- When a resource might become available
- When a timeout period has expired
- Transfer completion callbacks perform the tasks usually done in an interrupt service routine.
- In some sense, callback functions are similar to entry points. The functions that allow callbacks expect the callback function do to certain things. In the case of DMA routines, a callback function must return a value indicating whether the callback function wants to be rescheduled in case of a failure.
- Callback functions execute as a separate thread. They must consider all the usual multithreading issues.
-
Note - All scheduled callback functions must be canceled before a device is detached.
Interrupt Handling
- The Solaris 2.x DDI/DKI addresses these aspects of device interrupt handling:
-
- Registering device interrupts with the system
- Removing device interrupts from the system
- Interrupt information is contained in a property called interrupts (or intr on x86 platforms, see isa(4)), which is either provided by the PROM of a self-identifying device or in a hardware configuration file. See sbus(4), vme(4), and"Properties" on page 59 for more information.
- Since the internal implementation of interrupts is an architectural detail, special interrupt cookies are used to allow drivers to perform interrupt-related tasks. The types of cookies for interrupts are:
-
- Device interrupt cookies
- Block interrupt cookies
Device-Interrupt Cookies
- Defined as type ddi_idevice_cookie_t, this cookie is a data structure containing information used by a driver to program the interrupt-request level (or the equivalent) for a programmable device. See ddi_add_intr(9F) and "Registering Interrupts" on page 99 for more information.
Interrupt-Block Cookies
- Defined as type ddi_iblock_cookie_t this cookie is used by a driver to initialize the mutual exclusion locks it uses to protect data. This cookie should not be interpreted by the driver in any way.
Driver Context
- There are four contexts in which driver code executes:
-
- user
- kernel
- interrupt
- high-level interrupt
- The following sections point out the context in which driver code can execute. The driver context determines which kernel routines the driver is permitted to call. For example, in kernel context the driver must not call copyin(9F). The manual pages in section 9F document the allowable contexts for each function.
-
User Context A driver entry point has user context if it was directly invoked because of a user thread. The read(9E) entry point of the driver, invoked by a read(2) system call, has user context.
-
Kernel Context A driver function has kernel context if was invoked by some other part of the kernel. In a block device driver, the strategy(9E) entry point may be called by the pageout daemon to write pages to the device. Since the page daemon has no relation to the current user thread, strategy(9E) has kernel context in this case.
-
Interrupt Context Interrupt context is a more restrictive form of kernel context. Driver interrupt routines operate in interrupt context and have an interrupt level associated with them. See Chapter 6, "Interrupt Handlers" for more information.
-
High-level Interrupt Context High-level interrupt context is a more restricted form of interrupt context. If ddi_intr_hilevel(9F) indicates that an interrupt is high-level, driver interrupt routines added for that interrupt with ddi_add_intr(9F) run in high-level interrupt context. See "Handling High-Level Interrupts" on page 113 for more information.
Printing Messages
- Device drivers do not usually print messages. Instead, the entry points should return error codes so that the application can determine how to handle the error. If the driver really needs to print a message, it can use cmn_err(9F) to do so. This is similar to the C function printf(3S), but only prints to the console, to the message buffer displayed by dmesg(1M), or both.
-
-
void cmn_err(int level, char *format, ...);
-
format is similar to the printf(3S) format string, with the addition of the format %b which prints bit fields. level indicates what label will be printed: CE_NOTE NOTICE: format\n
-
CE_WARN WARNING:format\n CE_CONT format
-
CE_PANIC panic: format\n
-
CE_PANIC has the side-effect of crashing the system. This level should only be used if the system is in such an unstable state that to continue would cause more problems. It can also be used to get a system core dump when debugging.
- The first character of the format string is treated specially. See cmn_err(9F) for more detail.
Dynamic Memory Allocation
- Device drivers must be prepared to simultaneously handle all attached devices that they claim to drive. There should be no driver limit on the number of devices that the driver handles, and all per-device information must be dynamically allocated.
-
void *kmem_alloc(size_t size, int flag); The standard kernel memory allocation routine is kmem_alloc(9F). It is similar to the C library routine malloc(3C), with the addition of the flag argument. The flag argument can be either KM_SLEEP or KM_NOSLEEP, indicating whether the caller is willing to block if the requested size is not available. If KM_NOSLEEP is set, and memory is not available, kmem_alloc(9F) returns NULL.
-
kmem_zalloc(9F) is similar to kmem_alloc(9F), but also clears the contents of the allocated memory.
-
Note - Kernel memory is a limited resource, not pageable, and competes with user applications and the rest of the kernel for physical memory. Drivers that allocate a large amount of kernel memory may cause application performance to degrade.
-
void kmem_free(void *cp, size_t size); Memory allocated by kmem_alloc(9F) or by kmem_zalloc(9F) is returned to the system with kmem_free(9F). This is similar to the C library routine free(3C), with the addition of the size argument. Drivers must keep track of the size of each object they allocate in order to call kmem_free(9F) later.
Software State Management
State Structure
- For each device that the driver handles, the driver must keep some state information. At the minimum, this consists of a pointer to the dev_info node for the device (required by getinfo(9E)). The driver can define a structure that contains all the information needed about a single device:
-
-
struct xxstate {
dev_info_t *dip;
};
- This structure will grow as the device driver evolves. Additional useful fields might be:
-
- A pointer to each of the devices mapped registers
- Flags (such as busy)
- The initial state structure the examples in this book use is given in Code Example 3-2:
-
Code Example 3-2 Initial State Structure
-
-
struct xxstate {
dev_info_t *dip;
struct device_reg *regp;
};
- Subsequent chapters may require new fields. Each chapter will list any additions to the state structure.
State Management Routines
- To assist device driver writers in allocating state structures, the Solaris 2.x DDI/DKI provides a set of memory management routines called the software state routines (also known as the soft state routines). These routines dynamically allocate, retrieve, and destroy memory items of a specified size, and hide all the details of list management in a multithreaded kernel. An item number is used to identify the desired memory item; this can be (and usually is) the instance number assigned by the system.
- The driver must provide a state pointer, which is used by the soft state system to create the list of memory items:
-
-
static void *statep;
- Routines are provided to:
-
- Initialize the provided state pointer - ddi_soft_state_init(9F)
- Allocate space for a certain item - ddi_soft_state_zalloc(9F)
- Retrieve a pointer to the indicated item - ddi_get_soft_state(9F)
- Free the memory item - ddi_soft_state_free(9F)
- Finish using the state pointer - ddi_soft_state_fini(9F)
- When the module is loaded, the driver calls ddi_soft_state_init(9F) to initialize the driver state pointer, passing a hint indicating how many items to pre-allocate. If more items are needed, they will be allocated as necessary. The driver must call ddi_soft_state_fini(9F) when the driver is unloaded.
- To allocate an instance of the soft state structure, the driver calls ddi_soft_state_zalloc(9F), then ddi_get_soft_state(9F) to retrieve a pointer to the allocated structure. This is usually performed when the device is attached, and the inverse operation, ddi_soft_state_free(9F), is performed when the device is detached.
- Once the item is allocated, the driver only needs to call ddi_get_soft_state(9F) to retrieve the pointer.
- See "Loadable Driver Interface" on page 89 for an example use of these routines.
Properties
-
Properties define arbitrary characteristics of the device or device driver. Properties may be defined by the FCode of a self-identifying device, by a hardware configuration file (see driver.conf(4)), or by the driver itself using ddi_prop_create(9F).
- A property is a name-value pair. The name is a string that identifies the property, and the value is an array of bytes. Examples of properties are the height and width of a frame buffer, or the number of blocks in a partition of a block device. The value of a property may be one of three types:
-
- A boolean property, which has no length; it either exists or does not exist.
- An integer property, which has length four and has an integer value
- A long property, which has an arbitrary length, and whose value is a series of bytes
-
Note - Strictly speaking, ddi software property names are not restricted in any way; however, there are certain recommended uses. As defined in IEEE 1275- 1994, (the Standard for Boot Firmware) a property "is a human readable text string consisting of one to thirty-one printable characters. Property names shall not contain upper case characters or the characters "/", "\", ":", "[", "]" and "@". Property names beginning with the character "+" are reserved for use by future revisions of IEEE 1275-1994." By convention, underscores are not used in property names; use a hyphen (-) instead. Also by convention, property names ending with the question mark character ( auto-boot?) contain values that are strings, typically true or false.
- A driver can request a property from its parent, which in turn may ask its parent. The driver can control whether the request can go higher than its parent.
- For example, the "esp" driver maintains an integer-sized property called targetX-sync-speed. The prtconf(1M) command in its verbose mode displays driver properties. The following example shows a partial listing for the "esp" driver:
-
test% prtconf -v
...
esp, instance #0
Driver software properties:
name <target2-sync-speed> length <4>
value <0x00000fa0>.
...
|
- The property interface can be used to:
-
- Create a property with ddi_prop_create(9F). This usually is performed in attach(9E).
- Retrieve a property with ddi_prop_op(9F), or one of the following more specific routines:
· ddi_getproplen(9F) to retrieve the length of a property
· ddi_getprop(9F) for boolean and integer properties
· ddi_getlongprop(9F) and ddi_getlongprop_buf(9F) for other property sizes
-
ddi_prop_modify(9F) - Change the value of a property
-
ddi_prop_undefine(9F) - Explicitly undefine, but not remove, a property
-
ddi_prop_remove(9F) - Remove a property.
-
ddi_prop_remove_all(9F) - Remove all properties associated with a device.
prop_op( )
- To report the values of device properties to the system, drivers must fill in the entry point in the cb_ops(9S) structure with their own prop_op(9E) entry point or the ddi_prop_op(9F) routine. A prop_op(9E) routine is only necessary if more control is needed over property management. For example, if the value of a property changes frequently, it may be more efficient for the driver to maintain it locally, updating a variable representing the property
- value whenever it changes. If a caller requests the value of the property, the driver's prop_op(9E) modifies the property using ddi_prop_modify(9F) and then calls ddi_prop_op(9F) to retrieve it.
- Providing a prop_op(9E) entry point does not mean that the driver must manage all properties locally. If a property is modified dynamically, it should be maintained by the driver in prop_op(9E). If a property is static (set only once), it is easier for the driver to use ddi_prop_create(9F) to create it and allow ddi_prop_op(9F) to retrieve it. The prop_op(9E) entry point would just intercept some property requests and pass all others to ddi_prop_op(9F). Here is the prop_op(9E) prototype:
-
-
int xxprop_op(dev_t dev, dev_info_t *dip,
ddi_prop_op_t prop_op, int flags, char *name,
caddr_t valuep, int *lengthp);
- This section describes a simple implementation of the prop_op(9E) routine that intercepts property requests then uses the existing software property routines to update property values. For a complete description of all the parameters to prop_op(9E), see the manual page.
- In Code Example 3-3, the prop_op(9E) intercepts requests for the nblocks property. The driver updates a variable in the state structure whenever the property changes but only updates the property when a request is made. It then uses the system routine ddi_prop_op(9F) to get the new value. If the property request is not specific to a device, the driver does not intercept the request. This is indicated when the value of the dev parameter is equal to DDI_DEV_T_ANY (the wildcard device number).
-
State Structure This section adds the following field to the state structure. See "State Structure" on page 57 for more information.
-
-
int nblocks; /* number of blocks in block device */
-
Code Example 3-3 prop_op(9E) routine
-
-
static int
xxprop_op(dev_t dev, dev_info_t *dip, ddi_prop_op_t prop_op,
int flags, char *name, caddr_t valuep, int *lengthp)
{
int instance;
struct xxstate *xsp;
-
-
if (dev == DDI_DEV_T_ANY)
goto skip;
instance = getminor(dev);
xsp = ddi_get_soft_state(statep, instance);
if (xsp == NULL)
return (DDI_PROP_NOTFOUND);
if (strcmp(name, "nblocks") == 0) {
ddi_prop_modify(dev, dip, "nblocks", flags,
&xsp->nblocks, sizeof(int));
}
other cases
skip:
return (ddi_prop_op(dev, dip, prop_op, flags, name,
valuep, lengthp));
}
Driver Layout
- This section suggests a structure for device drivers. The following sections describe the example driver layout in detail.
- The code for a device driver is usually divided into the following files:
-
- headers (.h files)
- source files (.c files)
- possibly a configuration file (.conf file)
-
Note - This is a suggested layout only. It is not required, as only the final object module matters to the system.
Header Files
- Header files define data structures specific to the device (such as a structure representing the device registers), data structures defined by the driver for maintaining state information, defined constants (such as those representing the bits of the device registers), and macros (such as those defining the static mapping between the minor device number and the instance number).
- Some of this information, such as the state structure, may only be needed by the device driver. This information should go in private headers. These header files are only included by the device driver itself.
- Any information that an application might require, such as the I/O control commands, should be in public header files. These are included by the driver and any applications that need information about the device.
- There is no standard for naming private and public files. One possible convention is to name the private header file xximpl.h and the public header file xxio.h. Code Example 3-4, and Code Example 3-5 show the layout of these headers.
-
Code Example 3-4 xximpl.h Header File
-
-
/*
* xximpl.h
*/
struct device_reg {
/* fields... */
};
/*
* #define bits of the device registers...
*/
struct xxstate {
/* fields */
};
/*
* related #define statements
*/
-
Code Example 3-5 xxio.h Header File
-
-
/*
* xxio.h
*/
struct xxioctlreq {
/* fields */
};
/*
* etc.
*/
-
-
#define XXIOC ('b' << 8)
#define XXIOCTL_1 (XXIOC | 1) /* description */
#define XXIOCTL_2 (XXIOC | 2) /* description */
xx.c Files
- A.c file for a device driver contains the data declarations and the code for the entry points of the driver. It contains the #include statements the driver needs, declares extern references, declares local data, sets up the cb_ops and dev_ops structures, declares and initializes the module configuration section, makes any other necessary declarations, and defines the driver entry points. The following sections describe these driver components. Code Example 3-6 shows the layout of an xx.c file:
-
Code Example 3-6 xx.c File
-
-
/*
* xx.c
*/
#include "xximpl.h"
#include "xxio.h"
#include <sys/ddi.h> /* must include these two files */
#include <sys/sunddi.h> /* and they must be the last system */
/* includes */
/*
* Forward declaration of entry points
*/
/*
* static declarations of cb_ops entry point functions...
*/
static struct cb_ops xx_cb_ops = {
/*
* set cb_ops fields
*/
};
/*
* static declarations of dev_ops entry point functions...
*/
static struct dev_ops xx_ops = {
/*
-
-
* set dev_ops fields
*/
};
/*
* declare and initialize the module configuration section...
*/
static struct modldrv modldrv = {
/*
* set modldrv fields
*/
};
static struct modlinkage modlinkage = {
/*
* set modlinkage fields
*/
};
int
_init(void)
{
/* definition */
}
int
_info(struct modinfo *modinfop)
{
/* definition */
}
int
_fini(void)
{
/* definition */
}
static int
xxidentify(dev_info_t *dip)
{
/* definition */
}
static int
xxprobe(dev_info_t *dip)
{
/* definition */
}
-
-
static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
/* definition */
}
static int
xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
/* definition */
}
static int
xxgetinfo(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg,
void **result)
{
/* definition */
}
static int
xxopen(dev_t *devp, int flag, int otyp, cred_t *credp)
{
/* definition */
}
static int
xxclose(dev_t dev, int flag, int otyp, cred_t *credp)
{
/* definition */
}
static int
xxstrategy(struct buf *bp)
{
/* definition */
}
/* for character-oriented devices
*/
static int
xxread(dev_t dev, struct uio *uiop, cred_t *credp)
{
/* definition */
}
static int
xxwrite(dev_t dev, struct uio *uiop, cred_t *credp)
{
/* definition */
}
-
-
static int
xxioctl(dev_t dev, int cmd, int arg, int mode, cred_t *credp,
int *rvalp)
{
/* definition */
}
/*
* for memory-mapped character-oriented devices
*/
static int
xxmmap(dev_t dev, off_t off, int prot)
{
/* definition */
}
/*
* for support of the poll(2) system call
*/
static int
xxchpoll(dev_t dev, short events, int anyyet, short *reventsp,
struct pollhead **phpp)
{
/* definition */
}
/*
* for drivers needing a xxprop_op routine
*/
static int
xxprop_op(dev_t dev, dev_info_t *dip, ddi_prop_op_t prop_op,
int mod_flags, char *name, caddr_t valuep, int *lengthp)
{
/* definition */
}
/*
* other driver routines, such as xxintr()
*/
The C Language and Compiler Modes
- The SPARCworks 2.0.1 and ProWorks 2.0.1 C compilers are ANSI C compilers. They support several compilation modes, a number of new keywords and function prototypes.
Compiler Modes
- The following compiler modes are of interest to driver writers:
-Xt (Transition Mode)
- This mode accepts ANSI C and Sun C compatibility extensions. In case of a conflict between ANSI and Sun C, a warning is issued and Sun C semantics are used. This is the default mode.
-Xa (ANSI C Mode)
- This mode accepts ANSI C and Sun C compatibility extensions. In case of a conflict between ANSI and Sun C, the compiler issues a warning and uses ANSI C interpretations. This will be the default mode in the future.
Function Prototypes
- Function prototypes specify the following information to the compiler:
-
- The type returned by the function
- The number of the arguments to the function
- The type of each argument
- This allows the compiler to do more type checking and also to promote the types of the parameters to the type expected by the function. For example, if the compiler knows a function takes a pointer, casting NULL to that pointer type is no longer necessary. Prototypes are provided for most Solaris 2.x DDI/DKI functions, provided the driver includes the proper header file (documented in the manual page for the function).
New Keywords
- There are a few new keywords available in ANSI C. The following keywords are of interest to driver writers:
const
- The const keyword can be used to define constants instead of using #define:
-
-
const int count=5;
- However, it is most useful when combined with function prototypes. Routines that should not be modifying parameters can define the parameters as constants, and the compiler will then give errors if the parameter is modified. Since C passes parameters by value, most parameters don't need to be declared as constants. If the parameter is a pointer, though, it can be declared to point to a constant object:
-
-
int strlen(const char *s)
{
...
}
- Any attempt to change the string by strlen() is an error, and the compiler will now catch it.
volatile
- The correct use of volatile is necessary to prevent elusive bugs. It instructs the compiler to use exact semantics for the declared objects--in particular, do not optimize away or reorder accesses to the object. There are two instances where device drivers must use the volatile qualifier:
-
- When data refers to an external hardware device register (memory that has side effects other than just storage)
- When data refers to global memory that is accessible by more than one thread, is not protected by locks, and therefore is relying on the sequencing of memory accesses
- In general, drivers should not qualify a variable as volatile if it is merely accessible by more than one thread and protected from conflicting access by synchronization routines.
- The following is an example of the first use. There are two writes to a device required to begin a transfer:
-
-
struct device_reg *regp;
regp->csr = ENABLE_INTERRUPTS;
regp->csr = START_TRANSFER;
- A highly optimizing compiler may determine that the value of regp->csr is not used between the first and the second assignment, and could remove the first assignment. Such a compiler might also determine that reordering the instructions would provide better performance, causing the device to be programmed in the incorrect order. If the csr field is declared volatile, it is not allowed to do so.
- Following is an example of the second use of volatile. A busy flag is used to prevent a thread from continuing while the device is busy and the flag is not protected by a lock:
-
-
while (busy) {
-
do something else
-
-
}
- The testing thread will continue when another thread turns off the busy flag:
-
-
busy = 0;
- However, since busy is accessed frequently in the testing thread, the compiler may optimize the test by placing the value of busy in a register, then test the contents of the register without reading the value of busy in memory before every test. The testing thread would never see busy change and the other thread would only change the value of busy in memory, resulting in deadlock. The busy flag should be declared volatile, forcing its value to be read before each test.
-
Note - It would probably be preferable to use a condition variable mutex, discussed under "Condition Variables" on page 79 instead of the busy flag in this example.
- It is also recommended that the volatile qualifier be used in such a way as to avoid the risk of accidental omission. For example, this code
-
-
struct device_reg {
volatile u_char csr;
volatile u_char data;
};
struct device_reg *regp;
- is recommended over:
-
-
struct device_reg {
u_char csr;
u_char data;
};
volatile struct device_reg *regp;
- Although the two examples are functionally equivalent, the second one requires the writer to ensure that volatile is used in every declaration of type struct device_reg. The first example results in the data being treated as volatile in all declarations and is therefore preferred.
|
|