ChorusOS 5.0 Board Support Package Developer's Guide

Chapter 11 Writing a New Bus Driver

This chapter takes a procedural approach to writing both host and subsequent level bus drivers.

The small differences in writing a driver for a host bus and for a sub-bus (bus to bus bridge) are outlined in steps where they occur. Also, this chapter indicates the additional steps required to provide the common bus driver API and to allow multi-bus device drivers to run on top of your bus driver.

Including the Appropriate APIs (DKI/DDI)

The first step to building a bus driver is to include the header files for the DKI and DDI APIs involved in the bus driver's implementation.

A host bus driver implementation uses only the DKI interface, because there is no other driver component between the host bus and the microkernel API. On the other hand, a bus-to-bus bridge driver typically uses its parent bus DDI API (and some generic DKI services, like memory allocation).

In either case, the driver implementation must include:

Shown below is an example for a PCI-to-ISA bridge bus driver that uses both DKI and "PCI bus driver" APIs, and that provides both "Common bus driver" and "ISA bus driver" APIs.

#include <dki/dki.h> 
#include <ddi/pci/pci.h>
#include <ddi/isa/isa.h>
...
#include "w83c553.h"
#include "w83c553Prop.h"

The Bus Driver main() Routine

A driver component may be downloaded in various ways. It may be built into the system bootable image or it may be downloaded dynamically as a supervisor actor using the afexec system call. In either case, the driver code must contain the main routine which will be called by the system once the driver component is downloaded.

The only task of the driver's main routine is to perform the self-registration of the driver component within the system. To accomplish this task, the driver invokes the svDriverRegister microkernel call, passing as an argument a DrvRegEntry data structure which specifies the driver component properties.

Once the driver component is self-registered, any future driver management is mostly undertaken by its parent bus/nexus driver (or the DKI module for a host bus driver) using the properties specified in the DrvRegEntry structure. The DrvRegEntry specifies four driver entry points as follows:

drv_probe()

drv_probe() is invoked by the parent bus/nexus driver (or the DKI module for a host bus driver) when bus_class specified in the registry entry matches the parent bus/nexus driver class.

drv_bind()

drv_bind() is invoked by the parent bus/nexus driver (or the DKI module for a host bus driver) when bus_class specified in the registry entry matches the parent bus/nexus driver class.

drv_init()

drv_init() is invoked by the parent bus/nexus driver (or the DKI module for a host bus driver) when bus_class (specified in the registry entry) matches the parent bus class and the driver is bound to a node in the device tree. (Or put more simply, when a hardware device to be managed by the driver exists.)

drv_unload()

drv_unload() is invoked by the driver registry module when an application wishes to unload the driver component from the system.

    /*
     * Driver registry entry for Winbond w83c553 PCI/ISA bridge
     */
static void    drv_bind   (DevNode myNode);
static void    drv_init   (DevNode myNode, void* pciOps, void* pciId);
static KnError drv_unload ();

static DrvRegEntry w83c553Drv = {
    W83C553_DRV_NAME,
    "Winbond w83c553 PCI to ISA bridge" 
    "[#ident \"@(#)w83c553.c 1.5 99/02/23 SMI\"]",
    PCI_CLASS,           /* parent bus class      */
    PCI_VERSION_INITIAL, /* required bus version  */
    NULL,                /* probe method          */
    drv_bind,            /* bind method           */
    drv_init,            /* init method           */
    drv_unload           /* unload method         */
};
    /*
     * Driver main() routine.
     * Called by kernel at driver startup time.
     */
    int
main (int argc, char** argv)
{
    KnError res = svDriverRegister(&w83c553Drv);

    if (res != K_OK) {
        DKI_ERR(("%s: error -- svDriverRegister() failed (%d)\n",
                 w83c553Drv.drv_name, res));
    }
    return res;
}

The Bus Driver Class Specific Functions

At this step, you write the implementation for a specific hardware bus controller. In other words, the code has to be written for each function of the specified bus class, as defined in the API. These functions must then be provided to the subsequent level device drivers.


Note -

None of these functions is directly exported, but that all of them are defined as static functions, and then grouped as indirect calls in an "operations data structure" typed by the bus class API. The bus driver then gives this "operations data structure" as an argument to its child device driver's Probe and Init registered functions (see "The Driver Registry")


In this way you can ensure that the visibility and use of the bus API is restricted to device drivers which are servicing a device connected to this bus.

Each bus class API is different. Therefore, the functions to write are different for different classes of bus API. The complete list of the currently defined bus class APIs may be found in the Intro(9DDI) man page.


Note -

All of these steps are performed in the example provided for the "Write General Functions" section below. See specifically the sections dealing with the ISA bus.


Bus Driver General Functions

This section describes the additional functions that are required to provide the common bus API services. This section is specific to writing bus drivers.

Once the bus class services are implemented in a bus driver component, you can also write the additional functions needed to provide the Common bus API services (in addition to those provided by the bus class API). This allows child device drivers to be written to the common bus-driver interface (CBDI), making them bus class independent (bottom-interface transparent).

In most cases, these services have already been implemented in the previous step. There are normally only three functions that must be added to allow the subsequent device drivers to perform independently of their bus type. These functions essentially allow the bus driver's clients to retrieve property elements from arrays.

