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.
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:
Strong data ordering - If DDI_STRICTORDER_ACC is set, the CPU must issue the references in order, as specified by the programmer. This is the default behavior.
Reordering - If DDI_UNORDERED_OK_ACC is set, the CPU may reorder the data reference. This includes all kinds of reordering (for example, a load followed by a store may be replaced by a store followed by a load).
Data merging - If DDI_MERGING_OK_ACC is set, the CPU may merge individual stores to consecutive locations. For example, the CPU may turn two consecutive byte stores into one halfword store. It may also batch individual loads. For example, the CPU may turn two consecutive byte loads into one halfword load. DDI_MERGING_OK_ACC also implies reordering.
Cache loading - If DDI_LOADCACHING_OK_ACC is set, the CPU may cache the data it fetches and reuse it until another store occurs. The default behavior is to fetch new data on every load. DDI_LOADCACHING_OK_ACC also implies merging and reordering.
Cache storing - If DDI_STORECACHING_OK_ACC is set, the CPU may keep the data in the cache and push it to the device (perhaps with other data) at a later time. The default behavior is to push the data right away. DDI_STORECACHING_OK_ACC also implies load caching, merging, and reordering.
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.
Drivers should not directly dereference the returned address. A driver must access the device through one of the 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.
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.
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(..., ®_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.
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.
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 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.
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.
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:
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 examples use following macros:
#define INTERRUPTS_ENABLED 0x10 #define INTERRUPTING 0x08 #define DEVICE_BUSY 0x04 #define DEVICE_ERROR 0x02 #define TRANSFER_COMPLETE 0x01
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 *)®p, ... , &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, ®p->csr); if (status & TRANSFER_COMPLETE) { /* read data */ data = ddi_get8(data_access_handle, ®p->data); }
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.
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.
#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.