Linux Device Driver Development Cookbook
上QQ阅读APP看书,第一时间看更新

How to do it...

Let's look into the chrdev_legacy.c file from GitHub sources. We have our very first driver, so let's start and examine it in detail:

  1. First of all, let's take a look at the beginning of the file:
#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>

/* Device major umber */
static int major;
  1. At the end of chrdev_legacy.c, check the following code where the module's init() function is defined as below:
static int __init chrdev_init(void)
{
int ret;

ret = register_chrdev(0, "chrdev", &chrdev_fops);
if (ret < 0) {
pr_err("unable to register char device! Error %d\n", ret);
return ret;
}
major = ret;
pr_info("got major %d\n", major);

return 0;
}

And the module's exit() function looks like the following:

static void __exit chrdev_exit(void)
{
unregister_chrdev(major, "chrdev");
}

module_init(chrdev_init);
module_exit(chrdev_exit);
  1. If the major number is the driver reference into the kernel from the user space, the file operations structure (referenced by chrdev_fops) represents the only allowed system calls that we can execute on our driver, and they are defined as follows:
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.read = chrdev_read,
.write = chrdev_write,
.open = chrdev_open,
.release = chrdev_release
};
  1. Methods are then basically implemented as follows. Here are the read() and write() methods:
static ssize_t chrdev_read(struct file *filp,
char __user *buf, size_t count,
loff_t *ppos)
{
pr_info("return EOF\n");

return 0;
}

static ssize_t chrdev_write(struct file *filp,
const char __user *buf, size_t count,
loff_t *ppos)
{
pr_info("got %ld bytes\n", count);

return count;
}

While here are the open() and release() (aka the close()) methods:

static int chrdev_open(struct inode *inode, struct file *filp)
{
pr_info("chrdev opened\n");

return 0;
}

static int chrdev_release(struct inode *inode, struct file *filp)
{
pr_info("chrdev released\n");

return 0;
}
  1. To compile the code, we can do it the usual way on the host machine, as follows:
$ make KERNEL_DIR=../../../linux/
make -C ../../../linux/ \
ARCH=arm64 \
CROSS_COMPILE=aarch64-linux-gnu- \
SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy modules
make[1]: Entering directory '/home/giometti/Projects/ldddc/linux'
CC [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.mod.o
LD [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.ko
make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux'
  1. Then, to test our driver, we can load it in our target system (again we can use the scp command to load the module file into the ESPRESSObin):
# insmod chrdev_legacy.ko 
chrdev_legacy: loading out-of-tree module taints kernel.
chrdev_legacy:chrdev_init: got major 239

OK. The driver has been loaded and our major number is 239.

  1. As a final note, let me suggest you take a look into the /proc/devices file on the ESPRESSObin. This special file is generated on the fly when someone reads it and it holds all character (and block) drivers registered into the system; that's why we should find something as follows if we filter it with the grep command:
# grep chrdev /proc/devices 
239 chrdev
Of course, your major number can be a different number! There's nothing strange about that; just rewrite the next commands according to the number you get.
  1. To effectively execute some system calls on our driver, we can use the program stored in the chrdev_test.c file (still from GitHub sources); the beginning of its main() function looks like the following:
int main(int argc, char *argv[])
{
int fd;
char buf[] = "DUMMY DATA";
int n, c;
int ret;

if (argc < 2) {
fprintf(stderr, "usage: %s <dev>\n", argv[0]);
exit(EXIT_FAILURE);
}

ret = open(argv[1], O_RDWR);
if (ret < 0) {
perror("open");
exit(EXIT_FAILURE);
}
printf("file %s opened\n", argv[1]);
fd = ret;

  1. First of all, we need to open the file device and then get a file descriptor; this can be done by using the open() system call.
  1. Then, the main() function continues, as follow by writing data in the device:
    for (c = 0; c < sizeof(buf); c += n) {
ret = write(fd, buf + c, sizeof(buf) - c);
if (ret < 0) {
perror("write");
exit(EXIT_FAILURE);
}
n = ret;

printf("wrote %d bytes into file %s\n", n, argv[1]);
dump("data written are: ", buf + c, n);
}

And the by reading just written data from it:

    for (c = 0; c < sizeof(buf); c += n) {
ret = read(fd, buf, sizeof(buf));
if (ret == 0) {
printf("read EOF\n");
break;
} else if (ret < 0) {
perror("read");
exit(EXIT_FAILURE);
}
n = ret;

printf("read %d bytes from file %s\n", n, argv[1]);
dump("data read are: ", buf, n);
}

After the device opens, our program performs write() followed by a read() system call.

We should notice that I call  read()  and  write()  system calls inside a  for()  loop; the reason behind this implementation will be clearer i n the following recipe,  Exchanging data with a char driver, where we're going to see how these system calls actually work .
  1. Finally, main() can close the file device and then exit:
    close(fd);

return 0;
}

In this manner, we can test the system calls we implemented earlier.