The following code example illustrates this for a PCI-to-ISA bridge bus driver. In this example, the ISA bus class is provided, and the additional functions are written to provide the CBDI.


Note -

For clarity, only the code pertinent to this explanation is shown. Please refer to a complete implementation file for more details.


    /*
     * ISA bus provided API
     */
    static KnError
open (IsaId isaId, ...) { ... }
    static void
close (IsaDevId devId) { ... }
    /*
     * Interrupt management
     */
    static void
mask (IsaIntrId intrId) { ... } 
    static void
unmask (IsaIntrId intrId) { ... }
    static IsaIntrStatus
enable (IsaIntrId intrId) { ... }

static IsaIntrOps w83c553IntrOps = {
    mask,
    unmask,
    enable,
    unmask
};
    static KnError
intr_attach (IsaDevId devId, ...) { ... }
    static void
intr_detach (IsaIntrId intrId) { ... }
    /*
     * I/O management
     */
    static KnError
io_map (IsaDevId devId, ...) { ... }
    /*
     * Note that there is no io_unmap() method implementation.
     * The PCI bridge io_unmap() method is directly used instead.
     * This PCI method is set in the IsaBusOps at drv_init() time.
     */
    /*
     * DMA management
     */
    static KnError
dma_attach (IsaDevId devId, ...) { ... }
    static void
dma_detach (IsaDmaId dmaId) { ... }
    /*
     * Memory management
     */
    static KnError
mem_map (IsaDevId devId, ...) { ... }
    /*
     * Note that there is no mem_unmap() method implementation.
     * The PCI bridge mem_unmap() method is directly used instead.
     * This PCI method is set in the IsaBusOps at drv_init() time.
     */
    /*
     * Dynamic resource allocation
     */
    static KnError
resource_alloc (PciDevId devId, DevProperty prop) { ... }
    static void
resource_free  (PciDevId devId, DevProperty prop) { ... }

    /*
     * Common bus interface miscellaneous routines
     */
    static void*
intr_find (void* prop, int index)
{
    return ((IsaPropIntr*)prop) + index;
}
    static void*
io_regs_find (void* prop, int index)
{
    return ((IsaPropIoRegs*)prop) + index;
}
    static void*
mem_rgn_find (void* prop, int index)
{
    return ((IsaPropMemRgn*)prop) + index;
}

    /*
     * W83C553 driver initialization method
     */
    static void
drv_init (DevNode myNode, void* busOps, void* busId)
{
    PciBusOps*           pciOps = (PciBusOps*)busOps;
    PciBusOps*           pciId  = (PciId)busId;
    DevRegEntry*         w83c553Entry;
    W83c553Data*         w83c553;
    ...
        /*
         * Allocate driver instance data
         */
    w83c553 = w83c553Alloc(path, pathSize);
    if (w83c553 == NULL) {
        DKI_ERR(("%s: error -- not enough memory\n", path));
        return;
    }
    ...
        /*
         * Initialize my base level ISA bus operations
         */
    w83c553->isaOps->version        = ISA_VERSION_INITIAL;
    w83c553->isaOps->open           = open;
    w83c553->isaOps->close          = close;
    w83c553->isaOps->intr_attach    = intr_attach;
    w83c553->isaOps->intr_detach    = intr_detach;
    w83c553->isaOps->io_map         = io_map;
    w83c553->isaOps->io_unmap       = pciOps->io_unmap; /* parent bus ops */
    w83c553->isaOps->dma_attach     = dma_attach;
    w83c553->isaOps->dma_detach     = dma_detach;
    w83c553->isaOps->mem_map        = mem_map;
    w83c553->isaOps->mem_unmap      = pciOps->mem_unmap;/* parent bus ops */
    w83c553->isaOps->resource_alloc = resource_alloc;
    w83c553->isaOps->resource_free  = resource_free;
        /*
         * Initialize my base level Common bus operations
         */
    w83c553->busOps->version        = BUS_VERSION_INITIAL;
    w83c553->busOps->open           = open;
    w83c553->busOps->close          = close;
    w83c553->busOps->intr_attach    = (KnError(*)()) intr_attach;
    w83c553->busOps->intr_detach    = intr_detach;
    w83c553->busOps->io_map         = (KnError(*)()) io_map;
    w83c553->busOps->io_unmap       = pciOps->io_unmap; /* parent bus ops */
    w83c553->busOps->mem_map        = (KnError(*)()) mem_map;
    w83c553->busOps->mem_unmap      = pciOps->mem_unmap;/* parent bus ops */
    w83c553->busOps->intr_find      = intr_find;
    w83c553->busOps->io_regs_find   = io_regs_find;
    w83c553->busOps->mem_rgn_find   = mem_rgn_find;
    ...
        /*
         * Call children drv_probe() / drv_init() methods, if any.
         */
    childrenProbeInit(w83c553);
}

The Driver Registry

This section describes how to write the bus driver registry functions.

Write the Probe Function

The purpose of the bus probe routine is to detect devices residing on the bus and to create device nodes corresponding to these devices. The probe routine is optional. In case it is not provided (NULL entry point), a device node should be statically created at boot time, or should be created by another "probe only" driver component to activate the bus driver.

