GNU-Linux Rapid Embedded Programming
上QQ阅读APP看书,第一时间看更新

What is a device driver?

A device driver is a special code that interfaces a physical device into the system and exports it to the user-space processes using a well-defined API. In a UNIX-like OS, where everything is a file, the physical device is represented as a file. Then, the device driver implements all the system calls a process can do on a file.

Tip

The difference between a normal C function and a system call is just the fact that the latter is mainly executed into the kernel while a function executes into the user space only. For example, printf() is a function while write() is a system call. The latter (except for the prologue and epilogue part of a C function) executes into the kernel space, while the former executes into the user space (even if it calls write() to actually write its data to the output stream). The system calls are used to communicate with the peripherals, with other processes, and to get access to the kernel's internal data. That's why a system call triggers a switch from user space to kernel space where important code is executed, and after execution, the code switched back to user space to execute normal code. For this reason, the code executed in the kernel space is considered a code that is executed in a privileged mode.

As an example, let's consider the GPIO subsystem we already saw in the previous chapter where we talked about U-Boot. In Linux, we can manage these devices easily using some files in sysfs (we'll discuss more in detail in the following paragraphs). For each GPIO lines, we got a directory called /sys/class/gpio/gpioXX/ where we can find the value and direction files. Each read() system calls on the value file (for example, by issuing the cat /sys/class/gpio/gpioXX/value command) and is translated by the kernel in the gpio_read() kernel method that actually does the reading of the gpioXX status.

Tip

At the moment, we cannot still try these commands, so the reader should believe that this is actually what happens! Otherwise, they can skip to Chapter 6 , General Purposes Input Output signals - GPIO , where GPIOs management is shown.

When we do a read() system call on another file under the /dev directory, the kernel translates the read() system call into the corresponding device driver's method that actually executes the reading.

Note that the system call is always the same (read()), but inside the kernel, the right method is called each time! You should imagine that this mechanism works like an object programming language: read() is a method that operates in a different manner according to the object (device) passed to it.

Tip

For further information on how this complex mechanism exactly works and for everything about the device drivers in Linux, you can take a look at the book Linux Device Drivers, Third Edition available at the bookshop and online at:  http://lwn.net/Kernel/LDD3/ .

Char, block, and net devices

In the Linux kernel, three major device types exist:

  • Char device : This kind of device groups all the peripherals that can be accessed as a stream of bytes, such as a file (that is, serial ports, audio devices, and so on). A char driver is in charge of implementing this behavior usually by implementing at least the open(), close(), read(), and write() system calls. Char device drivers have the ioctl() system call that allows the developer to invent any interface necessary (it acts as a general purpose method).
  • Block device : This kind of device groups all the peripherals that can host a filesystem, so it is accessed as a block of bytes (usually 512 or a larger power of two).
  • Net device : This kind of device groups all the peripherals that can manage a network transaction. In a different manner from char and block devices, these special devices have no related filesystem nodes, such as /dev/ttyACM0 or /dev/sdb1.

A driver interfacing, a char device is usually called char driver, while a driver for a block device is called block driver, and of course, the net driver is the driver for a net device.

Despite these three major groups, in recent kernel releases, we can find several subgroups (or classes) of device drivers that are still based on one of the major groups but that are specialized in managing a particular device type, for example, the real-time clock (RTC) devices that are represented by a dedicated device driver class defined under the /drivers/rtc directory in the Linux source tree. In the same manner, the Pulse Per Second devices (PPS) have a dedicated device driver class defined under the /drivers/pps directory, and the same is the case for the input devices (mice, keyboards, and so on) that are defined under the /drivers/input directory. All these specific device drivers are implemented using the char drivers.

Another way to interact with a device is to use the sysfs filesystem (see the relative section below in this chapter). Strictly speaking, this not regular a device driver, that is, it's not implemented as a char or block or net device, but it uses a different API. It uses an in-memory representation of the device's internals that permits to get access to it in a simple and clean way using the file abstraction (that is everything is a file).

Modules versus built-in devices

