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