Embedded Linux device drivers: Writing a kernel device driver

Editor's Note: Embedded Linux has consistently ranked among the top operating systems used in embedded system 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 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.

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


Chapter 9. Interfacing with Device Drivers (Continued)
By Chris Simmonds

Writing a kernel device driver

Eventually, when you have exhausted all the previous user space options, you will find yourself having to write a device driver to access a piece of hardware attached to your device. Character drivers are the most flexible and should cover 90% of all your needs; network drivers apply if you are working with a network interface and block drivers are for mass storage. The task of writing a kernel driver is complex and beyond the scope of this book. There are some references at the end that will help you on your way. In this section, I want to outline the options available for interacting with a driver—a topic not normally covered—and show you the bare bones of a character device driver.

Designing a character driver interface

The main character driver interface is based on a stream of bytes, as you would have with a serial port. However, many devices don't fit this description: a controller for a robot arm needs functions to move and rotate each joint, for example. Luckily, there are other ways to communicate with device drivers than just read and write:

  • ioctl : The ioctl function allows you to pass two arguments to your driver which can have any meaning you like. By convention, the first argument is a command, which selects one of several functions in your driver, and the second is a pointer to a structure, which serves as a container for the input and output parameters. This is a blank canvas that allows you to design any program interface you like. It is pretty common when the driver and application are closely linked and written by the same team. However, ioctl is deprecated in the kernel, and you will find it hard to get any drivers with new uses of ioctl accepted upstream. The kernel maintainers dislike ioctl because it makes kernel code and application code too interdependent, and it is hard to keep both of them in step across kernel versions and architectures.

  • sysfs : This is the preferred way now, a good example being the GPIO interface described earlier. The advantages are that it is somewhat self-documenting, so long as you choose descriptive names for the files. It is also scriptable because the file contents are usually text strings. On the other hand, the requirement for each file to contain a single value makes it hard to achieve atomicity if you need to change more than one value at a time. Conversely, ioctl passes all its arguments in a structure in a single function call.

  • mmap : You can get direct access to kernel buffers and hardware registers by mapping kernel memory into user space, bypassing the kernel. You may still need some kernel code to handle interrupts and DMA. There is a subsystem that encapsulates this idea, known as uio , which is short for user I/O. There is more documentation in Documentation/DocBook/uio-howto , and there are example drivers in drivers/uio.

  • sigio: You can send a signal from a driver using the kernel function named kill_fasync() to notify applications of an event such as input becoming ready or an interrupt being received. By convention, the signal called SIGIO is used, but it could be any. You can see some examples in the UIO driver, drivers/uio/uio.c, and in the RTC driver, drivers/char/rtc.c. The main problem is that it is difficult to write reliable signal handlers in user space, and so it remains a little-used facility.

  • debugfs : This is another pseudo filesystem that represents kernel data as files and directories, similar to proc and sysfs . The main distinction is that debugfs must not contain information that is needed for the normal operation of the system; it is for the debug and trace information only. It is mounted as mount -t debugfs debug /sys/kernel/debug . There is a good description of debugfs in the kernel documentation, Documentation/filesystems/debugfs.txt .

  • proc : The proc filesystem is deprecated for all new code unless it relates to processes, which was the original intended purpose for the filesystem. However, you can use proc to publish any information you choose. And, unlike sysfs and debugfs , it is available to non-GPL modules.

  • netlink : This is a socket protocol family. AF_NETLINK creates a socket that links kernel space to user space. It was originally created so that network tools could communicate with the Linux network code to access the routing tables and other details. It is also used by udev to pass events from the kernel to the udev It is very rarely used in general device drivers.

There are many examples of all of the preceding filesystem in the kernel source code, and you can design really interesting interfaces to your driver code. The only universal rule is the principle of least astonishment . In other words, application writers using your driver should find that everything works in a logical way without any quirks or oddities.

The anatomy of a device driver

It's time to draw some threads together by looking at the code for a simple device driver. Here is a device driver named dummy, which creates four devices that are accessed through dev/dummy0 to /dev/dummy3 . The complete source code for the driver follows: you will find the code in MELP/chapter_09/dummy-driver :

#include #include #include #include #include #define DEVICE_NAME "dummy"#define MAJOR_NUM 42#define NUM_DEVICES 4static struct class *dummy_class;static int dummy_open(struct inode *inode, struct file *file){    pr_info("%sn", __func__);    return 0;}static int dummy_release(struct inode *inode, struct file *file){    pr_info("%sn", __func__);    return 0;}static ssize_t dummy_read(struct file *file,            char *buffer, size_t length, loff_t * offset){    pr_info("%s %un", __func__, length);    return 0;}static ssize_t dummy_write(struct file *file,             const char *buffer, size_t length, loff_t * offset){    pr_info("%s %un", __func__, length);    return length;}struct file_operations dummy_fops = {    .owner = THIS_MODULE,    .open = dummy_open,    .release = dummy_release,    .read = dummy_read,    .write = dummy_write,};int __init dummy_init(void){    int ret;    int i;    printk("Dummy loadedn");    ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dummy_fops);    if (ret != 0)        return ret;    dummy_class = class_create(THIS_MODULE, DEVICE_NAME);    for (i = 0; i < NUM_DEVICES; i++) {        device_create(dummy_class, NULL,                  MKDEV(MAJOR_NUM, i), NULL, "dummy%d", i);    }    return 0;}void __exit dummy_exit(void){    int i;    for (i = 0; i < NUM_DEVICES; i++) {        device_destroy(dummy_class, MKDEV(MAJOR_NUM, i));    }    class_destroy(dummy_class);    unregister_chrdev(MAJOR_NUM, DEVICE_NAME);    printk("Dummy unloadedn");}module_init(dummy_init);module_exit(dummy_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Chris Simmonds");MODULE_DESCRIPTION("A dummy driver");