Actions taken by a probe routine may be summarized as follows:

There are two kinds of probe routine:

Note that multiple probe routines for a given bus may be found in the driver registry. The Driver Framework does not specify the order in which the probe routines will be run. In addition, the probe routines may be invoked at run time (when, for example, the device insertion is detected on the bus).

When invoked at runtime, the probe routines must exercise extreme care with regard to active device nodes. Active device nodes are those for which the device drivers have been already started and may already be in use.

The following rules must be respected by generic and device specific probe routines:

Write the Bind Function

The bind routine enables the driver to perform a driver-to-device binding. Typical actions taken by a bind routine may be summarized as follows:

The driver examines properties attached to the device node to determine the type of device and to check whether the device may be serviced by the driver. Note that the properties examined by the driver are typically bus architecture specific. For instance, a PCI driver would examine the vendor and device identifier properties.

If the check is positive, the driver attaches a "driver" property to the device node. The property value specifies the driver name.

The parent bus/nexus driver should use the "driver" property to determine the name of the driver servicing the device. So, the child driver gives its name to the parent bus driver, through the "driver" property, asking the parent bus driver to invoke the drv_init() routine on that device.

Note that, if a "driver" property is already present in the device node, then the drv_bind() routine cannot continue; drv_bind() should not override an existing driver-to-device binding.

The driver-to-device binding mechanism used in the framework enables multiple implementations. A simple bind routine may be implemented by a device driver. Such an implementation would be device specific, only taking into account the devices known by the driver to be compatible with the driver's reference device.

Let us consider systems that support after-market, hot-plug devices and consult a network lookup service to locate the driver for a new device. It would be reasonable to provide a separate binder driver that would implement a smart driver-to-device mapping and a driver component download. Note that such a (generic) binder appears in the driver framework as a normal driver component. The binder driver provides the bind routine only and does not provide the probe and initialize routines.

  /*
   *  W83C553 driver bind method
   */
  static void
drv_bind (DevNode node)
{
  pciDevDrvBind(node, W83C553_VEND_ID, W83C553_DEV_ID, w83c553Drv.drv_name);
}

The pciDevDrvBind() function, detailed below, is implemented in the drv_tools.s.a library, not in the driver code itself.

		/*
     * Try to bind a given PCI driver to a given PCI device.
     * Basically, the driver is bound to the device node if
     * and only if the vendor/device ID pair specified by the
     * driver matches the vendor/device ID pair specified in
     * the device node.
     * This function is typically called by a drv_bind() method
     * of a PCI driver.
     */
    void
pciDevDrvBind (DevNode       dev_node,
               PciPropVendId drv_vid,
               PciPropDevId  drv_did,
               char*         drv_name)
{
    PciPropVendId dev_vid;
    PciPropDevId  dev_did;
    DevProperty   prop;
    
        /*
         * Do not bind the driver to an active device node.
         */
    if (dtreePropFind(dev_node, PROP_ACTIVE)) {
        return;
    }
        /*
         * Do not override an existing binding.
         */
    if (dtreePropFind(dev_node, PROP_DRIVER)) {
        return;
    }
        /*
         * Do not bind the driver if one of vendor/device ID's (or both)
         * is not specified.
         */
    prop = dtreePropFind(dev_node, PCI_PROP_VEND_ID);
    if (!prop) {
        return;
    }
    dev_vid = *(PciPropVendId*)dtreePropValue(prop);

    prop = dtreePropFind(dev_node, PCI_PROP_DEV_ID);
    if (!prop) {
        return;
    }
    dev_did = *(PciPropDevId*)dtreePropValue(prop);
        /*
         * Do not bind the driver if the device vendor/device IDs
         * do not match the driver ones.
         */
    if ((dev_vid != drv_vid) || (dev_did != drv_did)) {
        return;
    }   
        /*
         * Bind the driver to the device...
         */     
    dtreePropAdd(dev_node, PROP_DRIVER, drv_name, strlen(drv_name)+1);
}

Write the Init Function

The initialization routine of a bus driver component is optional. In case it is not provided (NULL entry point), the driver is typically either a probe only or bind only driver. That is, either a driver that probes a bus to discover devices and create associated device nodes, or a driver that examines device node properties to perform driver-to-device binding.

The initialization process (drv_init()) of a bus/nexus driver mainly goes through the following steps:

  1. bus/nexus device initialization

  2. child device nodes creation (enumeration/probing)

  3. bus resource allocation across child device nodes

  4. driver-to-device binding

  5. driver instance creation for child device nodes

A bus/nexus driver, like any device driver, needs to access its hardware (internal bus bridge registers).. It therefore needs to establish connection to its parent bus/nexus driver and needs to use services implemented by its parent driver.


Note -

The bus/nexus driver does not need to register a new bus/nexus instance in the device registry because the standard child-to-parent driver binding mechanism does not use the device registry. The bus/nexus driver gives a pointer to its service routines vector and its identifier to the child driver, when the drv_probe() or drv_init() routine of the child driver is invoked.


Once the bus/nexus device is initialized, the bus/nexus driver searches drivers in the driver registry which match the given bus/nexus class and implement the drv_probe() entry point.

