ChorusOS 4.0 Device Driver Framework Guide

Write 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 routines:

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 can not 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 libebd.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 instances 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 resources 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_probe routine is invoked (once for each existing child device node). The drv_bind routine gives the child driver an opportunity to bind itsekf 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.

childrenPropbeInit()()

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 libebd.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;

    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;    

    dev_node = dtreeNodeChild(bus_node);
    while (dev_node) {
        if (!dtreePropFind(dev_node, PROP_ACTIVE)) {
            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

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;
}