Fork us on GitHub Follow us on Google+ Follow us on Facebook Follow us on Twitter

Version 28 (modified by Jakub Jermář, 6 months ago) (diff)

Writing Device Drivers for HelenOS

What is a device driver?

The hardware an OS runs on can be modelled as a hierarchy of (peripheral) interconnects (buses) to which devices are attached. Each bus provides connectivity to devices or other buses. This hiearchy can be often represented as a tree (or directed acyclic graph), where inner (nexus) nodes correspond to buses and leaf nodes correspond to devices.

Needless to say this is just a model, meaning nothing is given a priori, the model can look differently depending on our choice. It is often not clear what is a device and what is not, where are the boundaries of a particular device, etc. Consequently, there is also no clear line between what still is a device driver and what is not.

Usually devices (or their drivers) have one or more of the following traits. When a system is coming up, it attempts to transitively discover all buses and devices connected to it. For each bus or device it selects the appropriate driver. The driver takes (complete) control of the bus or device and makes its services available to the system (to other drivers in case of a bus, to applications in case of a device). The driver abstracts away the details of a particular device model and provides an interface common to a class of devices (e.g. Ethernet adapter). The system often also virtualizes the device, providing concurrent access to multiple clients, but this can be done at a higher level, rather than in the driver itself.

Overview of Drivers in HelenOS

Device drivers in HelenOS come in two flavors, plain drivers and DDF drivers. Plain drivers originated before the Device Driver Framework (DDF) was created. They are simple servers which are started manually (from command line or from init task) and they reside in /srv. DDF drivers make use of the Device Driver Framework which takes care of starting them, attaching them to devices, etc. They reside in /drv.

Both kinds of drivers export their services to clients in the same way, as services registered with the Location Service. Each service has a unique name and it can be added to one or more categories. For a client it does not matter how a service is implemented, whether in a plain driver or in a DDF driver. This is an important design point.

DDF drivers are most useful for drivers that reside on busses that support discovery and hotplug (PCI, USB). Plain drivers are useful for implementing pseudo-devices. file_bd is an example of a plain driver. It implements a file-backed block device and it is started from the command line.

DDF (Device Driver Framework)

The Device Driver Framework (DDF) implements common functionality which is useful for most device drivers. It manages the device topology graph (device tree), coordinates enumeration, automatically starts drivers and allows communication between drivers of parent and child devices. DDF imposes certain structure of the driver and defines calls and entry points by which DDF and the driver communicate.

Internally DDF consists of a server, the Device Manager (devman) and the Device Driver Library (libdrv). Every driver is linked against libdrv, which internally communicates with the Device Manager via IPC — this is hidden from the driver. Device Manager holds information about drivers and device topology and coordinates operation of the drivers.

There is an administration tool associated with the Device Manager, devctl. devctl can be used to control operation of the Device Manager. It can display the device tree, offline and online devices (i.e. perform anticipated unplug operations).

Device Graph

Devices and Functions

The traditional view of the device graph, which models the system topology, is a tree which has a single type of node (device node). There are two interested parties to each device node, the bus driver (parent) and the device driver (child). Each views the same node from a different perspective.

From the point of view of the bus driver, the node identifies a device on the bus that the bus driver can provide access to, or, more broadly, a service/function provided by the bus driver. From the point of view of the child/device driver, the same node represents a device that the driver is handling (that usually corresponds to what is considered an instance of the driver). The device driver accesses the device via the parent (bus) driver and identifies the device using the device node.

In HelenOS DDF this traditional model is slightly modified. We split the single type of node — having two roles — into two node types, device node and function node. Having done that, each driver consumes device nodes and provides function nodes. From the driver perspective, these are represented by distinct C types, ddf_dev_t and ddf_fun_t.

Inner and Exposed Functions

There is no formal distinction between a bus (nexus) driver and a leaf (device) driver. All drivers consume device nodes and produce function nodes. There are two type of functions (function nodes), inner functions and exposed functions. Inner functions are functions internal to the device graph and DDF will attempt to attach child devices/drivers under these functions. Exposed functions are meant to be consumed by clients external to the DDF (such as applications or non-DDF servers) and the DDF will expose them as services via the Location Service.

Device and Function Life Cycle

TODO

DDF Driver Structure

When HelenOS runs the driver is an executable stored in /drv (e.g. /drv/foo). The driver is normally started automatically by the Device Manager. When the driver is needed, the Device Manager will start it and the driver connects to the Device Manager.

In HelenOS source tree drivers are located under uspace/drv. To add a new driver foo, you need to:

  • create a directory uspace/drv/a/b/foo
  • create a makefile uspace/drv/a/b/foo/Makefile
  • create at least one source file uspace/drv/a/b/foo/foo.c
  • add directory uspace/drv/a/b/foo to the DIRS definition in uspace/Makefile