The probe routine gives the child driver an opportunity to discover a device (serviceable by that driver) residing on the bus/nexus and to create the device node (associated to this device) in the device tree (see the section "Write the Bind Function" for an example).

A device node specifies bus resource properties required for the associated device. Note that some bus resources may be hardwired (such as fixed interrupt requests (IRQs)), while for other bus resources, some constraints may be specified (such as device address decoder constraints). When presented with configurable constraints, the bus/nexus driver iterates through existing child nodes to allocate configurable resources (with respect to constraints) and to check possible resource conflicts.


Note -

If a resource conflict is detected, the bus/nexus driver behavior is implementation specific. In any case, the bus/nexus driver must not activate any driver on a node for which bus resources are not allocated successfully.


Once the bus resource allocation is done, the bus/nexus driver searches the driver registry for drivers that implement the drv_bind() entry point and that match the given bus/nexus class. Once a driver is found, its drv_bind() routine is invoked (once for each existing child device node). This routine gives the child driver an opportunity to bind itself to the device by attaching to its name.

When the driver-to-device binding is complete, the bus/nexus driver iterates through the child nodes and, for each device node, tries to determine a driver component to apply to the given device. Once a driver component is found, its drv_init() routine is invoked by the bus/nexus driver.

If the child device is not a leaf one, the initalization process is recursively continued by the drv_init() routine of the child driver.

The example below presents a drv_init() routine for a PCI-to-ISA bus driver. The W83C553_DRV_UNLOAD compilation flag is used to allow downsizing of the driver component at compile time, in case the unload mechanism is not needed.

static void
childrenProbeInit (W83c553Data* bus)
{
        /*
         * Call probe methods for both supported bus classes: pci and bus.
         */
    genBusDrvProbe(ISA_CLASS, ISA_VERSION_INITIAL,
                   bus->node, bus->isaOps, bus);
    genBusDrvProbe(BUS_CLASS, BUS_VERSION_INITIAL,
                   bus->node, bus->busOps, bus);

        /*
         * Should check for resource overlapping between devices
         * and allocate each resources through resource_alloc()
         */

        /*
         * Call bind methods for both supported bus classes: pci and bus.
         */
    genBusDrvBind(ISA_CLASS, ISA_VERSION_INITIAL, bus->node);
    genBusDrvBind(BUS_CLASS, BUS_VERSION_INITIAL, bus->node);
        /*
         * Call init methods for both supported bus classes: pci and bus.
         */
    genBusDrvInit(ISA_CLASS, ISA_VERSION_INITIAL,
                  bus->node, bus->isaOps, bus);
    genBusDrvInit(BUS_CLASS, BUS_VERSION_INITIAL,
                  bus->node, bus->busOps, bus);
}

The genBusDrvProbe(), genBusDrvBind() and genBusDrvInit() functions, detailed below, are implemented in the drv_tools.s.a library, not in the driver code itself.

 /*
    * A generic (standard) probing loop performed by a bus driver.
    * Go through the driver registry. Check for each entry whether
    * it matches the bus class and provides a drv_probe() method.
    * If so, apply the drv_probe() method to the bus node.
    */
    void
genBusDrvProbe (char*   bus_class,
                int     bus_version,
                DevNode bus_node,
                void*   bus_ops,
                void*   bus_id)
{
    DrvRegId     drv_curr;
    DrvRegId     drv_prev;
    DrvRegEntry* entry;

        /*
         * Do not perform a probing within the critical initialization 
         * phase.
         */
    if (svDkiInitLevel() == DKI_INIT_LEVEL_CRITICAL) {
        return;
    }

    drv_curr = svDriverLookupFirst();
    while (drv_curr) {
        entry = svDriverEntry(drv_curr);
        if (entry->drv_probe && !strcmp(bus_class, entry->bus_class) &&
            (bus_version >= entry->bus_version)) {
            entry->drv_probe(bus_node, bus_ops, bus_id);
        }
        drv_prev = drv_curr;
        drv_curr = svDriverLookupNext(drv_curr);
        svDriverRelease(drv_prev);
    }
}

   /*
    * A generic (standard) binding loop performed by a bus driver.
    * Go through the driver registry. Check for each entry whether
    * it matches the bus class and provides a drv_bind() method.
    * If so, apply the drv_bind() method to each child node
    * attached to the bus node.
    */
    void
genBusDrvBind (char*   bus_class,
               int     bus_version,
               DevNode bus_node)
{
    DevNode      dev_node;
    DrvRegId     drv_curr;
    DrvRegId     drv_prev;
    DrvRegEntry* entry;

    drv_curr = svDriverLookupFirst();
    while (drv_curr) {
        entry = svDriverEntry(drv_curr);
        if (entry->drv_bind && !strcmp(bus_class, entry->bus_class) &&
            (bus_version >= entry->bus_version)) {
            dev_node = dtreeNodeChild(bus_node);
            while (dev_node) {
                entry->drv_bind(dev_node);
                dev_node = dtreeNodePeer(dev_node);
            }
        }
        drv_prev = drv_curr;
        drv_curr = svDriverLookupNext(drv_curr);
        svDriverRelease(drv_prev);
    }
}

   /*
    * A generic (standard) initialization loop performed by a bus driver.
    * Go through the child device nodes. Check for each node whether
    * it is inactive and has a driver bound to. If so, go through the
    * driver registry. Check for each entry whether it matches the bus
    * class, provides a drv_init() method and matches the driver name.
    * If so, apply the drv_init() method to the child node. The iteration
    * through the driver registry is aborted once the device node becomes
    * active.
    */
    void
