Sun Java Solaris Communities My SDN Account

Article

Writing a Device Driver in ChorusOS 4.0

 
May 2001  

This article is intended to help in understanding how to write a device driver in ChorusOS 4.0 and ChorusOS 4.0.1. The article explains the basics of the ChorusOS Driver Framework. The framework provides a hierachical set of APIs which define the services provided for use by each bus or device driver. The framework ensures protability and functionality across various platforms.


 

The Driver main() Routine


A driver is a supervisor actor that enables the kernel to manage a specific type of device (for example, a family of Ethernet boards). Each driver is entered in a database maintained by the ChorusOS system called the driver registry. The only role of its main routine is to register a structure in the driver registry using svDriverRegister().

A driver is represented by a DrvRegEntry static variable composed of the following:
 

 char* drv_name;    /* the driver name                                      */
 char* drv_info;    /* extra information such as version or author         */
 char* bus_class;   /* class of the parent driver API required such as "pci"*/
 int   bus_version; /* minimum version of the parent driver required        */
 void (*drv_probe)(DevNode  dev_node,              /* driver probe function */
                  void*    bus_ops,
                  void*    bus_id);
 void (*drv_bind)(DevNode  dev_node);              /* driver bind function  */
 void (*drv_init)(DevNode  dev_node,               /* driver init function  */
                  void*    bus_ops,
                  void*    bus_id);
 void (*drv_unload)();                            /* driver unload function */
 

For example, define my_drv as a PCI class driver as follows:

    /*
     * Driver entry.
     */
 static DrvRegEntry my_drv = {
    MY_DRIVER_DRV_NAME,        /* driver name           */
    "my driver v1.0 (my_drv.c)",
    PCI_CLASS,                 /* parent bus class      */
    PCI_VERSION_INITIAL,       /* minimal bus version   */
    NULL,                      /* probe method          */
    NULL,                      /* bind method           */
    my_drv_init,               /* init method           */
    NULL                       /* unload method         */
 };

    /*
     * Driver main routine (called by kernel at driver startup time)
     */
 int main ()
 {
    KnError res = svDriverRegister(&my_drv);
    if (res != K_OK) {
        DKI_ERR(("%s: error -- svDriverRegister() failed (%d)\n",
                 my_drv.drv_name, res));
    }
    return res;
 }

This should be implemented in nucleus/bsp/drv/src/my_drv/my_drv.c. Constant values such as MY_DRIVER_DRV_NAME can be exported in a header file called my_drvProp.h , which exports all the public properties used to configure this type of driver:

 #ifndef _D_MY_DRVPROP_H
 #define _D_MY_DRVPROP_H

 #define MY_DRIVER_DEV_NAME              "my_device"
 #define MY_DRIVER_DRV_NAME              "my_driver"
 #define MY_DRIVER_PROPERTY              "my_driver_property1"

 #endif  // _D_MY_DRVPROP_H

Don't forget to modify the Imakefile of the parent directory and to create a new Imakefile as follows:

 CSRCS = my_drv.c
 OBJS  = $(CSRCS:.c=.o)
 BuiltinDriver(D_my_drv.r, $(OBJS), $(DRV_LIBS))
 DistProgram(D_my_drv.r, $(DRV_DIST_BIN)$(REL_DIR))
 Depend($(CSRCS))
 
 

Device Tree


Each device is represented by a device node (DevNode) in the device tree. The device tree gives a description of the hardware topology in terms of parent/child relationships (which match bus/device connections). The device tree is the entry point for the kernel to initialize each device. To do so, the boot sequence goes through the device tree, associates a driver to each node, and uses the operations of this driver to initialize the device. Device nodes are also used to store specific configuration values as properties, which are made up of name/value pairs and declared in a property file such as my_drvProp.h.

An incomplete device tree is statically built at boot time. This device tree is then dynamically completed using a probing process.

The device tree can be found in:     nucleus/bsp/family/board/src/boot/deviceTree.c where family can be usparc, x86, or powerpc and board can be, for example, cp1500, mcp750.

