ChorusOS 4.0 Device Driver Framework Guide

Write Device Driver Registry Functions

Write the Probe Function

The purpose of the 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 cases where 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:

Basically, there are two kinds of probe routines:

A self-identifying bus (such as PCI) enumerator is a typical example of the generic probe routine.

A device probing code on an ISA bus is a typical example of the device specific 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. In the latter case, the probe routines must be extremely careful about active device nodes (existing device nodes for which the device drivers have been already started and may be already 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. 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.

Write the Init Function

The initialization routine of a device driver component is optional. In case it is not provided (NULL entry point), the driver is typically a "probe only" driver.

The initialization process ( drv_init) of a leaf device driver goes through the following steps:

First of all, the driver must establish connection to the parent driver by calling open . In open, the driver specifies call-back handlers which will be used by the parent driver to manage the device instance driver (such as to shutdown the driver instance). In addition, global bus events (such as a catastrophic bus error) are delivered to the driver through a call-back handler.

Once the child-to-parent connection is established, the driver may use services provided by the parent driver. Typically, at this point, the driver asks its parent driver to make the bus hardware resources needed for the device available.


Note -

The bus resources needed for the device are specified as properties in the device node. These resources are already allocated by the parent bus/nexus driver prior to the drv_init invocation. To make a given bus resource available (such as device I/O registers), the driver obtains an appropriate property value ("io-regs") and calls an appropriate bus/nexus service routine (such as io_map).


Once access to the device hardware is allowed, the driver initializes it to an operational state. Once initialized, the driver performs self-registration in the device registry to declare itself as a new device (such as a new device driver instance) within the system.

Once the device (the device driver instance) is registered, a driver client may find it in the registry (using the device registry API) and may perform operations on device-calling driver service routines exported through the device registry entry.


Note -

The driver is not necessarily required to register any device driver instances or offer any device service routines through the device registry. Device driver instance registration is required only for clients that find their devices through the device registry. If other client-to-driver binding mechanisms are in use, the associated devices need not take part in the device registry.


The drv_init routine is called in the context of the DKI thread. This makes it possible to directly invoke the bus/nexus and DKI services allowed in the DKI thread context.

Below is an example of the initialization function of the NS16x50 compatible UART device driver. The NS16_DEV_REMOVAL and NS16_DRV_UNLOAD compilation flags are used to allow downsizing of the driver component at compile time in case these mechanisms are not needed.

    /*
     * Init the NS16x50 uart. Called by BUS driver.
     */
    static void
ns16_init (DevNode node, void* pOps, void* pId)
{
    BusOps*       busOps = (BusOps*)pOps;
    DevProperty   prop;
    Ns16_Device*  dev;
    void*         ioRegs;
    void*         intr;
    KnError       res;
    char*         dpath;
    int           dpathLeng;

    dpathLeng = dtreePathLeng(node);
    dpath     = (char*) svMemAlloc(dpathLeng);
    if (!dpath) {
        DKI_ERR(("%s: error -- no enough memory\n", NS16_DRV_NAME));
        return;
    }
    dtreePathGet(node, dpath);

        /*
         * Allocate the device descriptor
         * (i.e. the driver instance local data)
         */
    dev = (Ns16_Device*)svMemAlloc(sizeof(Ns16_Device));
    if (dev == NULL) {
        DKI_ERR(("%s: error -- no enough memory\n", dpath));
        return;
    }

    bzero(dev, sizeof(Ns16_Device));

    dev->dpath           = dpath;
    dev->dpathLeng       = dpathLeng;
    dev->busOps          = busOps;
    dev->devEvent        = DEV_EVENT_NULL;
    dev->entry.dev_class = UART_CLASS;
    dev->entry.dev_id    = dev;
    dev->entry.dev_node  = node;

        /*
         * If the hot-plug removal is supported, the device driver ops
         * are located within the device descriptor. It makes possible
         * to substitute the service routines (to an empty implementation) 
         * when a hot-plug removal occurs.
         */
#if defined(NS16_DEV_REMOVAL)
    bcopy(&ns16_ops, &(dev->devOps), sizeof(ns16_ops));
    dev->entry.dev_ops = &(dev->devOps);
#else
    dev->entry.dev_ops = &ns16_ops
#endif

        /*
         * Allocate the device driver instance descriptor in the
         * device registry.
         * Note that the descriptor is allocated in an invalid state
         * and it is not visible for clients until svDeviceRegister()
         * is invoked.
         * On the other hand, the allocated device entry allows the
         * event handler (ns16_event) to invoke svDeviceEvent() on it.
         * If svDeviceEvent() is called on an invalid device entry,
         * the shutdown processing is deferred until svDeviceRegister().
         * In other words, if a shutdown event occurs during the
         * initialization phase, the event processing will be deferred
         * until the initialization is done.
         */
    dev->regId = svDeviceAlloc(&(dev->entry), 
                               UART_VERSION_INITIAL,
                               FALSE, 
                               ns16_release);
    if (!dev->regId) {
        DKI_ERR(("%s: error -- no enough memory\n", dpath));
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }

        /*
         * Retrieve the device I/O base addr from device tree.
         */
    prop = dtreePropFind(node, BUS_PROP_IO_REGS);
    if (prop == NULL) {
        DKI_ERR(("%s: error -- no '%s' property\n", dpath, BUS_PROP_IO_REGS));
        svDeviceFree(dev->regId);
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }
    ioRegs = dtreePropValue(prop);

        /*
         * Retrieve the device interrupt source from device tree.
         */
    prop = dtreePropFind(node, BUS_PROP_INTR);
    if (prop == NULL) {
        DKI_ERR(("%s: error -- no '%s' property\n", dpath, BUS_PROP_INTR));
        svDeviceFree(dev->regId);
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }
    intr = dtreePropValue(prop);

        /*
         * Retrieve the device clock frequency from device tree.
         * (if not specified, the default clock frequency is used)
         */
    prop = dtreePropFind(node, PROP_CLOCK_FREQ);
    if (prop == NULL) {
        dev->clock = NS16_CLOCK_FREQ;
    } else {
        dev->clock = *(PropClockFreq*)dtreePropValue(prop);
    }

        /*
         * Open a connection to the parent bus.
         */
    res = busOps->open(pId, node, ns16_event, NULL, dev, &dev->devId);
    if (res != K_OK) {
        DKI_ERR(("%s: error -- open() failed (%d)\n", dpath, res));
        svDeviceFree(dev->regId);
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }

        /*
         * Map the device I/O registers.
         */
    res = busOps->io_map(dev->devId, ioRegs, ns16_bus_error, dev,
                         &dev->ioOps, &dev->ioId);
    if (res != K_OK) {
        DKI_ERR(("%s: error -- io_map() failed (%d)\n", dpath, res));
        busOps->close(dev->devId);
        svDeviceFree(dev->regId);
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }

        /*
         * Connect interrupt handler to the bus interrupt source
         * (mask interrupts at chip level first).
         *
         * Note that the mask() routine is invoked indirecty because 
         * it may be substituted by the event handler (if a device
         * removal event has been already occured).
         */
    UART_OPS(dev->entry.dev_ops)->mask((UartId)dev);
    res = busOps->intr_attach(dev->devId, intr, ns16_intr, dev,
                              &dev->intrOps, &dev->intrId);
    if (res != K_OK) {
        DKI_ERR(("%s: error -- intr_attach() failed (%d)\n", dpath, res));
        busOps->io_unmap(dev->ioId);
        busOps->close(dev->devId);
        svDeviceFree(dev->regId);
        svMemFree(dev, sizeof(Ns16_Device));
        svMemFree(dpath, dpathLeng);
        return;
    }

        /*
         * If the driver unloading is supported, the list of active
         * device driver instances is handled.
         * Thus, we should add the new driver instance to the list.
         */
#if defined(NS16_DRV_UNLOAD)
    dev->next = ns16_devs;
    ns16_devs = dev;    
#endif

        /*
         * Finally, we register the new device driver instance
         * in the device registry. In case when a shutdown event
         * has been signaled during the initialization, the device entry
         * remains invalid and the ns16_release() handler is invoked
         * to shutdown the device driver instance. Otherwise, the device
         * entry becames valid and therefore visible for driver clients.
         */
    svDeviceRegister(dev->regId);

    DKI_MSG(("%s: %s driver started\n", dpath, NS16_DRV_NAME));
}

Write 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 theyell0w DKI thread. This makes it possible to directly invoke the bus/nexus and DKI services allowed in the DKI thread context.

The purpose of drv_unload is to check that the driver component is not currently in use. For drv_unload to succeed, the driver clients must have closed their connections with the driver and released the device registry lock (svDeviceRelease). On success, drv_unload returns K_OK, otherwise K_EBUSY is returned.

The drv_unload routine is optional. In cases when drv_unload is not provided, the driver code cannot be unloaded.

The 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, should 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 to 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 . For instance, all registered driver instances must be in place.

Consider the driver unloading implementation for a driver using the standard client-to-driver binding mechanism based on the device registry. In cases where another client-to-driver binding mechanism is used, the driver unloading implementation is binding mechanism dependent.

The drv_unload routine of a (leaf) device 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, invokes svDeviceUnregister to remove the driver instance entry from the registry.

    Once svDeviceUnregister fails (returns K_EBUSY ), the iteration is aborted and drv_unload proceeds to step 3. Otherwise (if all device instances are successfully unregistered), 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 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.

  3. Restores the initial state of the driver component.drv_unload iterates through the driver instances list and, for each driver instance which has been unregistered at step 1, invokes svDeviceRegister to register the driver instance again. Once the iteration is finished, drv_unload returns K_EBUSY.

Note that drv_unload runs in the DKI thread context. This guarantees the stability of driver instances during the drv_unload execution. In fact, a new driver instance may be created only by the drv_init routine, which is also invoked in the DKI thread context. In this way, drv_init is serialized with drv_unload by the DKI thread.

The following example shows the unload function of the NS16x50 compatible UART device driver. The NS16_DRV_UNLOAD compilation flag is used to allow downsizing of the driver component, at compile time, in case the driver unload mechanism is not needed.

    /*
     * Unload the NS16x50 uart driver.
     * This routine is called by the driver registry when an application
     * wishes to unload the NS16550 driver component.
     */
    static KnError
ns16_unload ()
{
        /*
         * The driver unloading is an optional feature.
         * In case when such a feature is not supported, ns16_unload()
         * just returns K_EBUSY preventing the driver component to be
         * unloaded.
         */
#if defined(NS16_DRV_UNLOAD)
    Ns16_Device*  dev;
    Ns16_Device*  udev;
        /*
         * Go through the driver instances list and try to unregister
         * all instances. Note that the device registry entry becomes
         * invalid if svDeviceUnregister() returns K_OK. The iteration
         * is aborted if svDeviceUnregister() fails.
         */
    udev = ns16_devs;
    while (udev && (svDeviceUnregister(udev->regId) == K_OK)) {
        udev = udev->next;      
    }
        /*
         * If all driver instances are unregistered successfully,
         * we invoke ns16_shutdown() for each instance in order to
         * shutdown it. Note that some shutdown events may be signaled by the
         * parent bus driver after the device entry has been unregistered.
         * In such a case, these events will be ignored. Indeed, once
         * unregistered, the device registry entry becomes invalid.
         * For invalid device entries, the device registry defers the events
         * processing until svDeviceRegister(). But, the entries will be
         * released (svDeviceFree) by ns16_shutdown() rather than registered
         * again.       
         */
    if (!udev) {
        while (ns16_devs) {
            ns16_shutdown(ns16_devs);       
        }
        return K_OK;
    }
        /*
         * If there is a driver instance in use (i.e. svDeviceUnregister()
         * failed) , the driver component cannot be unloaded. 
         * We must register again the driver instances unregistered above.
         * Note that shutdown events may be signaled by the parent bus driver
         * after the device entry has been unregistered.
         * In such a case, these events will be processed at this moment.
         * Indeed, once unregistered, the device registry entry becomes
         * invalid. For invalid device entries, the device registry defers
         * the events processing until svDeviceRegister().
         */
    dev = ns16_devs;
    while (dev != udev) {
        svDeviceRegister(dev->regId);
        dev = dev->next;
    }
#endif

    return K_EBUSY;
}