Writing Device Drivers

Bus Address Spaces

Three types of bus address space are memory space, I/O space, and configuration space. The device driver usually accesses memory space through memory mapping and I/O space through I/O ports. The configuration address space is accessed primarily during system initialization.

The preferred method depends on the device; it is generally not software configurable. For example, SBus and VMEbus devices do not provide I/O ports or configuration space, but some PCI devices may provide all three.

The data format of the host may also have different endian characteristics than the data format of the device. If this is the case, data transferred between the host and the device needs to be byte swapped to conform to the data format requirements of the destination location. Other devices may have the same endian characteristics as their host. In this case, no byte swapping is required. The DDI framework performs any required byte swapping on behalf of the driver. The driver simply needs to specify the endianness of the device to the framework.

Address Mapping Setup

Before a driver can access a device's bus address, the bus address spaces must be set up using ddi_regs_map_setup(9F). The driver can then access the device by passing the data access handle returned from ddi_regs_map_setup(9F) to one of the ddi_get8(9F) or ddi_put8(9F) family of routines.

One of the arguments required by ddi_regs_map_setup(9F) is a pointer to a device access attributes structure, ddi_device_acc_attr(9S). The ddi_device_acc_attr(9S) structure describes the data access characteristics and requirements of the device. The ddi_device_acc_attr(9S) structure contains the following members:

	ushort_t   devacc_attr_version;
 	uchar_t    devacc_attr_endian_flags;
 	uchar_t    devacc_attr_dataorder;

devacc_attr_version member identifies the version number of this structure. The current version number is DDI_DEVICE_ATTR_V0.

devacc_attr_endian_flags member describes the endian characteristics of the device. If DDI_NEVERSWAP_ACC is set, data access with no byte swapping is indicated. This flag should be set when no byte swapping is required. For example, if a device does byte-stream I/O, no byte swapping is required. If DDI_STRUCTURE_BE_ACC is set, the device data format is big endian. If DDI_STRUCTURE_LE_ACC is set, the device data format is little endian.

The framework will do any required byte swapping on behalf of the driver based on the flags indicated in devacc_attr_endian_flags and the host's data format endian characteristics.

devacc_attr_dataorder describes the order in which the CPU will reference data. Certain hosts may load or store data in certain orders to pipeline performance. The data ordering may be programmed to execute in one of the following ways:


Note -

The restriction to the hosts diminishes while moving from strong data ordering to cache storing in terms of data accesses by the driver.


The values assigned to devacc_attr_dataorder are advisory, not mandatory. For example, data can be ordered without being merged or cached, even though a driver requests unordered, merged, and cached together.

A driver for a big-endian device that requires strict data ordering during data accesses would encode the ddi_device_acc_attr structure as follows:

	static ddi_device_acc_attr_t access_attr = {
 		DDI_DEVICE_ATTR_V0,		/* version number */
 		DDI_STRUCTURE_BE_ACC, 	/* big endian */
 		DDI_STRICTORDER_ACC		/* strict ordering */
 	}

The system will use the information stored in the ddi_device_acc_attr structure and other system-specific information to encode an opaque data handle as one of the returned parameters from ddi_map_regs_setup(9F). The returned data handle is used as a parameter to the data access routines (such as ddi_put8(9F) or ddi_get8(9F)) during subsequent accesses to the mapped registers. The driver must never attempt to interpret the contents of the data handle.

If successful, ddi_regs_map_setup(9F) also returns a kernel virtual address that is mapped to the bus address base. The address base may be used as a base reference address in deriving the effective address of other registers by adding the appropriate offset.


Note -

Drivers should not directly dereference the returned address. A driver must access the device through one of the data access functions.


Data Access Functions

Data access functions allow drivers to transfer data to and from devices without directly referencing the hardware registers. The driver can transfer data to the device or receive data from the device using the ddi_put8(9F) or the ddi_get8(9F) families of routines. The ddi_put8(9F) routines allow a driver to write data to the device in quantities of 8 bits (ddi_put8(9F)), 16 bits (ddi_put16(9F)), 32 bits (ddi_put32(9F)), and 64 bits (ddi_put64(9F)). The ddi_get8(9F) routines exist for reading from a device. Multiple values may be written or read by using the ddi_rep_put8(9F) or ddi_rep_get8(9F) family of routines respectively. See Appendix C, Summary of Solaris 7 DDI/DKI Services for more information on data access functions.


Note -

These routines may be applied to any address base returned from ddi_regs_map_setup(9F) regardless of the address space the register resides in (such as memory, I/O, or configuration space).


Example 3-1 illustrates the use of ddi_regs_map_setup(9F) and ddi_put8(9F) to access device registers.


Example 3-1 Accessing Device Registers

static ddi_device_acc_attr_t access_attr = {
	DDI_DEVICE_ATTR_V0, /*version number */
	DDI_STRUCTURE_BE_ACC, /* big endian */
	DDI_STRICTORDER_ACC /*strict ordering */
};

