ChorusOS 5.0 Board Support Package Developer's Guide

Chapter 8 Introduction to the ChorusOS Driver Framework

This chapter contains an overview of the ChorusOS Device Driver Framework.

Driver Framework APIs

This section describes the layered interface of APIs. including definitions of the terms Device Kernel Interface API (DKI) and Device Driver Interface API (DDI) as well as the Common Bus Driver Interface (CBDI) and the bus specific interfaces.

One of the key attributes allowing portability and modularity of devices constructed using the Driver Framework is the hierarchical structure of the APIs, which can also be seen as the layered interface. Within this model, all calls to the microkernel are performed through the Driver Kernel Interface (DKI) API, while all calls between drivers are handled through the Device Driver Interface (DDI) API.

The figure below represents the layered (hierarchical) structure of the Driver Framework APIs.

Figure 8-1 Device Interface Layering

Graphic

Driver/Kernel Interface (DKI)

The DKI interface defines all services provided by the microkernel to driver components. Following the layered interface model, all services implemented by the DKI are called by the drivers, and take place in the microkernel.

Common DKI services are services common to all platforms and processors, usable by all drivers, no matter what layer in the hierarchical model they inhabit. These services are globally designed by the DKI class name.

Common DKI services cover:

Processor family specific DKI services are defined and available only for a given processor family and should be used only by the lowest-level drivers. Lowest-level drivers are those for buses and devices which are directly connected to the processor local bus. Note that these drivers typically use only the DKI services (no available layer of DDI). These services are globally designed by the FDKI class name (for Family DKI).

Processor family specific DKI (FDKI) services cover:

All DKI services are implemented as part of the embedded system library (libebd.s.a). Most of them are implemented as microkernel system calls. Note that the Intro(9DKI) man page gives an entry point to a detailed description of all DKI APIs.

Device Driver Interface (DDI)

The DDI defines several layers of interface between different layers of device drivers in the driver's hierarchy. Typically an API is defined for each class of bus or device, as a part of the DDI.

Note that a driver's client application may itself be a driver component (as a device driver is a client of the bus driver API). In this way, it can be seen that all DDI services are implemented by a driver component, and are in turn called by upper-layer drivers (or directly by the driver's client applications).

As illustrated earlier, in Figure 8-1, the DDI set of APIs is further divided along hierarchical lines into two principle interface layers -- Bus Driver Interfaces and Device Driver Interfaces.

Bus Driver Interface APIs

This layer of interfaces is implemented by the lowest level layer of drivers, using DKI services. This set of drivers can itself be composed of multiple sub-layers to reflect the bus hierarchy of a given platform.

Typically, only the primary (host) bus driver is built solely using DKI services. Subsequent drivers, those occupying a "downstream" position in the hierarchy, interface with the primary (host) bus. As all different I/O buses share a subset of features, and then have their particular specificities, the bus driver interfaces layer offers a subset of services called "Common bus driver interface" (CBDI) , which is independent of the bus type, offering a set of services common for all bus classes.

In addition to the CBDI, there is of course a collection of bus specific interfaces (such as PCI, VME, ISA) to implement bus-specific driver services.

Device Driver Interface APIs

This layer of interfaces is implemented by the device drivers, and is built upon the lower layer of services (bus driver interfaces). This set of device drivers provides different interfaces for each different class/type of device. Typically, there are different interfaces for timer devices, UART devices, Ethernet devices and so on.

Each of these APIs may be used by the driver's client application to manage the associated devices. Note that the Intro(9DDI) man page gives an entry point to a detailed description of all DDI APIs.

Driver Framework Mechanisms and Principles

This section describes the mechansims that are implemented to enforce well-defined behavior regarding driver component initialization, dynamic loading/unloading and bus event management.

As mentioned above, the ChorusOS operating system microkernel implements mechanisms to enforce a well-defined behavior regarding driver component initialization, dynamic loading/unloading, and bus events management. An overview of these mechanisms is contained in the following sections, though more detailed information is included in the chapters: Chapter 9, Driver Kernel Interface Overview, Chapter 10, Writing a New Device Driver and Chapter 11, Writing a New Bus Driver.

Driver Registration

The driver framework defines three device and driver registration entities which are managed through the DKI interface:

Driver Initialization

The microkernel initialization process is split into two steps. The first step is carried out inside the critical section with interrupts masked at the CPU level. The second step is executed outside the critical section with interrupts enabled at CPU level.