genBusDrvInit (char*   bus_class,
               int     bus_version,
               DevNode bus_node,
               void*   bus_ops,
               void*   bus_id)
{
    DevNode       dev_node;
    DrvRegId      drv_curr;
    DrvRegId      drv_prev;
    char*         drv_name;
    DrvRegEntry*  entry;
    DevProperty   prop;    
    PropInitLevel ilevel;

    dev_node = dtreeNodeChild(bus_node);
    while (dev_node) {
            /*
             * Get device initialization level in order to compare it
             * with the current initialization level.
             */
        prop = dtreePropFind(dev_node, PROP_INIT_LEVEL);            
        if (prop) {
            ilevel = *(PropInitLevel*)dtreePropValue(prop);
        } else {
            ilevel = DKI_INIT_LEVEL_CRITICAL;
        }       
        if ((ilevel <= svDkiInitLevel()) &&
            !dtreePropFind(dev_node, PROP_ACTIVE) &&
             dtreePropFind(dev_node, PROP_ALLOCATED)) {
            prop = dtreePropFind(dev_node, PROP_DRIVER);            
            if (prop) {
                drv_name = (char*)dtreePropValue(prop);
                drv_curr = svDriverLookupFirst();
                while (drv_curr) {
                    entry = svDriverEntry(drv_curr);
                    if (entry->drv_init &&
                        !strcmp(bus_class, entry->bus_class) &&
                        (bus_version >= entry->bus_version) &&
                        !strcmp(drv_name, entry->drv_name)) {
                        entry->drv_init(dev_node, bus_ops, bus_id);
                        if (dtreePropFind(dev_node, PROP_ACTIVE)) {
                            svDriverRelease(drv_curr);
                            break;
                        }
                    }
                    drv_prev = drv_curr;
                    drv_curr = svDriverLookupNext(drv_curr);
                    svDriverRelease(drv_prev);
                }
            }
        }
        dev_node = dtreeNodePeer(dev_node);
    }
}  

Write the Unload Function

The drv_unload() is called by the driver registry module (more precisely, by the svDriverUnregister routine) when an application wishes to unload the driver component from the system. The drv_unload() routine is called in the context of the DKI thread.

This makes it possible to invoke directly the bus/nexus and DKI services allowed in the DKI thread context only. The purpose of drv_unload() is to check that the driver component is not currently in use. On success, drv_unload() returns K_OK , otherwise K_EBUSY is returned.

The drv_unload() routine is optional. If drv_unload() is not provided, the driver code cannot be unloaded. drv_unload() is a global, per driver component routine. Therefore, to implement unloading, the driver should handle a list of driver instances. When drv_unload() is called, the driver should go through the list, and for each driver instance, check whether the driver instance is currently in use.


Note -

Once the check is positive (a given instance is not used), the driver instance must become invisible for potential clients. In other words, if drv_unload() returns K_OK , all previously created driver instances (if any) must be deleted and all previously allocated system resources (if any) must be released.


If drv_unload() returns K_EBUSY, the driver component will not be unloaded. In this case, the driver component state must not be changed by drv_unload().

The drv_unload() routine of a bus/nexus driver typically takes the following actions:

  1. Checks that the driver component is not in use.drv_unload() iterates through the driver instances list and, for each driver instance, checks whether a connection is opened to the driver instance.

    Once a driver instance with an open connection is found, the iteration is aborted and K_EBUSY is returned. Otherwise, drv_unload() proceeds to step 2.

  2. Releases resources associated to the driver component.drv_unload() iterates through the driver instances list and, for each driver instance, releases all system resources associated to the instance ( io_unmap , mem_unmap , ...) and, finally, closes the connection to the parent bus. Once the iteration is finished, drv_unload() returns K_OK.

Note that drv_unload() runs in the DKI thread context.

This guarantees stability of the driver instances and open connections during the drv_unload() execution. Indeed, a new driver instance may be created only by the drv_init() routine and a new parent-to-child connection may be opened only by the drv_init() or drv_probe() routines.

Both drv_init() and drv_probe() are invoked in the DKI thread context. Thus, drv_init() and drv_probe() are serialized with drv_unload() by the DKI thread.

On the other hand, if a bus/nexus driver supports hot-pluggable devices, it is up to the bus/nexus driver to implement a synchronization mechanism with a hot-plug insertion interrupt which may occur during the driver unloading.

In the following example, the W83C553_DRV_UNLOAD compilation flag is used to allow downsizing of the driver component, at compile time, if the unload mechanism is not needed.

    /*
     * Unload the W83C553 driver.
     * This routine is called by the driver registry when an application
     * wishes to unload the driver component.
     */
    static KnError