The Linux kernel holds by default a lot of device drivers, but it may happen that we need to install into the system a recent one not yet imported into the kernel tree for several reasons (that is, the driver is very new, or nobody asked for its insertion, or just because we write it ourselves!). In this case, we need to know some techniques about how a device driver can be compiled (advanced details about a device driver and how it can be used to exchange data with a peripheral will be explained in detail in the following chapters).

The device driver compilation steps may vary, and two major possibilities exist:

  1. ​The driver's source code is a patch to be applied to the kernel tree.
  2. ​The driver's source code has a standard Makefile compatible with Linux'sfile.

The first case is quite simple since after the device driver's patch has been applied, the developer just needs to recompile the kernel. In this case, the driver can be compiled as kernel built-in or as kernel module.

Note

A kernel module is a special binary file that can be inserted in the kernel at runtime when a specific functionality is requested. This prevents us from having a very large kernel image. In fact, we can select which functionalities are required since the boot and which ones can be loaded later on a demand basis. For example, when a new device is inserted into the system, the kernel may ask for loading a kernel module that holds the corresponding device driver. However, a module may also be built as a monolithic part of the kernel (kernel built-in).

The first case is just a normal kernel recompilation, while the latter case is a bit more complex, but all the complexity is managed by Makefile. The user has to properly configure it and then execute the make command only.

When a device driver code is not merged into the kernel sources, then the driver can be compiled as a kernel module only!

We just saw how to write a simple kernel module. Right now, we've to see the tools to effectively manage such modules.

The modutils

The basic command to load a module into the kernel is insmod. However, another command exists to load a module (and its dependencies), and its name is modprobe.

Actually, there is a group of commands to manage the kernel modules. They are called the modutils. On a Debian or Ubuntu system, the modutils are stored into a package named kmod:

$ apt-cache show kmod
Package: kmod
Priority: important
Section: admin
Installed-Size: 241
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Marco d'Itri <md@linux.it>
Architecture: amd64
Version: 22-1ubuntu4
Depends: libc6 (>= 2.17), libkmod2 (= 22-1ubuntu4), lsb-base (>= 4.1+D ebian11ubuntu7)
Breaks: oss-compat (= 4)
Filename: pool/main/k/kmod/kmod_22-1ubuntu4_amd64.deb
Size: 89122
MD5sum: bcfb58ca2dbc2f77137193b73c61590d
SHA1: 539d2410d0182f212b78a67b649135507c9fd9bb
SHA256: a65398f087ad47192e728ecbffe92e0363c03d229d72dc1d2f8b409880c9d0 ea
Description-en: tools for managing Linux kernel modules
This package contains a set of programs for loading, inserting, and
removing kernel modules for Linux.
It replaces module-init-tools.
...

The available commands in the preceding package can be listed here:

$ dpkg -L kmod | grep sbin\/
/sbin/insmod
/sbin/depmod
/sbin/modprobe
/sbin/rmmod
/sbin/lsmod
/sbin/modinfo

Let's see a bit of these commands in detail:

  • The insmod loads a module into the kernel.
  • The lsmod command shows to the user all the current modules loaded into the kernel. By running it on my PC, I get a long listing. So, here are a few lines:
     $ lsmod Module Size Used by pci_stub 16384 1 vboxpci 24576 0 vboxnetadp 28672 0 vboxnetflt 28672 0 vboxdrv 454656 3 vboxnetadp,vboxnetflt,vboxpci pl2303 20480 0 ftdi_sio 53248 0
    

    On the first column are reported all the modules currently loaded into my system. The second column is the module size in bytes, while the third column is the use count by other modules or user space accesses, which are listed in the fourth column.

  • The modprobe command is more complex than insmod, because it can handle the dependencies of the module, that is, it can load all the modules needed by the user-requested one to run.
  • The depmod command can be used to build a dependencies table suitable for the modprobe command.
    Note

    Explaining in detail how this mechanism works is out of the scope of this book. You can take a look at the depmod command's man pages using the man depmod command.

  • The rmmod command can be used to unload a module from the system releasing the RAM and other resources taken during its usage.
    Note

    This can be done only if the module is not actually used by other modules in the system. This fact is true when the number in the Used by column in the preceding output of the lsmod command is equal to 0.