You can use an existing driver as a starting point, for example uspace/drv/test/test1. A driver is a C program similar to a HelenOS server or application. It is linked with libdrv (and the makefile should add libdrv's include path to the header search paths).

An example driver makefile (with license stripped):

USPACE_PREFIX = ../../..
LIBS = $(LIBDRV_PREFIX)/libdrv.a
EXTRA_CFLAGS += -I$(LIBDRV_PREFIX)/include
BINARY = foo

SOURCES = \
        foo.c

include $(USPACE_PREFIX)/Makefile.common

The most basic DDF interfaces are defined in the header ddf/driver.h.

  • #include <ddf/driver.h>
  • driver_t
  • driver_ops_t — driver entry points
    • dev_add — ask driver to take ownership of device
    • dev_remove — ask driver to give up device
    • dev_gone — device connectivity lost
    • fun_online — ask driver to online function
    • fun_offline — ask driver to offline function
  • ddf_dev_ops_t

Every driver must define a driver_t structure which links to a driver_ops_t structure. driver_ops_t fields point to various driver entry points. dev_add is necessary in order for the driver to work. dev_remove, dev_gone, fun_online, fun_offline are required in order for the driver to support hot unplug (all drivers should support hot unplug).

Driver Entry Points (driver_ops_t)

dev_add

int (*dev_add)(ddf_dev_t *dev)

This entry point is called by DDF to ask the driver to take ownership of a new device. The driver should probe the device to verify that it is there and operational. If not, it should return failure. If the device is operational, the driver should take ownership and return EOK. The driver should also allocate soft state and create functions to expose functionality of the device (in case of bus driver some of these will correspond to devices currently connected to the bus).

It is up to the driver to which extent it wants to perform these initialization steps before or after returning from dev_add. This entry point is mandatory, it must always be implemented.

dev_remove

int (*dev_remove)(ddf_dev_t *dev)

This entry point is called by DDF to ask the driver to gracefully give up ownership of a device. The driver should gracefully terminate any pending operations on the device, quiesce the device and return it to some suitable, clean state (from which it could be picked up by dev_add, for example).

The driver must offline and unbind all functions belonging to this device and it should also clean up any software state associated with the device. If this entry point is not implemented, it should be either set to NULL or it should always return ENOTSUP.

dev_gone

int (*dev_gone)(ddf_dev_t *dev)

This entry point is called by DDF to inform the driver that connectivity to a device has been lost (e.g. because the device has been physically unplugged). The driver must coordinate with its parent to terminate any pending operations on the device. The parent will normally not allow any new operations to be started and, possibly, it will abort all outstanding operations (or wait for the driver to abort them).

The driver must unbind all functions belonging to this device and it should also clean up any software state associated with the device. If this entry point is not implemented, it should be either set to NULL or it should always return ENOTSUP.

fun_online

int (*fun_online)(ddf_fun_t *fun)

This entry point is called by DDF to ask the driver to online a function. The driver must online the function fun. It may also online other functions, if necessary (in case the function states are interlocked somehow). For many drivers this function will simply call ddf_fun_online(fun).

If this entry point is not implemented, it should be either set to NULL or it should always return ENOTSUP.

fun_offline

int (*fun_offline)(ddf_fun_t *fun)

This entry point is called by DDF to ask the driver to offline a function. The driver must offline the function fun. It may also offline other functions, if necessary (in case the function states are interlocked somehow). For many drivers this function will simply call ddf_fun_offline(fun).

If this entry point is not implemented, it should be either set to NULL or it should always return ENOTSUP.

Calls for Managing Functions

DDF provides the driver with a set of calls to manage functions:

  • ddf_fun_create()
  • ddf_fun_destroy()
  • ddf_fun_add_match_id()
  • ddf_fun_add_to_category()
  • ddf_fun_bind()
  • ddf_fun_unbind()
  • ddf_fun_online()
  • ddf_fun_offline()

ddf_fun_create

ddf_fun_t *ddf_fun_create(ddf_dev_t *dev, fun_type_t ftype, const char *name)

Create a new function of the device dev. ftype is either fun_inner or fun_exposed. Exposed functions are exported via the Location service to clients, while inner functions are used as points for attaching child devices. name identifies the function relative to dev (it can be e.g. its address on the bus).

The function will not be visible to the system until it is bound. ddf_fun_create will only fail if there is not enough memory. If ddf_fun_create fails, it will return NULL.

ddf_fun_destroy

ddf_fun_t *ddf_fun_destroy(ddf_fun_t *fun)

Destroy function fun. fun must not be bound. An unbound function is not visible to the system. The only effect of this function is that it frees any memory/resources that the driver internally allocated for the function.

ddf_fun_add_match_id

int ddf_fun_add_match_id(ddf_fun_t *fun, const char *match_id_str, int match_score)

Add match ID to an inner function. The match ID determines which driver will be attached to the child device. ddf_fun_add_match_id can only be called on an unbound inner function. If returns EOK on success or negative error code.

As an example, for an automatically enumerated bus the match ID could consist of bus type/name, vendor ID and device ID.

ddf_fun_add_to_category

int ddf_fun_add_to_category(ddf_fun_t *fun, const char *cat_name)

Add exposed function fun (more precisely the service which the function is exposed through) to the Location Service category name. This allows clients to locate the function by type. For example, the keyboard category is used for keyboard devices and mouse category is used for mice (pointing devices). ddf_fun_add_to_category can only be called on bound exposed functions. It returns EOK on success or negative error code.

ddf_fun_bind

int ddf_fun_bind(ddf_fun_t *fun)

Bind function fun to the system. This effectively makes fun visible to the system. Between creating and binding a function the caller has a chance to set any properties on the function that should be already set when the function becomes visible to the system. ddf_fun_bind returns EOK on success, negative error code on error. Specifically this function fails if a (bound) function with conflicting name already exists in the system. If ddf_fun_bind fails, fun is unchanged (specifically, it is not destroyed automatically).

ddf_fun_unbind

int ddf_fun_unbind(ddf_fun_t *fun)

Unbind function fun from the system. Makes the function fun unavailable to the system. Calling ddf_fun_unbind on an online inner function is interpreted as surprise removal (dev_gone will be called for the device under fun). Calling ddf_fun_unbind on an offline function is interpreted as anticipated removal. ddf_fun_unbind returns EOK on success, negative error code on failure. ddf_fun_unbind can fail if a child driver dev_remove or dev_gone entry point fails.

ddf_fun_online

int ddf_fun_online(ddf_fun_t *fun)

Bring function fun on line. This function is used from the fun_online entry point to online one or more functions. The DDF will set the state of the function to on line. If the function is an exposed function, corresponding Location service entry is created. If the function is an inner function, DDF will attach descendant devices to the function. ddf_fun_online returns EOK on success or negative error code.

ddf_fun_offline

int ddf_fun_offline(ddf_fun_t *fun)

Bring function fun off line. This function is used from the fun_offline entry point to offline one or more functions. The DDF will set the state of the function to off line. If the function is an exposed function, corresponding Location service entry is deleted. If the function is an inner function, DDF will recursively remove the device sub-tree under fun. ddf_fun_offline returns EOK on success or negative error code.

Soft State Management

A driver can associate driver-specific data with its devices, functions or both. The framework provides a way to allocate a block of memory (soft state) associated with a device or function and allows the driver to obtain pointer to this memory block. The framework frees the soft state automatically (and in such a way that it does not happen while the driver is still accessing it).

The driver is expected to synchronize access to this soft-state structure on its own in order to achieve mutual exclusion. Since the framework ensures that soft-state is not deallocated while the driver is accessing it, the synchronization can be as simple as a mutex embedded in the structure.

The soft-state management functions are:

  • ddf_dev_data_alloc() - allocate driver-specific device data
  • ddf_fun_data_alloc() - allocate driver-specific function data
void *ddf_dev_data_alloc(ddf_dev_t *dev, size_t size)

The function ddf_dev_data_alloc allocates a driver-specific data strutcure size bytes large, associated with the device dev. The structure will not be deallocated by the framework until the dev_remove or dev_gone entry point returns and control exits all driver entry points that are invoked with dev as a parameter. (Internally, this is achieved using reference counting).

void *ddf_fun_data_alloc(ddf_fun_t *fun, size_t size)

The function ddf_fun_data_alloc allocates a driver-specific data structure size bytes large, associated with the function fun. The structure will not be deallocated by the framework until ddf_fun_destroy() is called and control exits all driver entry points that are invoked with fun as parameter. 'ddf_dev_data_alloc is normally called from within the dev_add emtry point.

In practice, as long as you don't copy the pointer to device or function soft-state to a global/heap structure or pass it to another thread, it can never become dangling. As long as you have the reference, it is valid (i.e. points to an allocated block). ddf_fun_data_alloc is called on an unbound function (after creating, before binding).

Exposing Driver Services to Clients

All driver services are provided to clients (applications) via the location service. As such, they enjoy all benefits of such services, some of which we will note explicitly. For each bound exposed function node the device framework creates a service whose name is based on the physical path of that function. The services are created in the devices namespace. This is mapped to the /loc/devices directory via locfs. For example:

/# ls /loc/devices
\hw\pci0\00:01.0\com1\a
\hw\pci0\00:01.0\ctl
\hw\pci0\ctl
\virt\lo\port0

Each service currently running in the system is uniquely identified by a numerical identifier called service ID. Often the driver will want to add the function service to one or more service categories. Service categories imply the protocol spoken by a service (and possibly also the intended use). This can be done using the DDF function:

int ddf_fun_add_to_category(ddf_fun_t *fun, const char *cat_name)

Service consumers can find services by category. They can also register to receive notification when new services are registered. For example, the input server automatically picks up and opens any (device) service in the categories keyboard and mouse.

Traditional I/O Device Drivers

Programmed I/O

Interrupts

DMA

USB Device Drivers

USB drivers have their own standalone guide.