drv_unload ()
{
        /*
         * The driver unloading is an optional feature.
         * In case when such a feature is not supported, drv_unload()
         * just returns K_EBUSY preventing the driver component to be
         * unloaded.
         */
#if defined(W83C553_DRV_UNLOAD)
    W83c553Data*  udev;
        /*
         * Go through the driver instances list and check if it is unused
         * i.e. if the list of connected devices is empty.
         */
    udev = w83c553Devs;
    while (udev && (udev->dev == NULL)) {
        udev = udev->next;
    }
        /*
         * If all driver instances are unused, we invoke shutdown()
         * to shutdown each instance.
         */
    if (!udev) {
        while (w83c553Devs) {
            shutdown(w83c553Devs);
        }
        return K_OK;
    }
        /*
         * If there is a driver instance in use, we cannot unload the
         * driver, and return K_EBUSY.
         */
#endif

    return K_EBUSY;
}

The Event Handler Function

This section describes the events handled by the event handler at the bus level.

The event handler is invoked by the parent bus/nexus driver when an event occurs. The event handler address is given to the parent bus/nexus driver when a connection is established between the bus driver and its parent bus/nexus driver.

The event handler may be called as an interrupt handler and therefore the event handler implementation must be restricted to the API allowed at interrupt level. Among all events which are mostly bus/nexus class specific, there are three shutdown related events (specified by the common bus API) which are discussed in this section:

SYS_SHUTDOWN

system (emergency) shutdown The SYS_SHUTDOWN event notifies the driver instance that the system is going to be shut down. The parent bus/nexus driver requires the child driver instance to perform an emergency shutdown of the device hardware.

DEV_SHUTDOWN

normal device shutdown The DEV_SHUTDOWN event notifies the driver instance that a normal device shutdown is requested by the bus/nexus driver.

DEV_REMOVAL

surprise device removalThe DEV_REMOVAL event notifies the driver instance that the associated device has been removed from the bus/nexus and therefore the driver instance has to be shutdown.


Note -

The omitted prefix ( DKI_ , BUS_ , ...) means that the event semantics are the same for all such events.


In general, the shutdown event processing goes through three phases:

  1. shutdown prolog

  2. shutdown mode

  3. shutdown epilog

The first phase (called shutdown prolog) is processed synchronously within the event handler. The main purpose of the shutdown prolog is to notify child drivers that the shutdown event has occurred (to propagate the shutdown prolog downstream, from parent to child). Once the shutdown event is processed by child drivers, the driver begins the shutdown epilog. The shutdown epilog is invoked when the last connection to the driver instance is closed. Between the shutdown prolog and epilog, the driver operates in a special mode (called shutdown mode). In shutdown mode, the driver accepts only a subset of operations from child drivers, allowing connections to the driver to be closed correctly.

The table below shows typical actions taken by the shutdown prolog depending on the event type:

 ActionSYS_SHUTDOWNDEV_SHUTDOWNDEV_REMOVAL
 notify child drivers (by calling children event handler) + + +
 abort operations in progress  - - +
 reset hardware  + - -

The SYS_SHUTDOWN prolog of a bus/nexus driver invokes the event handlers of child drivers connected to it. Once the invocation is done, the bus/nexus driver puts the hardware into a clean state.

Note that the SYS_SHUTDOWN event is processed synchronously, (that is, within the event handler). The only purpose of the SYS_SHUTDOWN event is to put the board hardware into a clean state to perform the system reboot (or restart) correctly.

The DEV_SHUTDOWN prolog notifies child drivers that the DEV_SHUTDOWN event has occurred. The actual device shutdown is deferred until the DEV_SHUTDOWN epilog.

The DEV_REMOVAL prolog is closed to the DEV_SHUTDOWN prolog. In addition, the DEV_REMOVAL prolog aborts all I/O operations in progress, because otherwise these operations will never be completed. Aborted operations return an error code to callers.

As soon as the shutdown prolog is processed, the driver changes its internal state to enter into shutdown mode. In shutdown mode, the driver accepts only a subset of operations from child drivers:

All other operations (like opening a new connection, or starting an I/O operation) are refused by the driver. When in shutdown mode, the driver must wait until a shutdown epilog condition is met. The shutdown epilog condition is met when the last connection to the driver's instance is closed. Because the shutdown epilog is processed in the DKI thread context, it can call all services and avoid any synchronization issues, even DKI services which can only be called in the DKI thread.

The following table shows typical actions taken by the shutdown epilog by event type.

 ActionDEV_SHUTDOWNDEV_REMOVAL
 reset hardware + -
 release system resources + +
 close connection to the parent driver  + +

The DEV_SHUTDOWN epilog puts hardware into a clean state, releases system resources used by the driver instance (io_unmap, mem_unmap, ...) and, finally, closes connection to the parent driver (close).

When a shutdown epilog closes the last connection to the parent driver, the shutdown epilog condition may be met in the parent driver too. In such a way, the shutdown epilog is propagated upstream (from child to parent). Note that if one of the child drivers does not shut down properly, the driver may get lost in the shutdown mode forever, and never meet the shutdown epilog condition.