Therefore, the BSP drivers may be split into two groups: the first group of drivers that are initialized within the critical section, and the second group of drivers that are initialized later with interrupts enabled at CPU level.

Such an initialization process enables the majority of the BSP drivers to be moved from the critical section. Basically, only the host bus driver (which typically manages the master interrupt controller) should be initialized within the critical section in order to allow interrupts to be enabled at CPU level, prior to the second initialization step.

This is extremely important for a platform where the control of a critical device should be taken by a driver as soon as possible. For example, this is the case on a platform where a watchdog timer is enabled only once by the boot code, and the microkernel watchdog driver has to be started before the watchdog timer interval expires. Note that the driver of such a watchdog timer device typically needs to have microkernel timeout services available in order to reset (pat) the watchdog device periodically.

In the ChorusOS operating system, the microkernel initialization process goes through the following steps:

  1. Microkernel module initialization (including DATE and TICK)

  2. Critical device initialization

  3. Interrupt enabling at CPU level

  4. Normal device initialization

A BSP developer should split all platform devices into two groups. The first group of (critical) devices is initialized in step 2, that is, inside the critical section with interrupts masked at CPU level. The second group of (normal) devices is initialized in step 4, that is, outside of the critical section with interrupts enabled at CPU level. Note that the host bus bridge should be part of the critical group in order to disable interrupts at the bus interrupt controller level. Otherwise, the system behavior may be unpredictable once interrupts are enabled at CPU level (spurious interrupts may occur).

The PROP_INIT_LEVEL property is used to specify whether a device is critical or normal, and therefore, whether it should be initialized at step 2 or 4. The PROP_INIT_LEVEL property value is an integer value that specifies the device initialization level allowed. Attached to a device node, this property specifies at which step the associated device should be initialized.


Note -

The TICK and DATE modules are initialized at step 1. These modules use the svDeviceNewNotify() DKI service in order to be notified when a new device is registered in the device registry. Therefore, the DATE and TICK modules are initialized prior to the device drivers initialization. However, they only become operational when the associated device drivers are started (registered) and a connection is established between the module and the underlying device driver.


At step 2, each driver actor's main() function is invoked sequentially by the microkernel initialization thread. The driver's main() function should perform a self-registration of the driver component within the system using the DKI interface.

When registering, the driver exports its properties to the system. It exports the following:

Once the driver component is self-registered, future management of the driver is controlled by its parent bus/nexus driver, using the registered driver properties. The four possible entry points that a driver component may register are:

Once all of the driver main() functions are invoked, the microkernel initiates the device initialization process. This can be seen as the microkernel implementing a local bus driver (bound to the device tree root node) for a DKI/FDKI bus class.

The initialization process starts from driver components servicing bus or device controllers directly connected to the CPU local bus; the driver registry is searched to find out the appropriate drivers and to call their registered entry points. Typically, the drv_probe() registered function is called for all driver components requiring a DKI/FDKI parent bus class. After probing, the drv_bind function is called for all driver components requiring a DKI/FDKI parent bus class. Finally, after binding, the initialize registered function is called for all driver components requiring a DKI/FDKI parent bus class, that are bound to a child of the device tree root node (nodes representing a bus or a device controller directly connected to the CPU local bus).


Note -

All the driver entry point routines drv_probe(), drv_bind() and drv_init() are optional.


The drv_probe() routine detects device(s) residing on the bus and creates corresponding device nodes in the device tree. The drv_bind() routine allows drivers to perform a driver-to-device binding. The driver examines the properties attached to the device node in order to determine the type of device and to check whether the device may be serviced by the driver. If the check is positive, the driver attaches a driver property to the device node. The name of the driver node is "driver" and it has a string type value, specifying its name. The initialization process is propagated by the drv_init function of the bus/nexus drivers started by the microkernel.

In addition, when a driver instance is activated by a parent bus/nexus driver (through its registered drv_init() function), it establishes a connection to its parent bus driver (typically through an open service of the bus API) specifying a callback event handler and a load handler. The parent bus/nexus driver uses the call-back event handler mechanism to propagate the bus events to the connected child driver instances. These events are typically bus-class specific, but are usually used to shut down child driver instances. The load handler is used (together with the unload entry point) to manage dynamic loading/unloading of the driver components.

Driver Framework Components

This section describes the elements of the Device Driver Framework in the ChorusOS operating system.

