ChorusOS 4.0 Device Driver Framework Guide

Chapter 1 Introduction to the ChorusOS Driver Framework

Introduction

The ChorusOS system provides a driver framework, allowing the third-party developer to develop device drivers on top of a binary system distribution. The Driver Framework provides a well-defined, structured and easy-to-use environment to develop both new drivers and client applications for existing drivers.

Host bus drivers written with the Driver Framework are processor-family specific, meaning that they are portable within that processor family (UltraSPARCTM, PowerPC, Intel ix86 processor families). Drivers that occupy a higher place in the hierarchical bus structure (sub-bus drivers and device drivers) are usually portable between processor families.

Device Driver implementation is based on services (provided by a set of APIs, such as PCI or ISA) which allow the developer to choose the optimizability and portability of the driver they create. This allows the driver to be written to the parent bus class, and not the underlying platform. Drivers written within the Driver Framework may also take advantage of processor-specific services, allowing maximum optimization for a particular processor family.

Benefits of Using the Driver Framework

Using the Driver Framework to build bus and device drivers in the ChorusOS operating system provides the following benefits to the user:

Framework Architecture Overview

In the ChorusOS operating system, a driver entity is a software abstraction of the physical bus or device. Creating a device driver using the Driver Framework allows the device or bus to be represented to and managed in the ChorusOS operating system. The hierarchical structure of the driver software within the ChorusOS operating system mirrors the structure of the physical device / bus.

Each device or bus is represented by its own driver. A driver's component code is linked separately from the microkernel as a supervisor actor, with the device-specific code strongly localized in the corresponding device driver.

Note that a supervisor actor containing a driver code should be considered as a container only rather than as a real supervisor actor with its own execution personality. Driver code runs either in the interrupt execution environment (typically up-calls) or in the driver client execution environment (typically down-calls). In other words, the driver component code logically belongs to the current driver client (microkernel module or supervisor actor).


Note -

The driver is always considered a trusted system component.

This means that the Driver Framework defines a structure and principle, but since the driver is a trusted system component, parameter and logic checking are not performed on most drivers in release mode. Even if the task of creating drivers with the Driver Framework seems relatively simple, care should be taken to ensure that drivers are written in conformance with the framework. Some checking can be added in debug mode, but this can not replace writing the driver in compliance with the Driver Framework.


Driver components are organized, through a services-provider/user relationship, into hierarchical layers which mirror the hardware buses/devices connections.

Interactions between these drivers are implemented via simple indirect function calls (down-calls and up-calls).

Figure 1-1 Bus/Device Hierarchy, in Hardware and Software

Graphic

To sum up, the ChorusOS operating system Driver Framework can be considered in two ways:

Figure 1-2 shows the objects involved in the ChorusOS Driver Framework:

Figure 1-2 Driver Framework Objects

Graphic

Driver Framework APIs

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 1-3 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 dki(9) man page gives an entry point to a detailed description of all DKI APIs.

Device Drivers 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 1-3, 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 ddi(9) manpage gives an entry point to a detailed description of all DDI APIs.

Driver Framework Mechanisms and Principles

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 follows, while a more detailed examination is provided in "Chapter 2, Driver Kernel Interface Overview","Chapter 3, Writing Device Drivers" and "Chapter 4, Writing Bus Drivers."

Driver Registration

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

Driver Initialization

The microkernel initialization goes through the following steps:

  1. device independent microkernel initialization

  2. built-in device drivers initialization

  3. device dependent microkernel initialization

At the first step, the microkernel performs initialization of device-independent modules like executive, memory management and so on.

At the second step, the microkernel installs and launches the built-in device driver actor(s) (drivers which are embedded in the ChorusOS operating system archive). Note that 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, by using the DKI interface.

When registering, the driver exports its properties to the system:

Once the driver component is self-registered, future management of the driver is controlled by its parent bus/nexus driver, using the properties registered.

The four possible entry points that a driver component may register are:

Finally, once built-in driver components have been started, the microkernel performs initialization of device dependent modules (like the "TICK" module which relies on a TIMER class device).


Note -

Interrupts are disabled at CPU level during the first, second and third initialization steps. Once the built-in drivers are initialized, interrupts are enabled at CPU level.


Once all of the driver's 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 probe registered function is called for all driver components requiring a DKI/FDKI parent bus class. After probing, the bind function is called for all driver components requiring a DKI/FDKI parent bus class. Finally, after binding, the initializeregistered 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 -

The drv_probe, drv_bind and drv_init routines are all 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 call-back 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

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.r driver actor which is embedded in the archive.

CSRCS = raven.c

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

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

DistProgram(D_raven.r, $(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 4 file trees:

Note that both drv and drv_f trees are 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 and drv_f, in each bus/device class directory there is one directory per bus/device hardware controller for which a driver component is written.

Listed below are some path examples (header file paths are relative to the ChorusOS operating system delivery root 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

    drv_f/src/pci/raven/ravenProp.h           -> family specific driver component
    drv_f/src/pci/raven/raven.h                      for the Motorola RAVEN PCI host
    drv_f/src/pci/raven/raven.c                       bridge
    drv_f/src/pci/raven/Imakefile

    drv/src/uart/ns16550/ns16550Prop.h    -> Generic driver component for
    drv/src/uart/ns16550/ns16550.h                NS16x50 compatible UART devices.
    drv/src/uart/ns16550/ns16550.c
    drv/src/uart/ns16550/Imakefile

Manpage Documentation

Typically, there is one manpage for each written driver component. The manpage file for a devx driver component is called 'devx.9drv' and accessible through the devx name. This manpage 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 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