Embedded Linux device drivers: Understanding their role

Editor's Note: Embedded Linux has consistently ranked among the top operating systems used in embedded systems design. With the rapid growth in interest in the Internet of Things (IoT), the ability of embedded Linux to serve multiple roles will prove vital in supporting diverse needs found at each layer of the IoT application hierarchy. In turn, the ability of engineers to master embedded Linux systems will become critical for achieving rapid, reliable development of more sophisticated systems. In Mastering Embedded Linux Programming – Second Edition, author Chris Simmonds takes the reader on a detailed tour across the breadth and depth of this important operating system, using detailed examples to illustrate each key point.

In this excerpt, Chapter 9, from the book, the author describes how kernel device drivers interact with system hardware and how developers can write device drivers and use them in their applications. The following installments present this excerpt on Embedded Linux device drivers:

Understanding their role

Reading driver state at runtime

Device drivers in user space

Writing a kernel device driver

Discovering the hardware configuration

Adapted from Mastering Embedded Linux Programming – Second Edition, by Chris Simmonds.

Chapter 9. Interfacing with Device Drivers

Kernel device drivers are the mechanism through which the underlying hardware is exposed to the rest of the system. As a developer of embedded systems, you need to know how these device drivers fit into the overall architecture and how to access them from user space programs. Your system will probably have some novel pieces of hardware, and you will have to work out a way of accessing them. In many cases, you will find that there are device drivers provided for you, and you can achieve everything you want without writing any kernel code. For example, you can manipulate GPIO pins and LEDs using files in sysfs , and there are libraries to access serial buses, including SPI (Serial Peripheral Interface) and I 2 C (Inter-Integrated Circuit).

There are many places to find out how to write a device driver, but few to tell you why you would want to and the choices you have in doing so. This is what I want to cover here. However, remember that this is not a book dedicated to writing kernel device drivers and that the information given here is to help you navigate the territory but not necessarily to set up home there. There are many good books and articles that will help you to write device drivers, some of which are listed at the end of this chapter.

In this chapter we will cover the following topics:

  • The role of device drivers

  • Character devices

  • Block devices

  • Network devices

  • Finding out about drivers at runtime

  • Finding the right device driver

  • Device drivers in user space

  • Writing a kernel device driver

  • Discovering the hardware configuration

The role of device drivers

As I mentioned in Chapter 4, Configuring and Building the Kernel , one of the functions of the kernel is to encapsulate the many hardware interfaces of a computer system and present them in a consistent manner to user space programs. The kernel has frameworks designed to make it easy to write a device driver, which is the piece of code that mediates between the kernel above and the hardware below. A device driver maybe written to control physical devices such as a UART or an MMC controller, or it may represent a virtual device such as the null device ( /dev/null ) or a ramdisk. One driver may control multiple devices of the same kind.

Kernel device driver code runs at a high privilege level, as does the rest of the kernel. It has full access to the processor address space and hardware registers. It can handle interrupts and DMA transfers. It can make use of the sophisticated kernel infrastructure for synchronization and memory management. However, you should be aware that there is a downside to this; if something goes wrong in a buggy driver, it can go really wrong and bring the system down. Consequently, there is a principle that device drivers should be as simple as possible by just providing information to applications where the real decisions are made. You often hear this being expressed as no policy in the kernel . It is the responsibility of user space to set the policy that governs the overall behavior of the system. For example, the loading of kernel modules in response to external events, such as plugging in a new USB device, is the responsibility of the user space program, udev , not the kernel. The kernel just supplies a means of loading a kernel module.

In Linux, there are three main types of device driver:

  • Character : This is for an unbuffered I/O with a rich range of functions and a thin layer between the application code and the driver. It is the first choice when implementing custom device drivers.

  • Block : This has an interface tailored for block I/O to and from mass storage devices. There is a thick layer of buffering designed to make disk reads and writes as fast as possible, which makes it unsuitable for anything else.

  • Network : This is similar to a block device but is used for transmitting and receiving network packets rather than disk blocks.

There is also a fourth type that presents itself as a group of files in one of the pseudo file systems. For example, you might access the GPIO driver through a group of files in /sys/class/gpio , as I will describe later on in this chapter. Let's begin by looking in more detail at the three basic device types.

Character devices

Character devices are identified in user space by a special file called a device node . This file name is mapped to a device driver using the major and minor numbers associated with it. Broadly speaking, the major number maps the device node to a particular device driver, and the minor number tells the driver which interface is being accessed. For example, the device node of the first serial port on the ARM Versatile PB is named /dev/ttyAMA0 , and it has major number 204 and minor number 64. The device node for the second serial port has the same major number, since it is handled by the same device driver, but the minor number is 65. We can see the numbers for all four serial ports from the directory listing here:

# ls -l /dev/ttyAMA*crw-rw---- 1 root root 204, 64 Jan 1 1970 /dev/ttyAMA0crw-rw---- 1 root root 204, 65 Jan 1 1970 /dev/ttyAMA1crw-rw---- 1 root root 204, 66 Jan 1 1970 /dev/ttyAMA2crw-rw---- 1 root root 204, 67 Jan 1 1970 /dev/ttyAMA3

The list of standard major and minor numbers can be found in the kernel documentation in Documentation/devices.txt . The list does not get updated very often and does not include the ttyAMA device described in the preceding paragraph. Nevertheless, if you look at the kernel source code in drivers/tty/serial/amba-pl011.c , you will see where the major and minor numbers are declared:

  #define SERIAL_AMBA_MAJOR       204  #define SERIAL_AMBA_MINOR       64

Where there is more than one instance of a device, as with the ttyAMA driver, the convention for forming the name of the device node is to take a base name, ttyAMA, and append the instance number from 0 to 3 in this example.

As I mentioned in Chapter 5, Building a Root Filesystem , the device nodes can be created in several ways:

  • devtmpfs : The device node is created when the device driver registers a new device interface using a base name supplied by the driver (ttyAMA ) and an instance number.

  • udev  or mdev (without devtmpfs ): Essentially the same as with devtmpfs , except that a user space daemon program has to extract the device name from sysfs and create the node. I will talk about sysfs

  • mknod : If you are using static device nodes, they are created manually using mknod .

You may have the impression from the numbers I have used above that both major and minor numbers are 8-bit numbers in the range 0 to 255. In fact, from Linux 2.6 onwards, the major number is 12 bits long, which gives valid numbers from 1 to 4,095, and the minor number is 20 bits, from 0 to 1,048,575.

When you open a character device node, the kernel checks to see whether the major and minor numbers fall into a range registered by a character device driver . If so, it passes the call to the driver, otherwise the open call fails. The device driver can extract the minor number to find out which hardware interface to use.

To write a program that accesses a device driver, you have to have some knowledge of how it works. In other words, a device driver is not the same as a file: the things you do with it change the state of the device. A simple example is the pseudo random number generator, urandom , which returns bytes of random data every time you read it. Here is a program that does just this (you will find the code in MELP/chapter_09/read-urandom):

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h> int main(void){            int f;            unsigned int rnd;            int n;             f = open("/dev/urandom", O_RDONLY);            if (f < 0) {                        perror("Failed to open urandom");                        return 1;            }            n = read(f, &rnd, sizeof(rnd));            if (n != sizeof(rnd)) {                        perror("Problem reading urandom");                        return 1;            }            printf("Random number = 0x%xn", rnd);            close(f);            return 0;}

The nice thing about the Unix driver model is that once we know that there is a device named urandom and that every time we read from it, it returns a fresh set of pseudo random data, we don't need to know anything else about it. We can just use standard functions such as open(2) , read(2) , and close(2) .

You could use the stream I/O functions, fopen(3) , fread(3) , and fclose(3) instead, but the buffering implicit in these functions often causes unexpected behavior. For example, fwrite(3) usually only writes to the user space buffer, not to the device. You would need to call fflush(3) to force the buffer to be written out. Therefore, it is best to not use stream I/O functions when calling device drivers.

Block devices

Block devices are also associated with a device node, which also has major and minor numbers.

Although character and block devices are identified using major and minor numbers, they are in different namespaces. A character driver with a major number 4 is in no way related to a block driver with a major number 4.

With block devices, the major number is used to identify the device driver and the minor number is used to identify the partition. Let's look at the MMC driver on the BeagleBone Black as an example:

# ls -l /dev/mmcblk*brw-rw---- 1 root disk 179, 0   Jan 1 2000 /dev/mmcblk0brw-rw---- 1 root disk 179, 1   Jan 1 2000 /dev/mmcblk0p1brw-rw---- 1 root disk 179, 2   Jan 1 2000 /dev/mmcblk0p2brw-rw---- 1 root disk 179, 8   Jan 1 2000 /dev/mmcblk1brw-rw---- 1 root disk 179, 16  Jan 1 2000 /dev/mmcblk1boot0brw-rw---- 1 root disk 179, 24  Jan 1 2000 /dev/mmcblk1boot1brw-rw---- 1 root disk 179, 9   Jan 1 2000 /dev/mmcblk1p1brw-rw---- 1 root disk 179, 10  Jan 1 2000 /dev/mmcblk1p2