Source Files

Typically, a driver component is a ChorusOS operating system supervisor actor written in 'C' programming language. This type of component (named devx for the example) is usually composed of the following files:

Below is a typical example of an Imakefile, which exports a ravenProp.h file, compiles a raven.c implementation file and then builds a D_raven driver actor which is embedded in the system image.


Example 8-1 Imakefile to Build the D_raven Driver Actor

CSRCS = raven.c

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

BuiltinDriver(D_raven, $(OBJS), $(DRV_LIBS))

DistProgram(D_raven, $(DRV_DIST_BIN)$(REL_DIR))

Depend($(CSRCS))

FILES = ravenProp.h

DistFile($(FILES),)$(REL_DIR),$(DRV_DIST_INC)$(REL_DIR))

Organization (trees)

All files related to driver components are organized in three file trees:

Note that the drv tree is mainly populated by third party driver writers (although ChorusOS system deliveries contain drivers for the reference platform's devices).

The main functional components of the 'dki' tree are:

All other file trees are organized following the bus/device class provided APIs. In other words, there is a directory per class of bus and device, which contains the header file defining the API provided by this device class.

For drv the generic drivers are classified by DDI and are contained in corresponding directories, whereas the family-specific drivers are contained in family directories.

Listed below are some path examples (header file paths are relative to the ChorusOS operating system delivery source_dir directory):

include/chorus/ddi/bus/bus.h

DDI's Common bus class API

include/chorus/ddi/pci/pci.h

DDI's PCI bus class API

include/chorus/ddi/uart/uart.h

DDI's UART device class API

nucleus/bsp/src/powerpc/pci/raven/ravenProp.h, nucleus/bsp/src/powerpc/pci/raven/raven.h, nucleus/bsp/src/powerpc/pci/raven/raven.c, nucleus/bsp/src/powerpc/pci/raven/Imakefile

family specific driver component for the Motorola RAVEN PCI host bridge

nucleus/bsp/src/uart/ns16550/ns16550Prop.h, nucleus/bsp/src/uart/ns16550/ns16550.h, nucleus/bsp/src/uart/ns16550/ns16550.c, nucleus/bsp/src/uart/ns16550/Imakefile

Generic driver component for NS16x50 compatible UART devices

Driver Documentation

Typically, there is one man page for each driver component that is supplied with the ChorusOS operating system. The man page file for a devx driver component is called devx.9drv and accessible through the devx name. This page contains the following information:

Device Driver Conventions

There are several conventions one should be aware of when writing device drivers, pertaining to:

Driver Names

Driver names in the Driver Framework must follow the following conventions, in this order:

  1. driver vendor name

  2. bottom interface used by driver

  3. chip supported by driver

  4. top interface used by driver

For example:

sun:bus-ns16550-uart
sun:pci-cheerio-ether

where "sun" is the vendor name, "bus" and "pci" are the bottom interfaces used, "ns16550" and "cheerio" are the chips supported, and "uart" and "ether" are the top interfaces.

In the case of a driver providing several top interfaces, these interfaces are specified within parentheses, separated by commas:

sun:bus-mc146818-(rtc, timer)

Driver Information

Driver information is stored in the driver registry record, using the drv_info field. This info string must be built after the driver description and source management version information of the driver module.

For example:

NS16x50 UART driver [#ident \"@(#)ns16550.c 1.16 99/02/16 SMI\"]

Message Logging

Because messages are processed through the ChorusOS operating system, drivers must never use sysLog or printf directly to display messages. The ChorusOS operating system provides the following macros to handle message logging:

DKI_MSG((format, ...))

typically does: printf

DKI_WARN((format, ...))

typically does: printf + syslog

DKI_PANIC((format, ...))

typically does: printf + syslog + callDebug

DKI_ERR((format, ... ))

typically does: printff + syslog

Moreover, message format conventions are as follows:

DKI_MSG

"<name>: <message>"

DKI_WARN

"<name>: warning -- <message>"

DKI_ERR

"<name>: error -- <message>"

DKI_PANIC

"<name>: panic -- <message>"

where <name> is either:

Use of ASSERT Macro

ASSERT is a macro (fully defined in util/macro.h) which should only be used in situations which should not logically be possible in the construction of the software.

Situations such as critical resource allocation failures should be handled with the DKI_ERR macro instead.

ASSERT is enabled at compile time only, with

#define DEBUG