Writing a kernel device driver

Eventually, when you have exhausted all the user-space options above, you will find yourself having to write a device driver to access a piece of hardware attached to your device. While this is not the time or place to delve into details, it is worth considering the options. Character drivers are the most flexible and should cover 90% of all your needs; network devices apply if you are working with a network interface, and block devices 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 basic bones of a driver.

The main character device 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 that just read(2) and write(2):

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 with no quirks or oddities.

It's time to draw some threads together by looking at the code for a simple device driver.

The source code is provided for a device driver named dummy which creates four devices that are accessed through /dev/dummy0 to /dev/dummy3. This is the complete code for the driver:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#define DEVICE_NAME "dummy"
#define MAJOR_NUM 42
#define NUM_DEVICES 4

static struct class *dummy_class;
static int dummy_open(struct inode *inode, struct file *file)
{
  pr_info("%s\n", __func__);
  return 0;
}

static int dummy_release(struct inode *inode, struct file *file)
{
  pr_info("%s\n", __func__);
  return 0;
}

static ssize_t dummy_read(struct file *file,
  char *buffer, size_t length, loff_t * offset)
{
  pr_info("%s %u\n", __func__, length);
  return 0;
}

static ssize_t dummy_write(struct file *file,
  const char *buffer, size_t length, loff_t * offset)
{
  pr_info("%s %u\n", __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 loaded\n");
  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 unloaded\n");
}

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 module_init and module_exit specify the functions to be called when the module is loaded and unloaded. The other three add some basic information about the module which can be retrieved from the compiled kernel module using the modinfo command.

When the module is loaded, the dummy_init() function is called.

You can see the point at which it becomes a character device by calling register_chrdev, passing a pointer to struct file_operations containing 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 type 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 the /sys/class/dummy directory, containing the dummy0 to dummy3 subdirectories, each containing 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 exit function has to release the resources claimed by the init function which, here, means freeing up the device class and major number.

The file operation 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:

In this case, the messages appear because I was logged on to the console, and kernel messages are printed to the console by default.

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 data is moved between user and kernel spaces. Next, you need to build it.

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:

Set LINUXDIR to the directory of the kernel for your target device that you will be running the module on. The code obj-m := dummy.o will invoke the kernel build rule to take the source file, dummy.c and create kernel module, dummy.ko. Note that kernel modules are not binary compatible between kernel releases and configurations, the module will only load on the kernel it was compiled with.

The end result of the build is the kernel dummy.ko which you can copy to the target and load as shown in the next section.

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 that directory and add a line to build the driver unconditionally as a module, as follows:

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

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, Porting and Configuring the Kernel, when describing the kernel configuration.