Overview of the different types of Linux device abstractions, with sample source code
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