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()
, andwrite()
system calls. Char device drivers have theioctl()
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:
- The driver's source code is a
patch
to be applied to the kernel tree. - 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 thaninsmod
, 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 themodprobe
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 theman 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 thelsmod
command is equal to0
.
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 1 , Installing 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:
- The kernel module does not use the classic
module_init()
andmodule_exit()
functions used into our kernel module example shown earlier. - The
pulse_gpio_probe()
function and its oppositepulse_gpio_remove()
calls the functionspulse_device_register()
andpulse_device_unregister()
, respectively to add and remove a pulse device from the kernel. - The interrupt handler
irq_handler()
calls thepulse_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:
- 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. - 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 deviceoil
, thedev
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 1 , Installing 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).