To add a new device driver to the ChorusOS image, modify the device tree as follows (deviceTree.c):

  #include <drv/my_drv/my_drvProp.h>
  ...
  {
   /*
    * Adding a device node as the child of root. The PROP_DRIVER property
    * is used to associate the appropriate driver to that node.
    */
    DevNode my_device_node = dtreeNodeAdd(root, MY_DRIVER__DEV_NAME);
    dtreePropAdd(my_device_node, PROP_DRIVER, MY_DRIVER_DRV_NAME,
                 strlen(MY_DRIVER_DRV_NAME) + 1);
    /*
     * Adding a property to the device node :
     */
     dtreePropAdd(my_device_node, MY_DRIVER_PROPERTY, ..., sizeof(...));
   }
   ...

Modify the target.xml file of the board it should be built in, to add the definition and path of your driver:

 <definition name='my_drv'>
   <type name='File' />
   <value field='path'>
     <vstring>${DRV_DIR}/bin/drv/pci/my_drv/D_my_drv.r</vstring>
   </value>
   <value field='bank'><ref name='sys_bank' /></value>
   <value field='binary'><ref name='driver_model' /></value>
 </definition>
 

Driver Routines


A driver exports a DrvRegEntry stucture in the driver registry. This structure is composed of several functions like:
 

  • drv_probe:

  • This optional routine is first called by a bus driver to detect device(s) residing on the bus. When it finds one, this routine has to create a unique node  for the device and specify resource requirements and a physical device ID as device node properties.
     
  • drv_init:

  • This optional routine, called at boot time, is the entry point of the driver to initialize a device driver represented by a device node. It must check the validity of the device node and its properties and establishes a connection with the parent bus driver. Then, it allows access to the physical device and puts it in an operational state. It registers a device driver instance in the device registry.
     
  • drv_unload:

  • This optional routine is invoked by the driver registry module whenever an application wants to unload the driver from the system. Its function is to ensure the driver component is not currently in use and to release system resources associated with the driver instance. If this routine does not exist, the driver cannot be unloaded.

Device Registry


The device registry is a micro-kernel module that implements the database of self-registered device driver instances. It is the entry point for driver clients to obtain a pointer to the driver instance servicing a given device. Two sets of functions are defined: one set of functions is dedicated for a device driver instance to register in the device registry, and another one is used by driver clients to find a device driver instance and become a client.
 

  • Device registration:

  • A device driver instance must first call svDeviceAlloc() to allocate a device registry entry and then svDeviceRegister() to register it. Only registered driver instances are visible to clients through a DevRegEntry structure that describes the device driver instance:

    typedef struct {

      char*    dev_class;     /* Class of the device  */
      void*    dev_ops;       /* Points to a structure of DDI routines (DDI) */
      void*    dev_id;        /* Pass-back parameter for the driver routines */
      DevNode  dev_node;      /* Device node serviced by the driver instance */
      } DevRegEntry;


    The following functions are dedicated to device driver instances for self-registering and unregistering:

    DevRegId svDeviceAlloc(DevRegEntry *entry, unsigned int version,
                           Bool shared, DevRelHandler handler);
    svDeviceAlloc parameters:

DevRegEntry*  entry,   /* entry to allocate within the deviceregistry  */
unsigned int  version, /* specifies the version of the API implemented
                          by this driver */
Bool          shared,  /* indicates if it is a multi-client driver    */
DevRelHandler handler  /* driver handler which is invoked by the
                          device registry module as an acknowledgement
                          to a shut-down event                   */
    void     svDeviceRegister(DevRegId dev_id); /* registers the DevRegId previously allocated */
    KnError  svDeviceUnregister(DevRegId dev_id); /* unregisters a given DevRegId                  */
    void     svDeviceFree(DevRegId dev_id);
  • Device driver lookup:

  • svDeviceLookup() searches the device  entry in the registry matching the specified device class and logical unit.
 KnError
  svDeviceLookup(
       char* dev_class,          /* Specifies the device class       */
       unsigned int dev_version, /* Specifies the minimum device driver
                                     interface version required      */
       unsigned int dev_unit,    /* Specifies the logical device unit in the
                                    class                            */
       DevEventHandler
              cli_handler,       /* Specifies the event handler which is
                                    called when a device event is
                                    signalled */
       void* cli_cookie,         /* Fist parameter of cli_handler    */
       DevClientId* cli_id       /* Output argument identifying the client
                                   token on the matching device entry*/
  );