In the following example of a PCI bus events handler of a PCI-to-ISA bridge driver, the W83C553_DEV_REMOVAL compilation flag is used to allow downsizing of the driver component at compile time in case the device removal mechanism is not needed.

    /*
     * The event handler is invoked by the parent driver (i.e. PCI bus) 
     * when an event occurs.
     *
     * The W83C553 driver always supports the PCI_SYS_SHUTDOWN and
     * PCI_DEV_SHUTDOWN events. The PCI_DEV_REMOVAL support is optional and
     * is provided only when W83C553_DEV_REMOVAL is defined.
     */ 
    static KnError
pciEventHandler (void* cookie, PciBusEvent event, void* arg)
{
    KnError      res     = K_OK;
    W83c553Data* w83c553 = (W83c553Data*)cookie;
    IsaDev*      isaDev;

    switch (event) {
        /*
         * Mask all ISA interrupts.
         * Then propagate the event to connected device drivers
         * and disconnect the controller from parent bus.
         */
    case PCI_SYS_SHUTDOWN: {
        w83c553->pciIntrOps->mask(w83c553->pciIntrId);
        isaDev = w83c553->dev;
        while (isaDev) {
            if (isaDev->evtHandler) {
                 isaDev->evtHandler(isaDev->cookie,
                      ISA_SYS_SHUTDOWN, arg);
            }
            isaDev = isaDev->next;
        }
        w83c553->pciConfOps->store_16(w83c553->pciConfId, PCI_COMMAND, 0);
        break;
    }
        /*
         * The normal device shutdown is processed only from the
         * normal mode. In other words, this event is
         * ignored if the driver already operates in the device
         * shut-down or removal mode.
         *
         * Here, we just flag that the device is entered into shutdown
         * mode and notify child drivers about it.
         * The real shutdown procedure will be done by the last call to
         * close().
         */
    case PCI_DEV_SHUTDOWN: {
        if (! w83c553->evtState) {
            w83c553->evtState = PCI_DEV_SHUTDOWN;
            isaDev = w83c553->dev;
					while (isaDev) {
						if (isaDev->evtHandler) {
							isaDev->evtHandler(isaDev->cookie, 
                        ISA_SYS_SHUTDOWN, arg);
						}
            	isaDev = isaDev->next;
        		 	}
            DKI_MSG(("%s: entered into shut-down mode\n", w83c553->path));
        }
        break;
    }
#if defined(W83C553_DEV_REMOVAL)
        /*
         * The device removal is processed from either the
         * normal mode or shutdown mode. In other words,
         * this event is ignored if the driver already operates in the
         * device removal mode.
         *
         * Here, we flag that the device is entered into removal
         * mode. In addition, the device ops (isa and bus) are
         * substituted to empty routines in order to avoid to access
         * the hardware which has been disappeared from the bus.
         * Once ops are substituted, we propagate the event to the
         * connected ISA drivers.
         * The real shutdown procedure will be done by the last call
         * to close().
         * removal() is called in order to abort any operation
         * in progess. 
         *
         * Note that, receiving ISA_DEV_REMOVAL, the driver client must
         * update pointers to the device service routines (ops) if they
         * have been previously copied by the client.
         */
    case PCI_DEV_REMOVAL: {
        if (w83c553->evtState != PCI_DEV_REMOVAL) {
            w83c553->evtState = PCI_DEV_REMOVAL;
            w83c553->isaOps->open           = (KnError(*)())downError;
            w83c553->isaOps->intr_attach    = (KnError(*)())downError;
            w83c553->isaOps->io_map         = (KnError(*)())downError;
            w83c553->isaOps->mem_map        = (KnError(*)())downError;
            w83c553->isaOps->dma_attach     = (KnError(*)())downError;
            w83c553->isaOps->resource_alloc = (KnError(*)())downError;
            w83c553->busOps->open           = (KnError(*)())downError;
            w83c553->busOps->intr_attach    = (KnError(*)())downError;
            w83c553->busOps->io_map         = (KnError(*)())downError;
            w83c553->busOps->mem_map        = (KnError(*)())downError;
            isaDev = w83c553->dev;
            while (isaDev) {
                isaDev->evtHandler(isaDev->cookie, ISA_DEV_REMOVAL, arg);
                isaDev = isaDev->next;
            }
            removal(w83c553);
            DKI_MSG(("%s: entered into removal mode\n", w83c553->path));
        }
        break;
    }
#endif
        /*
         * W83C553 does not drive the SERR# pin (p 55)
         * and don't care about palette snoop
         */
    default:
        res = K_ENOTIMP;
    }

    return res;
}

Hot-Plug Removal

The hot-plug removal event is typically reported via interrupts.

To be notified when the hot-plug removal occurs, the bus driver connects an interrupt handler to an appropriate interrupt source. Consider two typical mechanisms of hot-plug removal:

surprise removal

Surprise removal means a device can be removed at any time with no warning. For instance, PCMCIA is a surprise removal device.

non-surprise removal

Non-surprise removal means that the device cannot be removed until the system is prepared for it. For instance, Hotswap CompactPCI is a non-surprise removal device.

Surprise Removal

The surprise removal interrupt notifies the bus driver that a device has been removed from the bus. The bus driver interrupt handler usually detects the removed device (and associated device node) using bus specific status register(s).

Once the device is detected, the interrupt handler checks whether the device node is active. If the device node is inactive (there is no driver instance servicing the device), the only task of the bus driver is to update the device tree removing the device node. This frees all bus resources associated with the node .


