Advertisement

Embedded Linux device drivers: Writing a kernel device driver

August 14, 2018

CHRIS.SIMMONDS_#1-August 14, 2018

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 <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");

Continue reading on page two >>

 

< Previous
Page 1 of 2
Next >

Loading comments...

Most Commented