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.

Designing a character device interface

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):

  • 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 and 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 self-documenting, so long as you choose descriptive names for the files. It is also scriptable because the file contents are ASCII 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. For example, if you want to set two values and then initiate an action, you would need to write to three files: two for the inputs and a third to trigger the action. Even then, there is no guarantee that the other two files have not been changed by someone else. 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, 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 kill_fasync() to notify applications of an event such as input becoming ready or an interrupt being received. By convention, signal SIGIO is used, but it could be anyone. 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 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 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, the 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 daemon. 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 with no quirks or oddities.

Anatomy of a device driver

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
", __func__);
  return 0;
}

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

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

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:

# echo hello > /dev/dummy0

[ 6479.741192] dummy_open
[ 6479.742505] dummy_write 6
[ 6479.743008] 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.

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.

Compile and load

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 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:

obj-m  += dummy.o

Or add the following line this 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, Porting and Configuring the Kernel, when describing the kernel configuration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.199.138