Writing our own device driver

We can now try to implement GPIOs management code that allows us to count how many state transactions a single GPIO line does. Actually, what we are going to do is not write a proper device driver, but we're going to write a kernel code that manages a peripheral, which is very close to be a real device driver! Simply speaking, I can use the next example to show you how a kernel functionality can be abstracted as a file.

Let's suppose we need to count some pulses that arrive on our SAMA5D3 Xplained at a certain amount of time. In this case, we can use one GPIO for each pulse source.

Tip

Note that this situation is quite common, and it can be found in some counters' devices! In fact, these devices, that simply count a quantity (water or oil litres, an energy power meter, and so on), return the counting as frequency modulated pulses.

In this situation, we can use a really simple kernel code to implement a new device class under sysfs that we can use to abstract these measurements to the user space. We use kernel code since the pulses can go fast. So, in order to have better responsiveness from our board, we must use interrupts. What we wish to do is install an interrupt handler that is called each time a specified GPIO line changes its status from low to high or from high to low or both. Then, we wish to have some dedicated files where we can read how many pulses have arrives since the last measurement.

As already stated, to simplify the implementation, we're not going to write a regular device driver (char, block or net device), but we're going to use a class instead. Even if this is not a proper driver, this solution allows us to see how a really simple kernel code works without going too deeply into device drivers programming (this topic is not covered by this book).

Using our new driver, you will see a new class named pulse and a new directory per device where you can read the actual counting.

Here is a simple example of the final result:

root@a5d3:~# tree -l -L 2 /sys/class/pulse/ /sys/class/pulse/ +-- oil -> ../../devices/soc0/pulses/pulse/oil | +-- counter | +-- counter_and_reset | +-- device -> ../../../pulses [recurs., not follow] | +-- power | +-- set_to | +-- subsystem -> ../../../../../class/pulse [recurs., not follow] | \-- uevent \-- water -> ../../devices/soc0/pulses/pulse/water +-- counter +-- counter_and_reset +-- device -> ../../../pulses [recurs., not follow] +-- power +-- set_to +-- subsystem -> ../../../../../class/pulse [recurs., not follow] \-- uevent 8 directories, 8 files 

In the preceding example, we have two pulse devices named oil and water, represented by the same name directories, and for each device, three attributes files are named: counter, counter_and_reset, and set_to (the other files named power and subsystem are not of interest to us).

You can now use the counter file to read the counting data, while by using the counter_and_reset file, you can do the same as with the counter file. However, after reading the data, the counter is automatically reset to the value 0. On the other side, using the set_to file, you can initialize the counter to a specific value different from 0.

Now, before continuing to describe the driver, a simple explanation of the code is needed. We have three files, and the first one is Makefile shown here:

ifndef KERNEL_DIR 
$(error KERNEL_DIR must be set in the command line) 
endif 
PWD := $(shell pwd) 
CROSS_COMPILE = arm-linux-gnueabihf- 
 
obj-m = pulse.o 
obj-m += pulse-gpio.o 
 
all: modules 
 
modules clean: 
   $(MAKE) -C $(KERNEL_DIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) \ 
         SUBDIRS=$(PWD) $@ 
Tip

The code is held in the chapter_03/pulse/Makefile file in the book's example code repository.

As we can see, it's quite similar to the one presented earlier. The only difference is in the obj-m variable. In fact, this time, it declares two object files: pulse.o and pulse-gpio.o.

The pulse-gpio.o file can obviously be obtained by compiling the pulse-gpio.c file that holds the definition of the pulse's GPIO sources, because we can suppose that not only the GPIOs can be possible pulse sources.

The relevant part of the pulse-gpio.c file is the pulse_gpio_probe() function, which is reported here:

static int pulse_gpio_probe(struct platform_device *pdev) 
{ 
    struct device *dev = &pdev->dev; 
    struct fwnode_handle *child; 
    struct pulse_gpio_priv *priv; 
    int count, ret; 
    struct device_node *np; 
 
    /* Get the number of defined pulse sources */ 
    count = device_get_child_node_count(dev); 
    if (!count) 
        return -ENODEV; 
 
    /* Allocate private data */ 
    priv = devm_kzalloc(dev, sizeof_pulse_gpio_priv(count),
                             GFP_KERNEL); 
    if (!priv) 
        return -ENOMEM; 
 
    device_for_each_child_node(dev, child) { 
        int irq, flags; 
        struct gpio_desc *gpiod; 
        const char *label, *trigger; 
        struct pulse_device *new_pulse; 
 
        /* Get the GPIO descriptor */ 
        gpiod = devm_get_gpiod_from_child(dev, NULL, child); 
        if (IS_ERR(gpiod)) { 
            fwnode_handle_put(child); 
            ret = PTR_ERR(gpiod); 
            goto error; 
        } 
        gpiod_direction_input(gpiod); 
 
        np = to_of_node(child); 
 
        /* Get the GPIO's properties */ 
       if (fwnode_property_present(child, "label")) { 
           fwnode_property_read_string(child, "label",
                        &label); 
       } else { 
           if (IS_ENABLED(CONFIG_OF) && !label && np) 
               label = np->name; 
           if (!label) { 
               ret = -EINVAL; 
               goto error; 
           } 
       } 
 
       flags = 0; 
       ret = fwnode_property_read_string(child, "trigger",
                         &trigger); 
       if (ret == 0) { 
            if (strcmp(trigger, "rising") == 0) 
                flags |= IRQF_TRIGGER_RISING; 
            else if (strcmp(trigger, "fallng") == 0) 
                flags |= IRQF_TRIGGER_FALLING; 
            else if (strcmp(trigger, "both") == 0) 
                flags |= IRQF_TRIGGER_RISING | \ 
                             IRQF_TRIGGER_FALLING; 
            else { 
                ret = -EINVAL; 
                goto error; 
            } 
        } 
 
        /* Register the new pulse device */ 
        new_pulse = pulse_device_register(label, dev); 
        if (!new_pulse) { 
            fwnode_handle_put(child); 
            ret = PTR_ERR(new_pulse); 
            goto error; 
        } 
 
        /* Is GPIO in pin IRQ capable? */ 
        irq = gpiod_to_irq(gpiod); 
        if (irq < 0) { 
            ret = irq; 
            goto error; 
        } 
 
        /* Ok, now we can request the IRQ */ 
        ret = request_irq(irq, (irq_handler_t) irq_handler,
                          flags, PULSE_GPIO_NAME, new_pulse); 
        if (ret < 0) 
                goto error; 
 
        priv->pulse[priv->num_pulses].dev = new_pulse; 
        priv->pulse[priv->num_pulses].irq = irq; 
        priv->num_pulses++; 
    } 
 
    platform_set_drvdata(pdev, priv); 
 
    return 0; 
 
error: 
    /* Unregister everything in case of errors */ 
    for (count = priv->num_pulses - 1; count >= 0; count--) { 
        if (priv->pulse[count].dev) 
            pulse_device_unregister(priv->pulse[count].dev); 
        if (priv->pulse[count].irq && priv->pulse[count].dev) 
            free_irq(priv->pulse[count].irq, priv->pulse[count].dev); 
    } 
 
    return ret; 
} 

This function parses the device tree settings and then defines the new pulse devices according to these settings. As already stated earlier, the device tree is defined into a DTS file. In particular, for the SAMA5D3 Xplained, the file is KERNEL/arch/arm/boot/dts/at91-sama5d3_xplained.dts (see the kernel sources downloaded in SAMA5D3 Xplained section in Chapter 1Installing the Developing System, in the A5D3/armv7_devel directory). So, if we modify it as shown here, we can define two pulse devices named oil and water connected to gpio17 (PA17) and gpio19 (PA19), respectively, both exported on the SAMA5D3 Xplained expansion connector:

--- a/arch/arm/boot/dts/at91-sama5d3_xplained.dts 
+++ b/arch/arm/boot/dts/at91-sama5d3_xplained.dts 
@@ -332,5 +332,22 @@ 
           label = "d3"; 
           gpios = <&pioE 24 GPIO_ACTIVE_HIGH>; 
       }; 
