A survey of Linux device drivers

Bill Gatliff, Freelance Embedded Consultant

May 15, 2006

Bill Gatliff, Freelance Embedded ConsultantMay 15, 2006

In Linux, a device driver is code that implements a userspace or kernelspace abstraction of a physical device. Examples of device drivers include code that allows user applications to stream data through a 16550 UART, code that configures an Epson S1D13xxx LCD controller chip, and code manages the AT91RM9200's built-in Ethernet controller.

Device drivers come in several different types, depending on what abstraction they provide. Serial port drivers, for example, often implement the character device type. The framebuffer driver type is normally used to enable userspace applications to write to an LCD or CRT display. Ethernet drivers allow the Linux kernel's TCP/IP protocol stack to send packets over an Ethernet network.

A device driver abstraction is purely that, an abstraction. The underlying hardware associated with an Ethernet device driver may not actually be an Ethernet controller, for example; there may not even be any physical hardware at all! Such is the case for the usbnet driver, which allows a USB Device to communicate with a USB Host as though the two were connected via Ethernet. (The usbnet driver packages Ethernet packets and forwards them to the USB host/device drivers for transmission).

There may be more than one device driver abstraction associated with a particular piece of hardware. A multifunction chip, like the Silicon Motion SM501 Multimedia Controller, will usually have one or more associated framebuffer drivers for its video controllers, a USB Host bus controller driver for its OHCI Host port, an AC97 codec driver for its AC97-Link interface, and a character driver for its serial ports.

Device nodes
For many device driver types, user applications communicate with the driver via a pseudofile called a device node. Device nodes look like files, and applications can even open() and close() them. But when data is written to a device node, the data is passed to the node's associated driver, and not stored in a filesystem.

To send data out a serial port, for example, an application will often open and write data to a device node named /dev/ttyS0. The device driver associated with that node handles enabling and disabling interrupts on the UART, queuing bytes for transmission, and deactivating the UART when transmission is complete.

The name of a device node is arbitrary, but there are some conventions. Serial ports are often called ttyS, and framebuffers are often called fb, for example. But the association between the device node and the device driver is actually controlled via the device's major number and minor number. Renaming the /dev/ttyS0 device node to /dev/cdrom does not magically transform the device node into one that streams data to a CD-ROM device!

You can see the major and minor numbers associated with a device node using the ls command:

$ ls -l /dev/console
crw-------1 bgat root 5, 1 Mar 7 08:30 /dev/console

The /dev/console device node, which helps applications communicate with their associated display device, is major number 5, minor number 1. Within the Linux kernel sources, a device driver (the console device driver) registers itself as "driver major number 5", which associates it with the /dev/console device node.

The crw in the above output indicates that the console device node expects to be associated with a character device abstraction, and may be both read from and written to.

Character Devices
The character device driver implements a byte-oriented interface that can be read from or written to. The interface is also stream-oriented, meaning that you cannot "seek" forwards or backwards through the data the way you can with an ordinary data file. Character devices are appropriate for things like serial port drivers, etc.

The most important data structure used by character device drivers is the file_operations structure, a portion of which appears below:

Character device drivers at a minimum must implement the open() and release() methods, but usually also implement the read() and write() methods as well. The poll() method assists applications in "sleeping" until data is available, and the ioctl() method provides an out-of-band channel often used for things like specifying the bit rate and parity settings in a UART driver's associated hardware device.

About struct cdev
Later Linux-2.6 kernels offer a struct cdev structure, which encapsulates the file_operations structure and some other important driver information. By using cdev, character devices gain a more exible and uniform interface to the kernel's internal character device resources, including proc filesystem entries and udev.

All new character drivers are expected to use cdev; existing drivers are being migrated as time permits.

ioctl()
The ioctl() entry point in a character device driver provides for out-of-band communications with the device driver itself. Common uses for this capability are to assert bit rate and parity settings for serial port hardware, and to change framebuffer modes.

A character device driver's ioctl method looks like this:

This example prints the parameters passed via the ioctl() system call. Device drivers that offer ioctls provide enumerations for the cmd parameter, and when those commands need arguments, the arg parameter can pass either an unsigned long parameter value, or the address of a data structure containing a collection of parameter values.

An application with a tweak_example_device() function that sends a hypothetical MYDRIVER_IOCTL1 command to a driver might look like this:

A common usage for ioctls is to enable the "user interrupt" feature of the real-time clocks used in many PCs. The code to do that might look like this:

Interrupt Handlers
Under Linux, interrupt handlers are not confined to device drivers. Functions not associated with device drivers may also bind to interrupt sources. An LED that blinks when a button is pressed is most easily done using a simple interrupt handler, for example.

Interrupt handler functions bind to interrupt sources using the kernel's request_irq() function. Under Linux interrupt handlers are ordinary C functions, but they are restricted in the kernel features they may use. Interrupt handlers cannot block on semaphores, for example.

A simple interrupt handler for a hypothetical interrupt source named GPIO_PIN_PB29 looks like this:

This interrupt handler disables the associated interrupt source when ten interrupts are detected (a button is pressed ten times, for example). Assuming the host processor provides a suitable definition for GPIO_PIN_PB29, the handler may be bound to the interrupt source using something similar to the following code:

A call to request_irq() may fail if a previously-registered interrupt handler did not permit the source to be shared with other handlers.

Block Devices
A block device driver implements an abstraction commonly associated with hard drives and other data-block-oriented media. A block device offers persistent storage, which (generally) allows applications to "seek" data within the device. Properly-implemented block device drivers can be controlled by the kernel's Virtual File System functionality, allowing all manner of devices to be used to store nearly all kernel-supported filesystems with little additional effort.

