ChorusOS 5.0 Board Support Package Developer's Guide

Chapter 10 Writing a New Device Driver

This chapter takes a procedural approach to writing device drivers, using as its example a leaf device driver. It follows a step-by-step, tutorial approach, explaining at each step the Driver Framework APIs and mechanisms involved.

The ChorusOS operating system Device Driver Framework provides numerous device and bus drivers as part of its installed package. It is recommended that you take a look at the provided drivers as an example of how to build your own (see source_dir/nucleus/bsp/src. For information on the drivers provided with this release, as well as the location of the code and header file for the drivers, see the 9DRV library of the man pages.

Driver Source File Structure

The driver component source tree is organized as follows:

Figure 10-1 New Driver Source Tree

Graphic

The new driver source tree can either be created within the ChorusOS source tree, or in another location. If you create it in the ChorusOS source tree, it will automatically be part of the standard DRV component of ChorusOS (see "Building a ChorusOS System Image with a New BSP/DRV Component"). However, if you create it elsewhere then you will need to add a new MYDRV component when you configure your system image in your build_dir:

% cd build_dir
% configure -b $TOOLS $KERNEL -s $DRV $BSP $MYDRV

Creating a New Device Driver Component

This section shows how a new driver can be created through the example of a non-existent keyboard chip 'zl345y'. The example explains which files should be present in which directories in order to create a new driver.

The Top Level Driver Directory

For this example, the new directory will be called MYDRV_DIR. This root-level directory must contain the following two files:


Example 10-1 New driver: Makefile.bin

COMPONENT += MYDRV

MYDRV.all::

The COMPONENT macro defines a list of components that will be processed in the build phase. The MYDRV source component is added to this list.


Example 10-2 New driver: Makefile.src

MYDRV_SRC = $(MYDRV)/src

all:: MYDRV.all 

MYDRV.all:: $(MYDRV_DIR)/DONE

$(MYDRV_DIR)/DONE : $(MYDRV_DIR)/Makefile
        cd $(MYDRV_DIR); $(MAKE)
        touch $(MYDRV_DIR)/DONE

$(MYDRV_DIR)/Makefile: $(MYDRV_SRC)/Imakefile
        sh $(DEVTOOLS_DIR)/ChorusOSMkMf $(BUILD_DIR)
                         -s $(MYDRV_SRC) -b $(MYDRV_DIR) -d $(MYDRV_DIR)
        cd $(MYDRV_DIR); $(MAKE) Makefiles

This makefile defines the MYDRV.all target used to compile and link the MYDRV source files. The ChorusOSMkMf tool is used to create Makefiles from Imakefiles (see the ChorusOSMkMf(1CC) manpage for more details). The MYDRV_DIR variable is automatically set to $(BUILD_DIR)/build-MYDRV. This directory will be used as the build directory, for example where sources are compiled and linked. It will be used also as the binary delivery directory, for example where include files and binaries are exported.


Note -

These two files, Makefile.bin and Makefile.src , are referenced in the $BUILD_DIR/Makefile, which resides at the root of the build directory. The contents of both files should not be changed.


The src Driver Directory

This src directory must contain the following two files:


Example 10-3 New driver: Project.tmpl

#include "Package.rules"

SRC_DIR         = SourceDir
BUILD_DIR       = BuildDir
DIST_DIR        = DistDir

VPATH           = $(SRC_DIR)$(REL_DIR)

WARN            = $(WARN_ON)
DEBUG           = $(DEBUG_ON)

DRV_DIST_INC    = $(DIST_DIR)/include/chorus/drv

DRV_DIST_BIN    = $(DIST_DIR)/bin/drv

INCLUDES        = -I$(NUCLEUS_DIR)/include/chorus \
                  -I$(NUCLEUS_DIR)/include/stdc \
                  -I$(DIST_DIR)/include/chorus \
                  -I$(DIST_DIR)/include/chorus/drv/private \
                  -I$(SRC_DIR)$(REL_DIR)

CPU_LIB         = $(NUCLEUS_DIR)/lib/cpu/cpu.s.a
EBD_LIB         = $(NUCLEUS_DIR)/lib/embedded/libebd.s.a

DRV_LIBS        = $(CPU_LIB) $(EBD_LIB)

The DRV_DIST_INC and DRV_DIST_BIN variables define paths where includes and binaries will be exported. This Project.tmpl file can be modified to add new variables or modify existing ones, for example to add a new include path or add a new library (DRV_LIBS).


Example 10-4 New driver: Imakefile

#define IHaveSubdirs

SUBDIRS = keybrd

This Imakefile lists all of the sub-directories for each driver class and needs updapting to cater for your own drivers.

The Driver Directory

In this example, for a keyboard driver, the driver class directory would be MYDRV_SRC_DIR/src/keybrd, along with all other keyboard drivers.

The keybrd.h include file defines the DDI interface of this new driver. This include file must be exported to a well-known place in order for it to be accessible to a client of this driver. For example:

#include <ddi/keybrd/keybrd.h>

For this reason, a DistFile macro appears in the Imakefile, as follows:

#define IHaveSubdirs  
SUBDIRS = zl345y  
DistFile(keybrd.h, $(DIST_DIR)/include/chorus/ddi/keybrd) 

The Imakefile also contains a list of keyboard drivers for different chips.

The Chip Directory

The actual driver for the zl345y keyboard device would be located in MYDRV_SRC_DIR/src/keybrd/zl345y and consists of four files:

The Imakefile contains the following:

CSRCS = zl345y.c

OBJS = $(CSRCS:.c=.o) 

BuiltinDriver(D_zl345y, $(OBJS), $(DRV_LIBS))

DistProgram(D_zl345y, $(DRV_DIST_BIN)$(REL_DIR))

Depend($(CSRCS))

DistFile(zl345yProp.h, $(DRV_DIST_INC)$(REL_DIR))

The BuiltinDriver macro is used to produce the D_zl345y relocatable binary. The final link will be done by the mkimage tool. The first argument is the name of the binary. The second argument is the list of objects and the third is the list of libraries used in the link process.

The DistProgram macro copies the file, given as its the first argument, to the directory specified by its second argument. In the above example, the D_zl345y binary will be copied to the $MYDRV_DIR/bin/drv/keybrd/zl345y directory.

The zl345yProp.h file is exported as code by the DistFile macro to allow other drivers to include it.

Adding the New Driver

Finally, the driver must be added to the ChorusOS operating system. This can be done by either:

To include the new driver in the ChorusOS operating system image you will need to add the new driver's definition to the BSP target.xml file, as follows:

<definition name='zl345y'>
  <type name='File' />
  <value field='path'>
    <vstring>${MYDRV_DIR}/bin/drv/keybrd/zl345y/D_zl345y</vstring>
  </value>
  <value field='bank'><ref name='sys_bank' /></value>
  <value field='binary'><ref name='driver_model' /></value>
</definition>

Also, a reference to the new driver will need be to be made in the BSP FileList:

<definition name='BSP_files'>
  <description>system image BSP files</description>
  <type name='FileList' />
   ....
  <value index='size'><ref name='zl345y' /></value>
   ....
</definition>

For more information on this file see "Files")