Note -

The bus driver is not able to accomplish this task immediately at interrupt level because the services used are typically not available at interrupt level. These types of services can typically be called in the DKI thread context only.


To satisfy the invocation context requirements, the bus driver calls svDkiThreadTrigger requesting the DKI thread to invoke the removal procedure. Using the DKI thread also allows you to serialize all actions related to initialization and termination operations.

If the device node is active, the bus driver must shut down the corresponding device driver instance prior to invoking the removal procedure. To accomplish this task, the bus driver invokes the device driver event handler signaling the DEV_REMOVAL event. In fact, the bus driver performs the shutdown prolog for the given driver instance (see the "The Event Handler Function" section). In other words, the bus driver initiates the shutdown process for the given device sub-tree. (The removed device node is the root of the sub-tree.)

As the last action in the shutdown event process, the child device driver closes the connection to the bus driver and, at this moment, the bus driver performs the removal procedure. Note that the removal procedure is executed in the DKI thread context because the close service routine is called in the DKI thread context.

Non-Surprise Removal

The non-surprise removal interrupt requests the bus driver to enable the device removal from the bus. This is discussed at length above (in the "Surprise Removal" section). The difference between surprise and non-surprise removal is that in non-surprise removal, the bus driver requests the normal device shutdown service (DEV_SHUTDOWN) rather than the device removal service ( DEV_REMOVAL).

In addition, once the device tree is updated, the bus driver enables the device removal. Device removal enabling is usually signaled by an LED, and/or by a card ejection mechanism.

Write Load Handler Function

The load handler function resides in the bus driver, and is invoked by the parent bus/nexus driver when a new driver appears in the system (as when a new driver is downloaded at run time). The load handler address is given to the bus/nexus driver when a connection is established between the child driver and its parent bus/nexus driver.


Note -

The load handler is optional. Load handler functions are usually used by a bus-to-bus bridge which needs to apply a newly downloaded driver to its child devices.


The actions taken by the load handler are essentially the same as those in the initialization process, even though the bus/nexus device is already initialized:

  1. child device node creation (enumeration/probing)

  2. resource allocation for the newly created child nodes

  3. driver-to-device binding

  4. driver instance creation for non-active child device nodes

  5. child load handler invocation

Firstly, the bus/nexus driver invokes the probe routines registered in the driver registry matching the bus/nexus class. The probing loop example given in "Write the Probe Function" may be used as-is within the load handler. Note that the probe routines already invoked at initalization time will probably be invoked again.

Note that (as described in the "Write the Probe Function" section) a probe routine must be aware of existing device nodes to avoid the creation of redundant nodes. In addition, a probe routine must explicitly ask for bus resources (being used for probing) to avoid conflicts with bus resources currently in use.

In this way, the active device nodes and associated running driver instances are protected against any disturbance caused by run-time probing. The probing process may create new child device nodes because a new probe routine (implemented by a newly downloaded driver) may be executed and, as a consequence, it may discover a device previously unknown in the system.

For this reason, the bus/nexus driver has to check/allocate the bus resources required for these device nodes. Note that to satisfy this run-time resource request, the bus/nexus driver may need to confiscate resources already assigned to existing device nodes.

The bus/nexus driver is not allowed to confiscate resources in use (resources assigned to active device nodes). Driver instances associated with an active node must be shutdown by the bus/nexus driver before they can be re-allocated. To shut down a driver instance, the bus/nexus driver sends a shutdown event to the child driver, requesting closure of the child-to-parent driver connection (by invoking close).

Once the connection is closed, the resources are freed (and may be re-allocated). The bus/nexus driver should start the driver instance again, invoking the driver drv_init() routine.

Once the bus resource allocation is done, the bus/nexus driver calls the drv_bind() routines registered in the driver registry for each inactive device node (as explained in the "Write the Bind Function" section).

Once the driver-to-device binding is done, the bus/nexus driver iterates through the child nodes and, for each inactive device node, determines the driver component to be applied to the given device. Once a driver component is found, the bus/nexus driver calls the driver drv_init() routine (see the "Write the Init Function" section).

Finally, the bus/nexus driver invokes the child load handlers (if any) to propagate the loading process downstream (from parent to child). In this way, the loading process is recursively continued by the child driver load handler. The load handler is called in the context of DKI thread. This means that it can call all services, without worrying about synchronization, even those DKI services which can only be called in the DKI thread.

The following example shows the load handler of the PCI-to-ISA bridge driver.

    /*
     * The load handler is invoked by the parent driver (i.e. PCI bus) 
     * when a new driver has been registered in the system (e.g. a
     * loadable driver has been downloaded).
     */ 
    static void
pciLoadHandler (void* cookie)
{
    W83c553Data* w83c553 = (W83c553Data*)cookie;
    IsaDev*      isaDev;
        /*
         * Perform inactive children probing / initialization to start
         * instances of the new loaded driver
         */
    childrenProbeInit(w83c553);
        /*
         * Propagate to all load handlers connected by the device drivers.
         */
    isaDev = w83c553->dev;
    while (isaDev) {
        if (isaDev->loadHandler) {
            isaDev->loadHandler(isaDev->cookie);
        }
        isaDev = isaDev->next;
    }
}