Writing Device Drivers

HBA Driver Dependency and Configuration Issues

In addition to incorporating SCSA HBA entry points, structures, and functions into a driver, HBA driver developers must also concern themselves with issues surrounding driver dependency and configuration. These issues are summarized in the following list:

Declarations and Structures

HBA drivers must include the following header files:

#include <sys/scsi/scsi.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>

To inform the system that the module depends on SCSA routines (see "SCSA HBA Interfaces" for more information), the driver binary must be generated with the following command:

% ld -r xx.o -o xx -N"misc/scsi"

The code samples are derived from a simplified isp driver for the QLogic Intelligent SCSI Peripheral device. The isp driver supports WIDE SCSI, with up to 15 target devices and 8 logical units (LUNs) per target.

Per-Command Structure

An HBA driver will usually need to define a structure to maintain state for each command submitted by a target driver. The layout of this per-command structure is entirely up to the device driver writer and needs to reflect the capabilities and features of the hardware and the software algorithms used in the driver.

The following structure is an example of a per-command structure. The remaining code fragments of this chapter use this structure to illustrate the HBA interfaces.

struct isp_cmd {
     struct isp_request            cmd_isp_request;
     struct isp_response           cmd_isp_response;
     struct scsi_pkt               *cmd_pkt;
     struct isp_cmd                *cmd_forw;
     uint32_t                      cmd_dmacount;
     ddi_dma_handle_t              cmd_dmahandle;
     uint_t                        cmd_cookie;
     uint_t                        cmd_ncookies;
     uint_t                        cmd_cookiecnt;
     uint_t                        cmd_nwin;
     uint_t                        cmd_curwin;
     off_t                         cmd_dma_offset;
     uint_t                        cmd_dma_len;
     ddi_dma_cookie_t              cmd_dmacookies[ISP_NDATASEGS];
     u_int                         cmd_flags;
     u_short                       cmd_slot;
     u_int                         cmd_cdblen;
     u_int                         cmd_scblen;
 };

Module Initialization Entry Points

Drivers for different types of devices have different sets of entry points, depending on the operations they perform. Some operations, however, are common to all drivers, such as the as _init(9E), _info(9E), and _fini(9E) entry points for module initialization. Chapter 2, Overview of Solaris Device Drivers gives a complete description of these loadable module routines. This section describes only those entry points associated with operations performed by SCSI HBA drivers.

The following code for a SCSI HBA driver illustrates a representative dev_ops(9S) structure. The driver must initialize the devo_bus_ops field in this structure to NULL. A SCSI HBA driver can provide leaf driver interfaces for special purposes, in which case the devo_cb_ops field might point to a cb_ops(9S) structure. In this example, no leaf driver interfaces are exported, so the devo_cb_ops field is initialized to NULL.

_init(9E)

The _init(9E) function initializes a loadable module and is called before any other routine in the loadable module.

In a SCSI HBA, the _init(9E) function must call scsi_hba_init(9F) to inform the framework of the existence of the HBA driver before calling mod_install(9F). If scsi_hba_init(9F) returns a nonzero value, _init(9E) should return this value. Otherwise, _init(9E) must return the value returned by mod_install(9F).

The driver should initialize any required global state before calling mod_install(9F).

If mod_install(9F) fails, the _init(9E) function must free any global resources allocated and must call scsi_hba_fini(9F) before returning.

The following code sample uses a global mutex to show how to allocate data that is global to all instances of a driver. The code declares global mutex and soft-state structure information. The global mutex and soft state are initialized during _init(9E).

_fini(9E)

The _fini(9E) function is called when the system is about to try to unload the SCSI HBA driver. The _fini(9E) function must call mod_remove(9F) to determine if the driver can be unloaded. If mod_remove(9F) returns 0, the module can be unloaded, and the HBA driver must deallocate any global resources allocated in _init(9E) and must call scsi_hba_fini(9F).

_fini(9E) must return the value returned by mod_remove(9F).


Note -