caddr_t reg_addr;
ddi_acc_handle_t data_access_handle;

ddi_regs_map_setup(..., &reg_addr, ..., &access_attr,
	&data_access_handle);

When ddi_regs_map_setup(9F) returns, reg_addr contains the address base and data_access_handle contains the opaque data handle to be used in subsequent data accesses. The driver may now access the mapped registers. The following example writes one byte to the first mapped location.

ddi_put8(data_access_handle, (uint8_t *)reg_addr, 0x10);

Similarly, the driver could have used ddi_get8(9F) to read data from the device registers.

Memory Space Access

In memory-mapped access, device registers appear in memory address space. The driver must call ddi_regs_map_setup(9F) to set up the mapping. The driver can then access the device registers using one of the ddi_put8(9F) or ddi_get8(9F) family of routines.

To access memory space, the driver can use the ddi_mem_put8(9F) and ddi_mem_get8(9F) family of routines. These functions may be more efficient on some platforms. Use of these routines, however, may limit the ability of the driver to remain portable across different bus versions of the device.

I/O Space Access

In I/O space access, the device registers appear in I/O space. Each addressable element of the I/O address is called an I/O port. Device registers are accessed through I/O port numbers. These port numbers can refer to 8, 16, or 32-bit registers. The driver must call ddi_regs_map_setup(9F) to set up the mapping, and it can then access the I/O port using one of the ddi_put8(9F) or ddi_get8(9F) family of routines.

The driver can also access I/O space using the ddi_io_put8(9F) and ddi_io_get8(9F) family of routines. These functions may be more efficient on some platforms. Use of these routines, however, may limit the ability of the driver to remain portable across different bus versions of the device.

Configuration Space Access

Configuration space is used primarily during device initialization. It determines the location and size of register sets and memory buffers located on the device. The driver can access configuration space using the ddi_regs_map_setup(9F) and the ddi_put(9F or ddi_get(9F) functions as described previously.


Note -

For PCI local bus devices, an alternative set of routines exists. To get access to the configuration address space, the driver can use pci_config_setup(9F) in place of ddi_regs_map_setup(9F). The pci_config_get8(9F) and pci_config_put8(9F) family of routines may be used in place of the generic routines ddi_get8(9F) and ddi_put8(9F). These functions provide equivalent configuration space access as defined in the PCI bus binding for the IEEE 1275 specifications for FCode drivers. However, use of these routines may limit the ability of the driver to remain portable across different bus versions of the device.


Example Device Registers

Most of the examples in this manual use a fictitious device that has an 8-bit command and status register (csr), followed by an 8-bit data register. The command and 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:

Graphic

The status register looks like this:

Graphic

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 examples use following macros:

	#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 been completed might look like this:

	uint8_t data;
 	uint8_t status;
 	/* get status */
 	status = ddi_get8(data_access_handle, (uint8_t *)reg_addr);
 	if (status & TRANSFER_COMPLETE) {
 		data = ddi_get8(data_access_handle,
 			(uint8_t *)reg_addr + 1); /* read data */
 	}

To make the code more readable, it is common to define a structure that matches the layout of the device registers. In this case, the structure could look like this:

	struct device_reg {
 		uint8_t csr;
 		uint8_t data;
 	};

The driver then maps the registers into memory and refers to them through a pointer to the structure:

	struct device_reg *regp;
	...
 	ddi_regs_map_setup(..., (caddr_t *)&regp, ... ,
 		&access_attributes, &data_access_handle);
 	...

The code that reads the data register upon a completed transfer now looks like this:

	uint8_t data;
 	uint8_t status;
 	/* get status */
 	status = ddi_get8(data_access_handle, &regp->csr); 	
 	if (status & TRANSFER_COMPLETE) {
 		/* read data */
 		data = ddi_get8(data_access_handle, &regp->data); 	
 	}

Structure Padding

A device that has a 1-byte command and status register followed by a 4-byte data register might lead to the following structure layout:

	struct device_reg {
 		uint8_t			csr;
 		uint32	_t		data;
 	};

This 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 1-byte alignment for the csr field, but 4-byte alignment for the data field. This results in three unused bytes between the two fields. When the driver accesses a data register, it will be three bytes off. Consequently, this layout should not be used.

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.


Example 3-2 Structure Padding

#include <sys/types.h>
#include <stdio.h>
#include <stddef.h>

struct device_reg {
	uint8_t			csr;
	uint32_t			data;
};

int main(void)
{
	printf("The offset of csr is %d, its size is %d.\n",
			offsetof(struct device_reg, csr), sizeof (uint8_t));
	printf("The offset of data is %d, its size is %d.\n",
			offsetof(struct device_reg, data), sizeof (uint32_t));
	return (0);
}

Here is a sample compilation with Sun WorkShopTM Compiler C version 4.2 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.

Be aware that padding is dependent not only on the processor but also on the compiler.