Upon success, svDeviceLookup returns K_OK, and the corresponding device entry is locked in the device registry.

Then call:

  DevRegEntry* svDeviceEntry(DevClientId cli_id);

cli_id is the DevClientId pointer previously obtained with svDeviceLookup() to obtain a pointer to the DevRegEntry structure that represents the device driver instance:

  typedef struct {
     char* dev_class;  /* Class of the device                         */
     void* dev_ops;    /* Points to a structure of DDI routines (DDI) */
     void* dev_id;     /* Pass-back parameter for the driver routines */
     DevNode dev_node; /* Device node serviced by the driver instance */
  } DevRegEntry;

DKI Thread


A thread called the DKI thread is launched by the ChorusOS microkernel at initialization time. Its role is to initialize and shutdown drivers and to handle synchronization services. This prevents the use of other synchronization mechanisms, such as locks, in the drivers' implementation.

Most of the time, both initialization and shutdown of drivers are performed implicitly in the context of the DKI thread, with no particular provisions needed in the drivers' code since their routines are called directly from the DKI thread. However, there are two distinct types of drivers that require specific use of the DKI thread services: hot-pluggable device drivers, such as PCMCIA drivers, and deferred drivers, which may defer their device initialization until they are opened. Deferred drivers are used to resolve conflicts about usage of the same resource by multiple drivers. In that way, drivers sharing a resource can be loaded at the same time, provided they are not opened at the same time.

Both types of drivers require initialization and shutdown to be performed at runtime, either when the device is inserted or removed during initialization, or when the device is opened or closed during shutdown.

There are only two DKI thread-related services:

  • svDkiThreadCall(), which synchronously invokes a routine in the context of the DKI thread

  •  
  • svDkiThreadTrigger(), which asynchronously invokes a routine in the context of the DKI thread

  • Note that calling svDkiThreadTrigger() is the best way to service an exception handler out of the context of the interrupt (we usually talk about the DKI thread context). For example, since calling svMemAlloc under interrupt is forbidden, the solution is to defer the interrupt handling with the DKI thread.
     

Bus Events Handler Function

Whenever a driver is initialized, it establishes a connection with its parent bus driver and specifies a callback event handler function. The parent bus driver uses this callback handler mechanism to propagate bus events to the connected child driver. Among all events, which are mostly bus-specific, there are three shutdown-related events that are specified in the common bus API:
 
  • SYS_SHUTDOWN - system emergency shutdown

  • This event notifies the driver instance that the system is going down and that it is required to perform an emergency shutdown of the hardware device it services. Upon receiving of a SYS_SHUTDOWN event, a device driver simply needs to put the hardware it services in a clean state so that the system can be rebooted properly.
     
  • DEV_SHUTDOWN - normal device shutdown

  • The DEV_SHUTDOWN event notifies the driver instance that a normal device shutdown is requested by its parent bus. Upon receiving of a DEV_SHUTDOWN event, the driver will enter a shutdown mode where it only accepts a subset of operation requests from its clients. It will service operations to:
    • Abort queued operations
    • Release resources
    • Close a connection to the driver
    Any other operation, such as opening a new connection or starting an I/O operation, will be rejected. This allows all clients to properly close existing connections to the driver. When the last device entry is released by the last driver client, the shutdown completes and the hardware is put in a clean state for a reboot.
     
  • DEV_REMOVAL - abnormal device removal

  • This event notifies the driver instance that the device has been removed from the bus, and therefore that the driver instance needs to be shut down. Since in this case the hardware device has already been removed, the driver must abort all existing I/O operations which would never complete. The driver will then enter the same shutdown mode as the DEV_SHUTDOWN event, except that it will not put the hardware device in a clean state.


In all these cases, it is the role of the device driver to inform the device registry that a given event has occured by calling svDeviceEvent(). The device registry will then call the event handlers of the clients (which are specified via the call to svDeviceLookup() ) of this device driver, informing them that the device they are using is being removed or shutdown. To acknowledge that event, a client has to call the svDeviceRelease() routine. Finally, once the device driver is released by the last driver client, the device registry module invokes the handler previously specified by the call to svDeviceAlloc(), which will perform the hardware reset.