Here, mmcblk0 is the microSD card slot, which has a card that has two partitions, and mmcblk1 is the eMMC chip that also has two partitions. The major number for the MMC block driver is 179 (you can look it up in devices.txt ). The minor numbers are used in ranges to identify different physical MMC devices, and the partitions of the storage medium that are on that device. In the case of the MMC driver, the ranges are eight minor numbers per device: the minor numbers from 0 to 7 are for the first device, the numbers from 8 to 15 are for the second, and so on. Within each range, the first minor number represents the entire device as raw sectors, and the others represent up to seven partitions. On eMMC chips, there are two 128 KiB areas of memory reserved for use by a bootloader. These are represented as devices: mmcblk1boot0 and mmcblk1boot1 , and they have minor numbers 16 and 24.

As another example, you are probably aware of the SCSI disk driver, known as sd, which is used to control a range of disks that use the SCSI command set, which includes SCSI, SATA, USB mass storage, and universal flash storage (UFS). It has the major number 8 and ranges of 16 minor numbers per interface (or disk). The minor numbers from 0 to 15 are for the first interface with device nodes named sda up to sda15 , the numbers from 16 to 31 are for the second disk with device nodes sdb up to sdb15, and so on. This continues up to the 16 disk from 240 to 255 with the node name sdp . There are other major numbers reserved for them because SCSI disks are so popular, but we needn't worry about that here.

Both the MMC and SCSI block drivers expect to find a partiton table at the start of the disk. The partition table is created using utilities such as fdisk , sfidsk , or parted .

A user space program can open and interact with a block device directly via the device node. This is not a common thing to do, though, and is usually only done to perform administrative operations such as creating partitions, formatting a partition with a filesystem, and mounting. Once the filesystem is mounted, you interact with the block device indirectly through the files in that filesystem.

Network devices

Network devices are not accessed through device nodes, and they do not have major and minor numbers. Instead, a network device is allocated a name by the kernel, based on a string and an instance number. Here is an example of the way a network driver registers an interface:

  my_netdev = alloc_netdev(0, "net%d", NET_NAME_UNKNOWN, netdev_setup);  ret = register_netdev(my_netdev);

This creates a network device named net0 the first time it is called, net1 the second time, and so on. More common names are lo , eth0 , and wlan0 . Note that this is the name it starts off with; device managers, such as udev , may change it to something different later on.

Usually, the network interface name is only used when configuring the network using utilities, such as ip and ifconfig , to establish a network address and route. Thereafter, you interact with the network driver indirectly by opening sockets, and letting the network layer decide how to route them to the right interface.

However, it is possible to access network devices directly from user space by creating a socket and using the ioctl commands listed in include/linux/sockios.h . For example, this program uses SIOCGIFHWADDR to query the driver for the hardware (MAC) address (the code is in MELP/chapter_09/show-mac-addresses):

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/ioctl.h>#include <linux/sockios.h>#include <net/if.h> int main(int argc, char *argv[]){            int s;            int ret;            struct ifreq ifr;            int i;            if (argc != 2) {                        printf("Usage %s [network interface]n", argv[0]);                        return 1;            }            s = socket(PF_INET, SOCK_DGRAM, 0);            if (s < 0) {                        perror("socket");                        return 1;            }            strcpy(ifr.ifr_name, argv[1]);            ret = ioctl(s, SIOCGIFHWADDR, &ifr);            if (ret < 0) {                        perror("ioctl");                        return 1;            }            for (i = 0; i < 6; i++)                        printf("%02x:", (unsigned char)ifr.ifr_hwaddr.sa_data[i]);            printf("n");            close(s);            return 0;}

The next article in this series will describe how to find out about drivers at runtime.

Reprinted with permission from Packt Publishing. Copyright © 2017 Packt Publishing


Chris Simmonds is a software consultant and trainer living in southern England. He has almost two decades of experience in designing and building open-source embedded systems. He is the founder and chief consultant at 2net Ltd, which provides professional training and mentoring services in embedded Linux, Linux device drivers, and Android platform development. He has trained engineers at many of the biggest companies in the embedded world, including ARM, Qualcomm, Intel, Ericsson, and General Dynamics. He is a frequent presenter at open source and embedded conferences, including the Embedded Linux Conference and Embedded World. You can see some of his work on the Inner Penguin blog at www.2net.co.uk.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.