Alternatively, to add the driver dynamically, after the system image has been created, the arun command should be used to run the new driver.

Including the Appropriate APIs (DKI/DDI)

As the first step, include the header files for the DKI and DDI APIs involved in the device driver's implementation. A device driver mainly uses its parent bus DDI API and some generic DKI services such as memory allocator.

The driver implementation must include:

Here is an example for a NS16x50 compatible UART device driver that uses DKI and "Common bus driver" APIs, and provides "UART device driver" DDI API.

#include 		<dki/dki.h> 
#include 		<ddi/bus/bus.h>
#include 		<ddi/uart/uart.h>
...
#include 		"ns16550.h"
#include 		"ns16650Prop.h"

The Driver main() Routine

This section describes how to write your main() routine which is called by the system once the driver component is downloaded, so that the driver component is self-registered within the ChorusOS system.

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 is called by the system once the driver component is downloaded. The only task of the driver main routine is to perform the self-registration of the driver component within the ChorusOS 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, the future driver management is largely undertaken by its parent bus/nexus driver using the properties specified in the DrvRegEntry structure.

DrvRegEntry specifies four driver entry points as follows:

drv_probe()

invoked by the parent bus/nexus driver when bus_class specified in the registry entry matches the parent bus/nexus driver class

drv_bind()