Block device drivers use the same file_operations structure used by character device drivers, but use structure members that are ignored by character devices. Block device drivers are more complex than character device drivers. The best example of a block device driver is "sbull", from Rubini's Linux Device Drivers.

MTD Chips and Maps
The Memory Technology Devices (MTD) implementation uses a layered driver approach. Chip drivers control the memory device itself, and map drivers use chip drivers to interact with memory chips using either a block or character device API. The JFFS2 filesystem depends on MTD, but MTD may also be used with other filesystems.

Among other things, MTD chip device drivers can probe ash memory chips and similar media to identify their geometry. This information allows MTD to properly erase and program the device. With the rise of the Common Flash Interface (CFI) protocol, the need to implement new chip device drivers is fast becoming history.

MTD map drivers provide the lowest-level read/write functionality for ash media, which is useful for hardware that makes ash memory available in pages rather than one large window in the host's memory space (very large ash chips may exceed the memory space accessible by the hardware). Map drivers also tell MTD the physical addresses of available ash memory, and can be used to limit MTD's accesses to only certain areas of that memory.

MTD block device nodes are usually named /dev/mtdblock. MTD character device nodes are usually named /dev/mtd.

Framebuffers
Framebuffer device drivers provide for direct userspace access to video frame buffers: the memory space used by an LCD controller to store the image actually visible on the LCD panel. XFree86, Qtopia, Microwindows and other GUI libraries interact directly with frame buffer memory.

Framebuffer drivers are mostly informational; they provide standardized interfaces that allow applications to set and query video modes and refresh rates. User applications use the framebuffer driver's mmap() system call to locate frame buffer memory.

The properties of a framebuffer driver's associated hardware are stored in a struct fb_info structure. This structure also contains the physical address of the frame buffer memory, and the physical addresses of the registers that control display modes, geometry and timing.

Framebuffer drivers also use a struct fb_ops structure, which is roughly equivalent to the struct file_operations structure used by character and block device drivers. Applications like fbset use a standardized set of ioctls to invoke these frame buffer "operations".

Framebuffer device nodes are usually named /dev/fb. One way to tell if your kernel contains a working framebuffer driver is to look for Tux, the Linux mascot, on the display panel during kernel startup.

I2C Bus Interfaces and Chips
The Linux I2C implementation uses a layered stack of device drivers to control chips connected to an I2C-compatible bus. At the lowest level are "algorithm" and "bus" drivers, which provide a uniform abstraction for interacting with the physical I2C media. I2C bus implementations vary, from simple "bit bang" GPIO-based signaling to high-performance dedicated peripherals.

I2C "chip" drivers control a member of an I2C bus, including temperature sensors, GPIO controllers, ADCs, and so forth. The uniformity provided by I2C algorithm and bus drivers allow the same chip driver to be used regardless of how the actual I2C bus is implemented.

I2C chip drivers provide a detect() function that allows the I2C system to confirm the presence of the associated device. If the device is found, the driver registers itself with the I2C system using the i2c_attach_client() function.

Once registered, I2C chip drivers vary widely in the kernel resources they use. Touch screen controller chip drivers, for example, may interact with the kernel's input subsystem, interrupt handlers, proc filesystem, and other functionality. Temperature and fan speed sensors are more uniform, so that userspace monitoring applications will work across the wide range of devices employed by PCs and embedded hardware.

Ethernet Devices
Ethernet drivers provide an Ethernet-specific abstraction that focuses almost exclusively on data exchange with the network media access controller (MAC). Other network-related functions, like IP packet assembly and decoding, are handled within the kernel's network protocol stacks and do not directly influence the implementation of an Ethernet device driver.

The most common reason for getting involved with an Ethernet device driver is to add support for a new physical access controller (PHY). In an existing Ethernet device driver's probe() function, the PHY must often be identified so that the Ethernet network controller can be properly configured.

The network controller generally provides control registers for communicating with an attached PHY, including reading the device's ID register. Often, the only change required is to add an enumeration so that the new PHY will be recognized during probing.

The Platform Model
The Platform Model offered by kernel versions 2.6 and beyond provides a refined way to attach devices to drivers that eliminates the need for device drivers to contain hard coded physical addresses of the devices they control. The platform model also prevents resource conflicts and improves kernel portability, helps get startup ordering right, and integrates with the kernel's power management features.

Under the platform model, device drivers know how to control a device once informed of its physical location and interrupt lines. This information is provided to the driver in the form of a \resource list" passed to the driver during probing.

A framebuffer driver needs to know the physical addresses of the frame buffer memory and control registers. The resource list might look like this:

Once the resources are defined, that information is merged into a platform_device structure:

Finally, the resource list and associated driver name are registered:

platform_device_register(&lcd_panel_dev);

At some point during kernel startup, a device driver named "s1d13xxxfb" may register itself. After registration, the platform model implementation invokes the driver's probe() function, passing it the associated resource list. Helper functions allow the driver to extract resource entries from the list and thereby locate the device in question.

Bill Gatliff is a freelance embedded developer and training consultant with 10 years of experience using GNU and other tools for building embedded systems targeting automotive, aerospace, and medical instrumentation applications. He is a contributing editor for Embedded Systems Design, author of the Embedded GNU Jumpstart and Embedded Linux Jumpstart series of training materials.

This paper was written for and presented at the Embedded Systems Conference Silicon Valley 2006. For more information, please visit www.embedded.com/esc/sv


Loading comments...