The HBA driver must not free any resources or call scsi_hba_fini(9F) unless mod_remove(9F) returns 0.


Example 15-1 shows SCSI HBA module intialization.


Example 15-1 SCSI HBA Module Initialization

static struct dev_ops isp_dev_ops = {
        DEVO_REV,           /* devo_rev */
        0,                  /* refcnt  */
        isp_getinfo,        /* getinfo */
        nulldev,            /* probe */
        isp_attach,         /* attach */
        isp_detach,         /* detach */
        nodev,              /* reset */
        NULL,               /* driver operations */
        NULL,               /* bus operations */
        isp_power,          /* power management */
};

/*
 * Local static data
 */
static kmutex_t          isp_global_mutex;
static void              *isp_state;

int
_init(void)
{
        int     err;
    
        if ((err = ddi_soft_state_init(&isp_state,
            sizeof (struct isp), 0)) != 0) {
                return (err);
        }
        if ((err = scsi_hba_init(&modlinkage)) == 0) {
                mutex_init(&isp_global_mutex, "isp global mutex",
                MUTEX_DRIVER, NULL);
                if ((err = mod_install(&modlinkage)) != 0) {
                    mutex_destroy(&isp_global_mutex);
                    scsi_hba_fini(&modlinkage);
                    ddi_soft_state_fini(&isp_state);    
                }
        }
        return (err);
}

int
_fini(void)
{
        int     err;
    
        if ((err = mod_remove(&modlinkage)) == 0) {
                mutex_destroy(&isp_global_mutex);
                scsi_hba_fini(&modlinkage);
                ddi_soft_state_fini(&isp_state);
        }
        return (err);
}

Autoconfiguration Entry Points

Associated with each device driver is a dev_ops(9S) structure, which allows the kernel to locate the autoconfiguration entry points of the driver. A complete description of these autoconfiguration routines is given in Chapter 5, Autoconfiguration. This section describes only those entry points associated with operations performed by SCSI HBA drivers. These include attach(9E) and detach(9E).

attach(9E)

The attach(9E) entry point for a SCSI HBA driver must perform a number of tasks to configure and attach an instance of the driver for the device. For a typical driver of real devices, the following operating system and hardware concerns must be addressed:

Soft State Structure

The driver should allocate the per-device-instance soft state structure, being careful to clean up properly if an error occurs.

DMA

The HBA driver must describe the attributes of its DMA engine by properly initializing the ddi_dma_attr_t structure.

static ddi_dma_attr_t isp_dma_attr = {
         DMA_ATTR_V0,        /* ddi_dma_attr version */
         0,                  /* low address */
         0xffffffff,         /* high address */
         0x00ffffff,         /* counter upper bound */
         1,                  /* alignment requirements */
         0x3f,               /* burst sizes */
         1,                  /* minimum DMA access */
         0xffffffff,         /* maximum DMA access */
         (1<<24)-1,          /* segment boundary restrictions */
         1,                  /* scatter/gather list length */
         512,                /* device granularity */
         0                   /* DMA flags */
};

The driver, if providing DMA, should also check that its hardware is installed in a DMA-capable slot:

        if (ddi_slaveonly(dip) == DDI_SUCCESS) {
             return (DDI_FAILURE);
         }

Transport Structure