invoked by the parent bus/nexus driver when bus_class specified in the registry entry matches the parent bus/nexus driver class

drv_init()

invoked by the parent bus/nexus driver when bus_class specified in the registry entry matches the parent bus/nexus class and there is a node in the device tree to which the driver is bound (a hardware device to be managed by the driver exists)

drv_unload()

invoked by the driver registry module when an application wishes to unload the driver component from the system

    /*
     * NS16550 driver registry entry.
     * 
     * The driver requires the common bus driver interface
     * implemented by the parent bus driver. Thus, the driver
     * should work on any bus providing such an API (e.g. ISA,
     * PCI, PCMCIA).
     *
     * Note that the driver does not provide any probe routine because
     * the probing mechanism is bus class specific. Thus, if one wants
     * to implement the NS16550 device probing, it may either add probe
     * routine(s) to the driver code or implement probe-only driver(s)
     * for NS16550 device.      
     */
static DrvRegEntry ns16_drv = {
    NS16_DRV_NAME,		  			   	/* drv_name    */
    "NS16x50 UART driver [#ident \"@(#)ns16550.c 1.16 99/02/16 SMI\"]",
    BUS_CLASS,									/* bus_class   */
    BUS_VERSION_INITIAL,					/* bus_version */
    NULL,											/* drv_probe   */
    NULL,											/* drv_bind   */
    ns16_init,									/* drv_init    */
    ns16_unload								/* drv_unload  */
};

    /*
     * Driver main() routine.
     * Called by microkernel at driver initialization time.
     */
    int
main ()
{
    KnError res;
			/*
			 * Register the driver component in the driver registry.
			 */
    res = svDriverRegister(&ns16_drv);
    if (res != K_OK) {
		  	  DKI_ERR(("%s: error -- svDriverRegister() failed (%d)\n",
	                 NS16_DRV_NAME, res));
	  }
	  return res;
}

The Device Driver Class Specific Functions

In this next step, you will write an implementation of the services specific to the driver device class for a hardware device of the given class.

Once the code is written, these functions must be made available to the device driver's clients.


Note -

None of these functions are directly exported, but all of them are defined as static functions, and then grouped as indirect calls in an operations data structure (typed by the device class API). The device driver component then provides this operations data structure as a property when registering an instance of itself at initialization time.

This way of providing the device driver operations allows for a dynamic binding mechanism between device drivers and driver clients.


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

The following code example illustrates this step for an NS16x50 compatible UART device driver. In this example, the provided device class API is for the UART device class.


Note -

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


    /*
     * Open device.
     */
    static int
ns16_open (UartId id, ...) { ... }
    /*
     * Disable device interrupts.
     */
    static void
ns16_mask (UartId id) { ... }
    /*
     * Enable device interrupts.
     */
    static void
ns16_unmask (UartId id) { ... }
    /*
     * Send a buffer.
     */
    static void
ns16_transmit (UartId id, ...) { ... }
    /*
     * Abort an output in progress.
     */
    static void
ns16_abort (UartId id) { ... }
    /*
     * Send a break
     */
    static void
ns16_txbreak (UartId id) { ... }
    /*
     * Set/reset the modem control signals.
     */
    static void
ns16_control (UartId id, ...) { ... }
    /*
     * Set receive buffer.
     */
    static void
ns16_rxbuffer (UartId id, ...) { ... }
    /*
     * Close device.
     */     
    static void
ns16_close (UartId id) { ... }
    /*
     * NS16550 service routines:
     */
