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.
Using the Driver Framework to build bus and device drivers in the ChorusOS operating system provides the following benefits to the user:
A structured framework, easing the task of building drivers
Hierarchical structure of drivers in Driver Framework mirrors hardware structure
Ensures compliance and functionality within the ChorusOS operating system
Enables the user to develop multi-bus device drivers, which may run on all buses supporting the Common Bus Driver Interface
Drivers built with the Driver Framework are homogeneous across various system profiles (flat memory, protected memory, virtual memory)
Allows dynamic configuration (and re-configuration) needed for plug-and-play, hot-plug and hot-swap support
Supports the binary driver model
APIs are version resilient
Is adaptive (in terms of the memory footprint and complexity) to the various system profiles and customer requirements
Supports the dynamic loading and unloading of driver components
Meets real-time requirements, by providing non-blocking (asynchronous) run-time APIs
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).
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).
To sum up, the ChorusOS operating system Driver Framework can be considered in two ways:
A hierarchical set of APIs which defines the services provided for and used by each bus or device driver at each layer of the architecture. This approach ensures portability and functionality across various platforms and continued validity of drivers across subsequent system releases.
A set of mechanisms implemented by the ChorusOS microkernel, ensuring compliance and synchronicity with the ChorusOS operating system architecture and methods.
Figure 1-2 shows the objects involved in the ChorusOS Driver Framework:
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.
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:
Synchronization through the DKI thread
Device tree
Driver registry
Device registry
General purpose memory allocation
Timeout
Precise busy wait
Special-purpose physical memory allocation
System event management
Global interrupts masking
Specific I/O services
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:
Processor interrupts management
Processor caches management
Processor specific I/O services
Physical to virtual memory mapping
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.
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.
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.
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.
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."
The driver framework defines three device and driver registration entities which are managed through the DKI interface:
the Driver Registry is used to register and manage loaded driver components
the Device Tree is used to represent the hardware buses/devices hierarchy and defines the properties of each hardware chip
the Device Registry is used to register/retrieve driver component's instances servicing a given hardware device (bound to and initialized to manage a given node in the device tree)
The microkernel initialization goes through the following steps:
device independent microkernel initialization
built-in device drivers initialization
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:
information about the component (name, version)
the required parent bus API class and version
driver's entry points, which have a well-defined semantic
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:
a driver's probe function (drv_probe) to detect device(s) residing on the bus and to create device tree node(s) corresponding to these types of device(s)
a driver's bind function (drv_bind) to bind a driver to a device tree node
a driver's initialize function (drv_init) to initialize the hardware device, and to create a running instance of the driver component
a driver's unload function (drv_unload) which is invoked by the driver registry module when an application has to unload the driver component from the system
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).
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).
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.
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:
A header file (named devxProp.h) defining properties which are specific to this devx driver component. This file should be visible from the component(s) responsible for building the associated device tree node (to create the node's properties). This may be the boot program or any other "probe only" driver component.
An implementation file (devx.c) which contains the C code for the driver component. Note that for very big driver components, there may be multiple .c files. Note also that the hardware related definitions and constants are sometimes extracted from the .c file and put in a header file (devx.h) where use is restricted to the implementation file.
An Imakefile that is used to generate a Makefile (through the imake tool) in order to compile and link the driver component. Refer to the make and imake sections in "ChorusOS 4.0 Introduction", and the imake header file (see the TOOLS directory: installation_directory/platformtype-bin/dtool) for details about the imake macros that are available to build driver components.
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))
All files related to driver components are organized in 4 file trees:
The 'dki' tree, populated by the ChorusOS operating system delivery and exporting the DKI set of APIs (header files onl).
The 'ddi' tree, populated by the ChorusOS operating system delivery and exporting the DDI set of APIs (header files only).
The 'drv' tree, populated by generic driver components and exporting the device-specific properties header files. Generic driver components are drivers which do not use the family specific DKI APIs, and therefore are portable across all families and platforms (header and 'c' files).
The 'drv_f' tree, populated by processor family specific driver components and exporting device specific properties header files. Processor family-specific driver components are drivers which use one family specific DKI API, and therefore can run only on this processor family (header and 'c' files).
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:
<dki/dki.h> which defines the Common DKI API
<dki/f_dki.h> which defines the processor family-specific DKI API
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
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:
the hardware that can be serviced by the driver
the driver name
the driver framework features and mechanisms that are implemented in the driver (probing, dynamic loading, and so on)
the description of device tree node properties used by the driver
There are several conventions one should be aware of when writing device drivers, pertaining to:
Driver Names
Driver Information
Message Logging
Use of ASSERT Macro
Driver names in the Driver Framework must follow the following conventions, in this order:
driver vendor name
bottom interface used by driver
chip supported by driver
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 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\"]
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:
the name of the driver (if the message is not related to a particular instance of the driver)
the path of the device in the device tree (if the message is related to a driver instance)
The dtreePathLeng() and dtreePathGet() calls can be used to get the device tree path for a particular instance.
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