The driver should further allocate and initialize a transport structure for this instance. The tran_hba_private field is set to point to this instance's soft-state structure. tran_tgt_probe can be set to NULL to achieve the default behavior, if no special probe customization is needed.

    tran = scsi_hba_tran_alloc(dip, SCSI_HBA_CANSLEEP);

     isp->isp_tran                   = tran;
     isp->isp_dip                    = dip;

     tran->tran_hba_private          = isp;
     tran->tran_tgt_private          = NULL;
     tran->tran_tgt_init             = isp_tran_tgt_init;
     tran->tran_tgt_probe            = scsi_hba_probe;
     tran->tran_tgt_free             = (void (*)())NULL;

     tran->tran_start                = isp_scsi_start;
     tran->tran_abort                = isp_scsi_abort;
     tran->tran_reset                = isp_scsi_reset;
     tran->tran_getcap               = isp_scsi_getcap;
     tran->tran_setcap               = isp_scsi_setcap;
     tran->tran_init_pkt             = isp_scsi_init_pkt;
     tran->tran_destroy_pkt          = isp_scsi_destroy_pkt;
     tran->tran_dmafree              = isp_scsi_dmafree;
     tran->tran_sync_pkt             = isp_scsi_sync_pkt;
     tran->tran_reset_notify         = isp_scsi_reset_notify;
     tran->tran_bus_quiesce          = isp_tran_bus_quiesce
     tran->tran_bus_unquiesce        = isp_tran_bus_unquiesce
     tran->tran_bus_reset            = isp_tran_bus_reset

Attaching an HBA Driver

The driver should attach this instance of the device, and perform error cleanup if necessary.

    i = scsi_hba_attach_setup(dip, &isp_dma_attr, tran, 0);
    if (i != DDI_SUCCESS) {
         do error recovery     
            return (DDI_FAILURE);
     }

Register Mapping

The driver should map in its device's registers, specifying the index of the register set, the data access characteristics of the device and the size of the register set to be mapped.

ddi_device_acc_attr_t                        dev_attributes;

     dev_attributes.devacc_attr_version = DDI_DEVICE_ATTR_V0;
     dev_attributes.devacc_attr_dataorder = DDI_STRICTORDER_ACC;
     dev_attributes.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC;

     if (ddi_regs_map_setup(dip, 0, (caddr_t *)&isp->isp_reg,
         0, sizeof (struct ispregs), &dev_attributes,
         &isp->isp_acc_handle) != DDI_SUCCESS) {
         do error recovery
            return (DDI_FAILURE);
     }

Adding an Interrupt Handler

The driver must first obtain the iblock cookie to initialize mutexes used in the driver handler. Only after those mutexes have been initialized can the interrupt handler be added.

    i = ddi_get_iblock_cookie(dip, 0, &isp->iblock_cookie};
     if (i != DDI_SUCCESS) {
         do error recovery
            return (DDI_FAILURE);
     }

     mutex_init(&isp->mutex, "isp_mutex", MUTEX_DRIVER,
         (void *)isp->iblock_cookie);
     i = ddi_add_intr(dip, 0, &isp->iblock_cookie,
         0, isp_intr, (caddr_t)isp);
     if (i != DDI_SUCCESS) {
         do error recovery
            return (DDI_FAILURE);
     }

The driver should determine if a high-level interrupt handler is required. If a high-level handler is required and the driver is not coded to provide one, the driver must be rewritten to include either a high-level interrupt or fail the attach. See "Handling High-Level Interrupts" for a description of high-level interrupt handling.

Create Power Manageable Components

If the host bus adapter hardware supports power management, and it is sufficient to have the host bus adapter powered down only when all of the target adapters are power manageable and are at power level 0, then the host bus adapter driver only needs to provide a power(9E) entry point as described in Chapter 9, Power Management and create a pm-components(9) property that describes the components that the device implements.

Nothing more is necessary, since the components will default to idle, and the power management framework's default dependency processing will ensure that the host bus adapter will be powered up whenever an target adapter is powered up and will automatically power down the host bus adapter whenever all of the target adapters are powered down (provided that automatic power management is enabled).

Report Attachment Status

Finally, the driver should report that this instance of the device is attached and return success.

    ddi_report_dev(dip);
     return (DDI_SUCCESS);

detach(9E)

The driver should perform standard detach operations. See "detach(9E)".

Example 15-2 provides an example of the isp_detach() function.


Example 15-2 isp_detach

static int
isp_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
    switch (cmd) {
    case DDI_DETACH:
           /*
            * At present, detaching HBA drivers is not supported
            */
            return (DDI_FAILURE);
    
    default:
            return (DDI_FAILURE);
    }
}