At the end of the code, the macros called module_init and module_exit specify the functions to be called when the module is loaded and unloaded. The three macros named MODULE_* add some basic information about the module, which can be retrieved from the compiled kernel module using the modinfo

When the module is loaded, the dummy_init() function is called. You can see the point at which it becomes a character device when is makes the call to register_chrdev , passing a pointer to struct file_operations , which contains pointers to the four functions that the driver implements. While register_chrdev tells the kernel that there is a driver with a major number of 42, it doesn't say anything about the class of driver, and so it will not create an entry in /sys/class . Without an entry in /sys/class , the device manager cannot create device nodes. So, the next few lines of code create a device class, dummy and four devices of that class called dummy0 to dummy3 . The result is that the /sys/class/dummy directory is created when the driver is initialized, containing subdirectories dummy0 to dummy3 . Each of the subdirectories contains a file, dev , with the major and minor numbers of the device. This is all that a device manager needs to create device nodes: /dev/dummy0 to /dev/dummy3 .

The dummy_exit function has to release the resources claimed by dummy_init , which here means freeing up the device class and major number.

The file operations for this driver are implemented by dummy_open() , dummy_read(), dummy_write(), and dummy_release() and are called when a user space program calls open(2), read(2), write(2), and close(2). They just print a kernel message so that you can see that they were called. You can demonstrate this from the command line using the echo command:

   # echo hello > /dev/dummy0
   dummy_open
   dummy_write 6
   dummy_release

In this case, the messages appear because I was logged on to the console, and kernel messages are printed to the console by default. If you are not logged onto the console, you can still see the kernel messages using the command dmesg .

The full source code for this driver is less than 100 lines, but it is enough to illustrate how the linkage between a device node and driver code works, how the device class is created, allowing a device manager to create device nodes automatically when the driver is loaded, and how the data is moved between user and kernel spaces. Next, you need to build it.

Compiling kernel modules

At this point, you have some driver code that you want to compile and test on your target system. You can copy it into the kernel source tree and modify makefiles to build it, or you can compile it as a module out of tree. Let's start by building out of tree.

You need a simple makefile which uses the kernel build system to do the hard work:

   LINUXDIR := $(HOME)/MELP/build/linux

   obj-m := dummy.o
   all:
           make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-
             -C $(LINUXDIR) M=$(shell pwd)
   clean:
           make -C $(LINUXDIR) M=$(shell pwd) clean

Set LINUXDIR to the directory of the kernel for your target device that you will be running the module on. The obj-m := dummy.o code will invoke the kernel build rule to take the source file, dummy.c , and create kernel module, dummy.ko . I will show you how to load kernel modules in the next section.

Kernel modules are not binary compatible between kernel releases and configurations: the module will only load on the kernel it was compiled with.

If you want to build a driver in the kernel source tree, the procedure is quite simple. Choose a directory appropriate to the type of driver you have. The driver is a basic character device, so I would put dummy.c in drivers/char . Then, edit the makefile in the directory, and add a line to build the driver unconditionally as a module, as follows:

   obj-m  += dummy.o

Or add the following line to build it unconditionally as a built-in:

   obj-y   += dummy.o

If you want to make the driver optional, you can add a menu option to the Kconfig file and make the compilation conditional on the configuration option, as I described in Chapter 4, Configuring and Building the Kernel, in the section, Understanding kernel configuration .

Loading kernel modules

You can load, unload, and list modules using the simple insmod, lsmod, and rmmod commands. Here they are shown loading the dummy driver:

# insmod /lib/modules/4.8.12-yocto-standard/kernel/drivers/dummy.ko
# lsmod
 
Tainted: G
dummy 2062 0 - Live 0xbf004000 (O)
# rmmod dummy

If the module is placed in a subdirectory in /lib/modules/ , you can create a modules dependency database using the command, depmod -a:

# depmod -a
# ls /lib/modules/4.8.12-yocto-standard
kernel modules.alias modules.dep modules.symbols

The information in the module.* files is used by the modprobe command to locate a module by name rather than the full path. modprobe has many other features, which are described on the manual page modprobe(8) .

The next article in this series will describe how to discover the system's hardware configuration.

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.