Note that the parent bus driver should not deal directly with the device registry. It should only propagate events between its parent and its children drivers.
 

As an example, here is a simple event handler function for the my_drv driver.

    static KnError
my_drv_event (void*    id,
              BusEvent event,
              void*    arg)
{
    KnError        res  = K_OK;
    My_Drv_Device* dev  = (My_Drv_Device *)id;
    DevNode        node = dev->entry.dev_node;

    switch (event) {

        case BUS_SYS_SHUTDOWN: {
            DKI_MSG(("%s: system emergency shutdown\n", dev->dpath));
            break;
        }

        case BUS_DEV_SHUTDOWN: {
            if (dev->devEvent == DEV_EVENT_NULL) { /* Ignore the event if device
                                                      already in shutdown mode */
                dev->devEvent =  DEV_EVENT_SHUTDOWN;
                svDeviceEvent(dev->regId, DEV_EVENT_SHUTDOWN, NULL);
                DKI_MSG(("%s: entered into shutdown mode\n", dev->dpath));
            }
            break;
        }

        case BUS_DEV_REMOVAL: {
            if (dev->devEvent != DEV_EVENT_REMOVAL) { /* Ignore the event if device
                                                         already in removal mode */
                dev->devEvent =  DEV_EVENT_REMOVAL;
                svDeviceEvent(dev->regId, DEV_EVENT_REMOVAL, NULL);
                DKI_MSG(("%s: entered into removal mode\n", dev->dpath));
            }
            break;
        }

        default: {
            res = K_ENOTIMP;
            break;
        }
    }
    return res;
}
 
 

Device Driver Interface (DDI)


An API, called DDI, is defined for each class of buses or devices. Each driver is a client of the DDI of its parent's bus driver. These APIs can be found in nucleus/sys/common/src/ddi. The development of a device driver is based on the DDI of the class of its parent's driver. Actually, the drv_init routine of a driver is called by the parent bus driver. It is the reason why the second parameter of drv_init is a pointer to a structure containing the parent bus operations. Usually, the last argument is the parameter for the functions of the parent bus.

To continue the  example export a generic bus DDI:

#include  <dki/dki.h>
#include  <ddi/bus/bus.h>
...
#include  "my_drv.h"
#include  "my_drvProp.h"

     /*
      * Open device.
      */
     static int
 my_drv_open (BusId id, ...) { ... }
     /*
      * Disable device interrupts.
      */
     static void
 my_drv_mask (BusId id) { ... }
     /*
      * Enable device interrupts.
      */
     static void
 my_drv_unmask (BusId id) { ... }

 ...
     /*
      * Close device.
      */
     static void
 my_drv_close (BusId id) { ... }

     /*
      *  my_drv service routines:
      */
 static BusDevOps my_drv_ops =
 {
     BUS_VERSION_INITIAL,
     my_drv_open,
     my_drv_mask,
     my_drv_unmask,
     ...
     my_drv_close
 };

    /*
     * Define a device descriptor structure to store
     * all data needed by the driver instance.
     */
 typedef struct My_Drv_Device
 {
    DevRegEntry entry;
    DevRegId    regId;

 };

     /*
      * Init the my_drv Bus. Called by parent BUS driver.
      */
     static void
 my_drv_init (DevNode node, void* pOps, void* pId)
 {
     BusOps*       busOps = (BusOps*)pOps;
     My_Drv_Device*  dev;
     ...
         /*
          * Allocate the device descriptor
          * (i.e. the driver instance local data)
          */
     dev = (My_Drv_Device*) svMemAlloc(sizeof(My_Drv_Device));
     ...
     dev->entry.dev_class = Bus_CLASS;
     dev->entry.dev_id    = dev;
     dev->entry.dev_node  = node;
     ...
     dev->entry.dev_ops = &my_drv_ops

         /*
          * 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 (my_drv_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),
                                BUS_VERSION_INITIAL,
                                FALSE,
                                my_drv_release);
     ...
         /*
          * Finally, we register the new device driver instance
          * in the device registry. If a shutdown event
          * has been signaled during the initialization, the device entry
          * remains invalid and the my_drv_release() handler is invoked
          * to shut down 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, MY_DRV_DRV_NAME));
 }

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.

Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.