static UartDevOps ns16_ops =
{
    UART_VERSION_INITIAL,
    ns16_open,
    ns16_mask,
    ns16_unmask,
    ns16_transmit,
    ns16_abort,
    ns16_txbreak,
    ns16_control,
    ns16_rxbuffer,
    ns16_close
};
    /*
     * Init the NS16x50 uart. Called by BUS driver.
     */
    static void
ns16_init (DevNode node, void* pOps, void* pId)
{
    BusOps*       busOps = (BusOps*)pOps;
    Ns16_Device*  dev;
    ...
        /*
         * Allocate the device descriptor
         * (i.e. the driver instance local data)
         */
    dev = (Ns16_Device*)svMemAlloc(sizeof(Ns16_Device));
    ...
    dev->entry.dev_class = UART_CLASS;
    dev->entry.dev_id    = dev;
    dev->entry.dev_node  = node;
    ...
    bcopy(&ns16ops, &(dev->myops), sizeof(UartDevOps));
    dev->entry.dev_ops = &(dev->myops);
         /*
         * 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);
    ...
        /*
         * 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));
}

The Driver Registry

This section describes the implementation of the standard driver entry point routines: Probe, Bind, Init and Unload.

Write the Probe Routine

The purpose of the drv_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 Routine

The drv_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 can 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 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.

Write the Init Routine

The initialization routine of a device driver component is optional. If 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 callback handlers that 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 available the bus hardware resources needed for the device.


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 can find it in the registry (using the device registry API) and can 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.

    /*
     * 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.
         */
    bcopy(&ns16ops, &(dev->myops), sizeof(UartDevOps));
    dev->entry.dev_ops = &(dev->myops);

        /*
         * 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.
         */
    dev->next = ns16_devs;
    ns16_devs = dev;    

        /*
         * 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 the Unload Routine

The drv_unload() routine 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 yellow 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() routine 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 example, 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.

    /*
     * 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 ()
{
    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
         * shut it down. 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;
    }

    return K_EBUSY;
}

Bus Event Handler Function

The event handler is invoked by the parent bus/nexus driver when a bus/nexus event occurs. The event handler address is given to the bus/nexus driver when a connection is established between the child 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 removal

The 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 shut down.


Note -

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


In the standard case, the shutdown event processing goes through three phases:

  1. shutdown prolog

  2. shutdown mode

  3. shutdown epilog

The shutdown prolog phase is processed synchronously within the event handler. The main purpose of the shutdown prolog is to notify driver clients that the shutdown event has occurred, in other words, to propagate the shutdown prolog downstream (from driver to clients).

Once the shutdown event is processed by driver clients, the driver enters the shutdown epilog phase. Basically, the shutdown epilog is invoked when the last reference on the driver instance goes away. Between the shutdown prolog and epilog, the driver operates in a special mode (called shutdown mode). In this mode, the driver accepts only a subset of operations from clients allowing proper closure of connections to the driver.

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

 ActionSYS_SHUTDOWNDEV_SHUTDOWNDEV_REMOVAL
 notify driver clients (with svDeviceEvent) - + +
 abort operations in progress  - - +
 reset hardware  + - -

The SYS_SHUTDOWN prolog of a leaf (device) driver does not notify driver clients about the system shutdown event. The driver simply puts the hardware into a clean state.

Note that the SYS_SHUTDOWN event is processed synchronously, within the event handler. In other words, the system shutdown epilog is empty.

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 simply notifies the driver clients that the DEV_SHUTDOWN event has occurred. Actual shutdown of the device is deferred until the DEV_SHUTDOWN epilog.

The DEV_REMOVAL prolog is similar to the DEV_SHUTDOWN one. In addition, the DEV_REMOVAL prolog aborts all I/O operations in progress (otherwise, these operations would never be completed).

Aborted operations return to callers with an error code.

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

All other operations (like opening a new connection, starting an I/O operation) are refused by the driver. In other words, in shutdown mode, the driver is waiting until a shutdown epilog condition is met. This enables clients to close existing connections to the driver correctly. The shutdown epilog condition is met within a leaf device driver when the device entry is released by the last driver client, and the callback release handler is invoked by the device registry.


Note -

The callback release handler is called in the DKI thread context. Therefore, the shutdown epilog is processed in the DKI thread context. This makes it possible to directly invoke the parent bus/nexus and DKI services allowed in the DKI thread context.


The table below shows typical actions taken by the shut-down epilog depending on the 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).

The DEV_REMOVAL epilog is similar to the DEV_SHUTDOWN one, except the device hardware is not touched by the driver because the device hardware is no longer present on the parent bus.

When a shutdown epilog closes the last connection to the parent bus 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 -

If one of the driver clients does not implement the shutdown procedure properly (for example, if it simply does not support the shutdown), the driver may be caught in shutdown mode forever. This type of driver would then never meet the shutdown epilog condition.


In the following example, the bus events management code from the NS16x50 compatible UART device driver is shown.

    /*
     * ns16_down_xxx stubs are used in the device removal mode in order
     * to avoid to access the device hardware.
     */

    static void
ns16_down (UartId id)
{
}

#define ns16_down_mask          ns16_down
#define ns16_down_unmask        ns16_down
#define ns16_down_txbreak       ns16_down

    /*
     * Open device.
     */
    static int
ns16_down_open (UartId        id,
                UartConfig*   cfg,
                void*         cookie,
                UartCallBack* client_ops,
                uint32_f*     signals)
{
    return K_EFAIL;
}

    /*
     * Send a buffer.
     */
    static void
ns16_down_transmit (UartId      id,
                    uint8_f*    buffer,
                    uint32_f    count)
{
}

    static void
ns16_down_control (UartId   id,
                   uint32_f signals)
{
}

static UartDevOps ns16_down_ops =
{
    UART_VERSION_INITIAL,
    ns16_down_open,
    ns16_down_mask,
    ns16_down_unmask,
    ns16_down_transmit,
    ns16_abort,
    ns16_down_txbreak,
    ns16_down_control,
    ns16_rxbuffer,
    ns16_close
};
    /*
     * This routine aborts a pending operation (if any) on the device.
     * The driver client is notified by a call-back handler about
     * the operation failing.
     *
     * This routine is only used by the hot-plug management code
     * in order to abort an operation in progress when a hot-plug removal 
     * occurs.
     */
    static void
ns16_removal (Ns16_Device* dev)
{
    if (dev->tx_csize || dev->tx_signals) {
        uint32_f count   = dev->tx_isize - dev->tx_csize;
        uint32_f signals = dev->tx_signals | UART_SIG_TX_ABORTED;
        dev->tx_cbuff    = NULL;
        dev->tx_csize    = 0;
        dev->tx_isize    = 0;
        dev->tx_signals  = 0;
        dev->clientOps->txdone(dev->cookie, count, signals);
    }
}


    /*
     * NS16550 event handler
     *
     * The event handler is invoked by the parent bus driver when a bus
     * event occurs in the bus.
     *
     * The NS16550 UART driver always supports the BUS_SYS_SHUTDOWN and
     * BUS_DEV_SHUTDOWN events. The BUS_DEV_REMOVAL support is optional and
     * is provided only when NS16_DEV_REMOVAL is defined.
     */
    static KnError
ns16_event (void*    id,
            BusEvent event,
            void*    arg)
{
    KnError      res  = K_OK;
    Ns16_Device* dev  = NS16_DEV(id);
    DevNode      node = dev->entry.dev_node;

    switch (event) {
            /*
             * In case of system (emergancy) shutdown,
             * we only disable the device interruts, in order to
             * properly perform the system reboot (or restart).
             *
             * Note that the mask() routine is invoked indirecty because 
             * it may be substituted by the event handler.
             */
        case BUS_SYS_SHUTDOWN: {
            UART_OPS(dev->entry.dev_ops)->mask(id);
            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 (dev->devEvent) and ask the device registry to 
             * notify clients about it. The real shutdown procedure 
             * will be done by the ns16_release() handler. This handler
             * is called by device registry when the the reference to the
             * driver instance goes away (i.e. when svDeviceRelease() is
             * called by client).
             */
        case BUS_DEV_SHUTDOWN: {
            if (dev->devEvent == DEV_EVENT_NULL) {

                dev->devEvent = DEV_EVENT_SHUTDOWN;
                svDeviceEvent(dev->regId, DEV_EVENT_SHUTDOWN, NULL);

                DKI_MSG(("%s: entered into shut-down mode\n", dev->dpath));
            }
            break;
        }

            /*
             * 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 (dev->devEvent). In addition, the device ops 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 ask the device registry to 
             * notify clients about the device removal event.
             * The real shutdown procedure will be done by the 
             * ns16_release() handler. This handler is called by device 
             * registry when the reference to the driver instance goes 
             * away (i.e. when svDeviceRelease() is called by client).
             * ns16_removal() is called in order to abort a transmission
             * in progess (if any). 
             *
             * Note that, receiving DEV_EVENT_REMOVAL, the driver client 
             * must update pointers to the device service routines (ops) 
             * if they have been previously copied by the client.
             */
        case BUS_DEV_REMOVAL: {
            if (dev->devEvent != DEV_EVENT_REMOVAL) {

                dev->devEvent = DEV_EVENT_REMOVAL;
                bcopy(&ns16_down_ops, &(dev->devOps), 
                       sizeof(ns16_down_ops));
                svDeviceEvent(dev->regId, DEV_EVENT_REMOVAL, NULL);

                ns16_removal(dev);

                DKI_MSG(("%s: entered into removal mode\n", dev->dpath));
            }
            break;
        }

        default: {
            res = K_ENOTIMP;
            break;
        }
    }

    return res;
}

    /*
     * The error handler is called by the parent bus driver
     * if a bus error occurs when accessing the device registers.
     * In the current implementation, we consider that the device
     * is not present on the bus if such an error occurs.
     * Thus, an I/O error is equivalent to the device removal event.
     */
    static void
ns16_bus_error (void*     id,
                BusError* err)
{
    DKI_ERR(("%s: error -- bus error (%d, 0x%x)\n",
             NS16_DEV(id)->dpath, err->code, err->offset));

    (void) ns16_event(id, BUS_DEV_REMOVAL, NULL);
}

    /*
     * ns16_shutdown() implements the real shutdown of a given
     * device driver instance.
     * This routine is called either by ns16_release() or ns16_unload().
     * In both cases, this routine is invoked in the DKI thread context.
     * Note that the device driver instance has been unregistered by the
     * device registry, i.e. the corresponding device registry entry is
     * invalid.
     */
    static void
ns16_shutdown (Ns16_Device* dev)
{
        /*
         * When the driver unloading is supported, we must remove
         * the driver instance from the list.
         */
    Ns16_Device*  cdev;
    Ns16_Device** link = &ns16_devs

    while ((cdev = *link) != dev) {
        link = &(cdev->next);
    } 

    *link = dev->next;
        /*
         * Release bus resources and close connection to the bus.
         */
    dev->busOps->intr_detach(dev->intrId);
    dev->busOps->io_unmap(dev->ioId);
    dev->busOps->close(dev->devId);
        /*
         * Release the device registry entry.
         */
    svDeviceFree(dev->regId);
    DKI_MSG(("%s: %s driver stopped\n", dev->dpath, NS16_DRV_NAME));
        /*
         * Finally, free memory allocated for the device descriptor.
         */
    svMemFree(dev->dpath, dev->dpathLeng);
    svMemFree(dev, sizeof(Ns16_Device));
}

    /*
     * The release handler is called by the device registry when
     * a DEV_EVENT_SHUTDOWN or DEV_EVENT_REMOVAL event has been signaled
     * (via svDeviceEvent()) and the (last) reference to the device driver
     * instance goes away.
     */
    static void
ns16_release (DevRegEntry* entry)
{
    ns16_shutdown(NS16_DEV(entry->dev_id));
}