+ 
+  }; 
+ 
+  pulses { 
+      compatible = "gpio-pulses"; 
+ 
+      oil { 
+          label = "oil"; 
+          gpios = <&pioA 17 GPIO_ACTIVE_HIGH>; 
+          trigger = "both"; 
+      }; 
+ 
+      water { 
+          label = "water"; 
+          gpios = <&pioA 19 GPIO_ACTIVE_HIGH>; 
+          trigger = "rising"; 
+      }; 
   }; 
 }; 
Tip

The patch is held in the chapter_03/pulse/pulse-gpio_at91-sama5d3_xplained.dts.patch file in the book's example code repository.

You should also notice that while the water pulse device is triggered on the rising edge of the input transaction, the oil one is triggered on both edges.

Using the preceding code into our DTS file, we can expect that pulse_gpio_probe() executes two loops where it reads all the configuration data of each pulse source and then calls the pulse_device_register() function to define the new device into the kernel. After that, it calls the request_irq() function, which is used to declare an interrupt handler (the handler irq_handler() function) connected to the GPIO status where the effective counting takes place. In fact, the handler looks like this:

static irqreturn_t irq_handler(int i, void *ptr, struct pt_regs *regs) 
{ 
    struct pulse_device *pulse = (struct pulse_device *) ptr; 
 
    BUG_ON(!ptr); 
 
    pulse_event(pulse); 
 
    return IRQ_HANDLED; 
} 

At this point, we must point out three important things:

  1. ​The kernel module does not use the classic module_init() and module_exit() functions used into our kernel module example shown earlier.
  2. The pulse_gpio_probe() function and its opposite pulse_gpio_remove() calls the functions pulse_device_register() and pulse_device_unregister(), respectively to add and remove a pulse device from the kernel.
  3. The interrupt handler irq_handler() calls the pulse_event() function to signal to the system that a particular pulse event has arrived.

Regarding the first point, we can observe that the missing functions are actually used by the module_platform_driver() statement, which is defined into the kernel file include/linux/platform_device.h as follows:

#define module_platform_driver(__platform_driver) \ 
    module_driver(__platform_driver, platform_driver_register, \ 
                  platform_driver_unregister) 

Then, module_driver() is defined into the kernel file include/linux/device.h as shown here, where we can see that module_init() and module_exit() are called:

#define module_driver(__driver, __register, __unregister, ...) \ 
static int __init __driver##_init(void) \ 
{ \ 
    return __register(&(__driver) , ##__VA_ARGS__); \ 
} \ 
module_init(__driver##_init); \ 
static void __exit __driver##_exit(void) \ 
{ \ 
    __unregister(&(__driver) , ##__VA_ARGS__); \ 
} \ 
module_exit(__driver##_exit); 

Regarding the second and third points, about the pulse_device_register(), pulse_device_unregister(), and pulse_event() functions, we observed that these functions are defined into the third file composing our driver, that is, the pulse.c file. Here is a snippet of such a file where the functions are defined:

void pulse_event(struct pulse_device *pulse) 
{ 
    atomic_inc(&pulse->counter); 
} 
EXPORT_SYMBOL(pulse_event); 
 
struct pulse_device *pulse_device_register(const char *name, 
                                           struct device *parent) 
{ 
    struct pulse_device *pulse; 
    dev_t devt; 
    int ret; 
 
    /* First allocate a new pulse device */ 
    pulse = kmalloc(sizeof(struct pulse_device), GFP_KERNEL); 
    if (unlikely(!pulse)) 
        return ERR_PTR(-ENOMEM); 
 
    mutex_lock(&pulse_idr_lock); 
    /* 
     * Get new ID for the new pulse source.  After idr_alloc() calling 
     * the new source will be freely available into the kernel. 
     */ 
    ret = idr_alloc(&pulse_idr, pulse, 0, PULSE_MAX_SOURCES,
    GFP_KERNEL); 
    if (ret < 0) { 
        if (ret == -ENOSPC) { 
            pr_err("%s: too many PPS sources in the system\n", name); 
            ret = -EBUSY; 
        } 
        goto error_device_create; 
    } 
    pulse->id = ret; 
    mutex_unlock(&pulse_idr_lock); 
 
    devt = MKDEV(MAJOR(pulse_devt), pulse->id); 
 
    /* Create the device and init the device's data */ 
    pulse->dev = device_create(pulse_class, parent, devt, pulse, 
                               "%s", name); 
    if (unlikely(IS_ERR(pulse->dev))) { 
        dev_err(pulse->dev, "unable to create device %s\n", name); 
        ret = PTR_ERR(pulse->dev); 
        goto error_idr_remove; 
    } 
    dev_set_drvdata(pulse->dev, pulse); 
    pulse->dev->release = pulse_device_destruct; 
 
    /* Init the pulse data */ 
    strncpy(pulse->name, name, PULSE_NAME_LEN); 
    atomic_set(&pulse->counter, 0); 
    pulse->old_status = -1; 
 
    dev_info(pulse->dev, "pulse %s added\n", pulse->name); 
 
    return pulse; 
 
error_idr_remove: 
    mutex_lock(&pulse_idr_lock); 
    idr_remove(&pulse_idr, pulse->id); 
 
error_device_create: 
    mutex_unlock(&pulse_idr_lock); 
    kfree(pulse); 
 
    return ERR_PTR(ret); 
} 
EXPORT_SYMBOL(pulse_device_register); 
 
void pulse_device_unregister(struct pulse_device *pulse) 
{ 
    /* Drop all allocated resources */ 
    device_destroy(pulse_class, pulse->dev->devt); 
 
    dev_info(pulse->dev, "pulse %s removed\n", pulse->name); 
} 
EXPORT_SYMBOL(pulse_device_unregister); 

You can see all the steps done to create the driver data structures into the register function and the respective inverse steps done into the unregister one. Then, the pulse_event()function is just a counter increment.

Also, you should notice that all functions are declared as exported symbols by the code:

EXPORT_SYMBOL(pulse_event); 
EXPORT_SYMBOL(pulse_device_register); 
EXPORT_SYMBOL(pulse_device_unregister); 

This says to the compiler that these functions are special because they can be used by other kernel modules.

At the module initialization (the pulse_init()function), we use class_create() to create our new pulse class and, as the opposite action, at the module exit (the pulse_exit()function), we destroyed it by calling class_destroy().

You should now take attention to the pulse_init()function at line:

pulse_class->dev_groups = pulse_groups; 

Using such an assignment, we will declare the three attribute files, count, counter_and_reset, and set_to that are all reported in struct pulse_attrs:

static struct attribute *pulse_attrs[] = { 
    &dev_attr_counter.attr, 
    &dev_attr_counter_and_reset.attr, 
    &dev_attr_set_to.attr, 
    NULL, 
}; 

Each entry of the preceding structure is created by the DEVICE_ATTR_XX() function as outlined here:

static ssize_t counter_show(struct device *dev, 
                      struct device_attribute *attr, char *buf) 
{ 
    struct pulse_device *pulse = dev_get_drvdata(dev); 
 
    return sprintf(buf, "%d\n", atomic_read(&pulse->counter)); 
} 
static DEVICE_ATTR_RO(counter); 

This code specifies the attributes of the dev_attr_gpio.attr entry by declaring the file attribute counter as read-only, and when the function body counter_show() is called each time from the user space, we do a read() system call on the file. In fact, as there are read() and write() system calls for files, there are show() and store() functions for sysfs attributes.

As a dual example, the following code declares the attributes of the dev_attr_set_to.attr entry by declaring the file attribute set_to as write-only, and when the set_to_store()function body is called each time from the user space, we do a write() system call on the file:

static ssize_t set_to_store(struct device *dev, 
                            struct device_attribute *attr, 
                            const char *buf, size_t count) 
{ 
    struct pulse_device *pulse = dev_get_drvdata(dev); 
    int status, ret; 
 
    ret = sscanf(buf, "%d", &status); 
    if (ret != 1) 
        return -EINVAL; 
 
    atomic_set(&pulse->counter, status); 
 
    return count; 
} 
static DEVICE_ATTR_WO(set_to); 
Tip

Note that the sprintf() and sscanf()functions, which are quite common functions for C programmers, are not the ones implemented into libc. Rather they are homonym functions written ad-hoc for the kernel space to simplify the kernel code development by representing to the developer well-known functions.

You should also notice that for the show() and store() functions we have that:

  1. ​The attribute files are the ones that get/set the data from/to the user space by reading/writing the data into the buffer pointed by the buf pointer.
  2. All these functions work on the dev pointer that represents the device that is currently accessed, that is, if the user gets access to the device oil, the dev pointer will point to a data structure representing such a device! This recalls the object-oriented programming model, and this magic allows the developer to write a clean and compact code!

At this time, the driver functioning should be clear. The pulse.c (the core of our driver) file defines the basic structures and functions, while the pulse-gpio.c file, by reading the device tree, defines the pulse sources based on GPIOs (note that this solution is quite generic, and it allows the developers to add other kinds of sources into pulse.c using the mechanisms).

Now, to test the code, we should compile it, so let's use the command:

$ make KERNEL_DIR=~/A5D3/armv7_devel/KERNEL/

If everything works well, we should get the two kernel modules pulse.ko and pulse-gpio.ko we defined in Makefile.

Tip

Note that KERNEL_DIR points to the directory where the kernel sources are downloaded into SAMA5D3 Xplained section in Chapter 1Installing the Developing System , so you should set it according to your system configuration.

So, let's copy the two files into the SAMA5D3 Xplained using the scp command:

$ scp *.ko root@192.168.8.2:

Then, load the pulse.ko module with the following command:

root@a5d3:~# insmod pulse.ko

Looking at the kernel messages with dmesg, we should see the following message:

Pulse driver support v. 0.80.0 - (C) 2014-2016 Rodolfo Giometti

Great! The new device class is now defined into the kernel. In fact, looking into the sysfs directory /sys/class/, we see that the new class is up and running:

root@a5d3:~# ls -ld /sys/class/pulse/
drwxr-xr-x 2 root root 0 Apr 2 17:45 /sys/class/pulse/

Now, we should add the two devices oil and water defined into the device tree and enabled by the pulse-gpio.ko module as shown here:

root@a5d3:~# insmod pulse-gpio.ko

Again, using the dmesg command, we should see two new kernel messages:

pulse oil: pulse oil added
pulse water: pulse water added

This is what we expected! Now in the sysfs we now have:

root@a5d3:~# ls /sys/class/pulse/
oil water

Perfect! The system is now ready to count the pulses on the programmed GPIOs, but how we can generate these pulses to test the new driver? Well, this is quite simple. We can use another GPIO as the pulse generator and the script in the chapter_03/pulse_gen.sh file of the book's example code repository to actually generate the pulses. If we connect gpio16 (PA16) to gpio17 (PA17), and in another terminal window, if we run the preceding script with the following command line, we would generate a 4Hz pulse signal from the first GPIO to the second one where the oil counter is connected:

root@a5d3:~# ./pulse_gen.sh a5d3 A16 4

So, if we try to read its data, we get the following line of code:

root@a5d3:~# cat /sys/class/pulse/oil/counter
48

If we try to read the counter file, we see that it increments (more or less) at the speed of 4 pulses per second. However, the functioning may be more clear if we use the following commands that reset the counter first (using the set_to file) and then use the counter_and_reset file to restart the counting after each reading:

root@a5d3:~# echo 0 > /sys/class/pulse/oil/set_to ; \ while sleep 1 ; do \ cat /sys/class/pulse/oil/counter_and_reset ; \ done
7
8
8
8
Tip

Note that we get 8 instead of 4 because the pulse driver counts both high-to-low and low-to-high transactions (try to connect gpio16 (PA16) to the water counter to see 4). Also, note that the 7 is due to the fact that there can be some delays in reading the counting data due to the fact we're using a Bash script to generate the waveform, which is certainly not the best solution (even if it's certainly the quickest).