Seti B4 TP
Seti B4 TP
Seti B4 TP
Embedded Linux
Valerii DROBOTUN
Yasser BOURSSIA
Sirine MAMA
Télécom Paris
Institut Polytechnique de Paris & Université Paris-Saclay
January 2023
Contents
Introduction 2
4 TP4: Interrupts 31
4.1 Intro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.2 Update of the ADXL345 setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.3 Interrupt handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.4 Test program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
References 38
1
Introduction
Embedded Linux is a type of Linux operating system/kernel that was designed to be installed and
used in embedded devices or systems.
Embedded Linux is adaptable, affordable, open-source, and works with special-purpose micropro-
cessors. With a stable kernel and the flexibility to read, change, and redistribute source code, Linux
has several software, development, and support providers available in comparison to proprietary
embedded operating systems. Additionally, it enables a very modular building block approach to
creating a unique system, increasing versatility. It has become very popular among the embedded
systems engineers.
The aim of our project is:
• Write a driver for an I2C device as well as initialize the device when detected and reset the
device when it is removed.
• Add the possibility for the user space to interact with the ADXL345 device driver via the
misc framework.
2
1 TP1: Minimal Linux/initramfs image and bootloader
1.1 Intro
During this practical work, we are learning how to create a minimal system to run Linux. It includes
the kernel compilation, then adding some flavor of kernel environment, namely BusyBox, default
initramfs file system, and finally secondary bootloader, called U-Boot or Das Boot if you wish.
Remember, we are trying to compile binaries for the platform, different from the host one, so we
have to use our specific compiler, to let our host system know where to find it. In addition, we are
setting the ARCH variable, because we intend to build the kernel for the ARM system.
Then, we are cloning the official Linux repository and choosing a bit older kernel, version 5.x.x
to ensure compatibility. More modern version 6.x.x seems to be a bit unstable for now. After down-
loading the kernel, we just need to update the build options by executing make vexpress defconfig
to set default parameters, specific for our devboard, and then make menuconfig. Actually, all the
defaults are fine, we only have to update the only one option, called udev enable, shown in the
figure 1. Basically, uevent or User Events are a way to interact between kernel or especially drivers,
running in the kernel space, and user space.
Finally, kernel build is made with make install, probably with -j option to enable multi-core
compilation. Luckily, the target board doesn’t have too many devices, which means that not so
modules should be compiled. That results in an appropriate compile time, around 10 minutes on
the notebook processor, compared to 3+ hours for the x86 system kernel.
3
Figure 1: Pseudographical kernel compilation setup
Some errors could appear after launching the Makefile, telling that there are no header files found
and compilation cannot proceed. Usually, that means some additional Linux packages should be
installed. Any kind of such problem could be solved by copying make output to the StackOverflow
search and finding the first response with the list of packages required to install. That simply
works.
In my proper case, I had everything necessary already installed, but make was not happy because
there were MPEG libraries missing.
As a result of the compilation, we obtain two necessary files:
4
• Detect the CPU kernel is running on
• Multicore activation
• SCSI initialization
But in the end, the kernel panicked, as can be seen in figure 2. This happens because it cannot
mount rootfs, which have nowhere to mount actually.
5
#include <stdio.h>
#include <unistd.h>
So the next step is to provide a filesystem to the kernel, with the existing init program. In
any other case, the kernel will panic – it always looks for init, because it is the first process to
be launched, as an ancestor of all the following processes. We then have to write the init code,
presented in the listing 1. This code basically does nothing, except print to the standard output,
wait for 10 seconds, and terminate. This will allow us to see the message before the kernel panic.
It is left only to compile init with our cross compiler with the following command:
We should use static option because otherwise, it won’t be possible to launch the program: it
will require shared libraries, that should be loaded by someone else, who doesn’t exist yet in our
build. With such an option, we ensure that the code of libraries will be integrated into the same
binary.
Then, the output binary file should be zipped in the following way:
The cpio program was used to create filesystems on the tape, but now it’s used to create
filesystems for the kernel. The -o option tells it to read standard input and create an archive on
standard output. Option H stands for the format definition, in our case newc says that we are
using SVR4 format, which supports more than 65536 i-nodes. Finally, the archive is passed to gzip
to create a compressed one.
With newly created filesystem, we can launch our simulation again to ensure that kernel launches
the init process:
Note that this time we passed our archive with init in it via -initrd option, denoting the
rootfs. After launch, we see the same initialization procedure, but with the print of Hello from
init, shown in figure 3. At this moment we can be sure that init was launched. For sure, after 10
seconds after the print, we observe another kernel panic message, telling that the system attempted
to kill init, which is normally not expected. Since the init is an ancestor of other processes, it
should never exit, as it was made in our case. Anyway, it is sufficient to know that our system
starts the kernel and launches something.
6
Figure 3: Hello init and kernel panic
1.5 Busybox
In this part, we will be using BusyBox, because in fact, our Linux kernel cannot do anything on
its own – it doesn’t even work with the terminal. It needs environmental utilities to do so (and a
lot more).
BusyBox includes simplified (very reduced functionality) versions of many Unix utilities such
as the init, sh, mount, cp, ls and so on into one small executable, which can be very practical for
small embedded systems.
We have to first prepare the tree structure (the directories for the BusyBox image), as presented
in the figure 4.
We then configure some files; by configuring the rcS file, which does the following:
• Activates mdev.
After the creation of the aforementioned file (and the fstab/inittab files), we can then create
the full image using the cpio command:
This command is used the same as it is used before, that is, creating an archive, in this case,
we are giving it the output of the find command, which actually outputs all files in the current
7
directory. We add an external argument -R, which simply gives the ownership of the archive to the
0 user. Then passing the result to gzip to compress our archive.
As seen in figure 5, the kernel image has been correctly executed on the ARM core. With no
kernel panics in sight, we have actually provided it with the necessary components for a successful
launch (kernel, the image with correct init and the utilities provided by BusyBox).
8
As it can be seen in figure 6, showing the result of the execution, bootloader run ends with an
error. This is pretty expected, as we haven’t provided the bootloader with a kernel image to load.
Actually it tries all available physical partitions, and then all available network cards to retrieve
the kernel image.
$ mkdir sdcard
$ cd sdcard
$ dd if=/dev/zero of=sd bs=1M count=64
Then, we have to create a partition for this SD Card. We will only need one FAT partition
covering the whole card for this application. To do that, we launch parted and create label and
partition itself:
9
(parted) mklabel msdos
(parted) mkpart primary fat32 1MiB 100%.
After QEMU’s boot, we use U-Boot to load the contents of the SD Card into memory
(specifically, 0x62000000, 0x63000000, 0x63100000 as magic addresses):
Now that all the pieces needed for a full kernel boot are ready, we can launch the kernel using
the command:
This command, evidently, boots the kernel using the 3 memory addresses given as arguments.
These memory addresses now contain the three pieces needed for the kernel boot: zImage, the DTB,
and the initramfs. After typing the last command, Linux kernel is correctly launched, as it can
be seen on the image 7.
10
Figure 7: U-Boot BusyBox Linux launch
1.8 Conclusion
In this first TP, we got to understand what is really a kernel boot, and what is needed for a
complete and successful kernel boot. We also got to explore concepts such as machine emulation
and cross-compiling programs for the target machine.
The first attempts have shown us that a kernel cannot fully launch without a full system
environment consisting of a filesystem, usually included in the initramfs and init program on
that filesystem.
We then got to use the BusyBox framework, that allows for a fast setup of an embedded Linux
ecosystem. At the end, we have shown that we can setup our own embedded system containing the
kernel image and initramfs and load it into QEMU, using Das U-boot bootloader.
11
2 TP2: First driver
2.1 Intro
The aim of this TP is to understand how drivers and devices in Linux systems are working, by
creating the driver from scratch. As a device, we are targeting to use a rather simple I2 C chip
– ADXL345 accelerometer. This accelerometer could be accessed via I2 C bus, presented on the
board and simulated by QEMU, which allows us debugging our driver code without an access to
the physical one.
We are targeting to create a driver for it, that will allow us to get the accelerometer measure-
ments and set the specific settings of it.
2.2 Prerequisites
Since we used a bare Linux and built it from scratch during the last TP, the compiled kernel doesn’t
know that such a device exists. It requires us to download a version of Linux that includes a proper
board device tree, at the very end of which a mention of ADXL345 could be found:
&v2m_i2c_dvi {
adxl345: adxl345@53 {
compatible = "qemu,adxl345";
reg = <0x53>;
interrupt-parent = <&gic>;
interrupts = <0 50 4>;
};
};
From this part it could be seen that our drive must be "qemu,adxl345" compatible, the device
uses one of two available addresses (0x1D and 0x53). Moreover, the interrupts are routed to the
gic interrupt controller of Cortex-A9.
As we mentioned before, we are using a patched QEMU simulator to have a virtual accelerom-
eter, to be able to debug the code without a physical board. It will emulate the behavior of the
accelerator and allow to update the registers via I2 C bus.
12
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/i2c.h>
MODULE_DEVICE_TABLE(i2c, ADXL345_idtable);
#ifdef CONFIG_OF
static const struct of_device_id ADXL345_of_match[] = {
{ .compatible = "qemu,ADXL345",
.data = NULL },
{}
};
MODULE_DEVICE_TABLE(of, ADXL345_of_match);
#endif
static struct i2c_driver ADXL345_driver = {
.driver = {
.name = "ADXL345",
.of_match_table = of_match_ptr(ADXL345_of_match),
},
.id_table = ADXL345_idtable,
.probe = ADXL345_probe,
.remove = ADXL345_remove,
};
module_i2c_driver(ADXL345_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("ADXL345 driver");
MODULE_AUTHOR("Me");
13
The driver can be built using the following command:
In any case, it needs to do something. So let’s add some prints to know that our driver starts.
ifneq (£(KERNELRELEASE),)
# kbuild part of makefile
obj-m := adxl345.o
else
# normal makefile
KDIR ?= /lib/modules/`uname -r`/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
endif
To see what’s happens when we load the driver, we have to compile it first, and then use the
adxl345.ko file.
14
After compilation, we start the emulator with
Note that we include a folder with compiled driver, adding a virtual device.
After booting, we have to enable the device with filesystem to make /mnt available, using the
specific config for blocksize and version:
With all above, we can finally load and unload the module with our driver via
$ insmod /mnt/adxl345.ko
$ rmmod adxl345.
As expected, we see the messages from probe and remove functions, because the device is
present in the system, so these functions are executed on driver load/unload.
However, we see an unexpected message too: the kernel is tainted for some reason. This happens
because our driver is compiled outside of device tree. Anyway, it is just a warning, let’s skip it for
now and pass to I2 C messages.
15
typedef enum {
ADXL345_DEVID_REG = 0x00,
ADXL345_BW_RATE_REG = 0x2C,
ADXL345_DATA_FORMAT_REG = 0x31,
ADXL345_FIFO_CTL_REG = 0x38,
ADXL345_POWER_CTL_REG = 0x2D,
ADXL345_INT_ENABLE_REG = 0x2E,
ADXL345_INT_MAP_REG = 0x2F,
ADXL345_DATAX0_REG = 0x32,
ADXL345_DATAX1_REG = 0x33,
ADXL345_DATAY0_REG = 0x34,
ADXL345_DATAY1_REG = 0x35,
ADXL345_DATAZ0_REG = 0x36,
ADXL345_DATAZ1_REG = 0x37,
} adxl_reg_t;
2.7 Conclusion
In this TP, we got to see how we can add device drivers to our kernel, in particular, we have created
an ADXL345 driver, able to interact with the respecting accelerometer.
16
static int ADXL345_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
printk(KERN_INFO "ADXL345 connected\n");
if (ret < 0) {
printk(KERN_WARNING "ADXL345 is not responding, error %d\n", ret);
return ret;
}
17
Figure 9: Result of DEVID register read
We have also gotten a chance to use the different I2 C functions such as i2c transfer() and
i2c msg(). These functions exist thanks to the lower-level API of the Linux (HAL), and we don’t
have to manipulate the registers by hand to perform a data transfer. We then used those functions
to check the link between the driver and the device by verifying its DEV ID register, and then
initialize the device by modifying its different registers and successfully reading them back.
18
static int ADXL345_setup(struct i2c_client *client) {
adxl_reg_pair_t setup[5] = {
{ .reg = ADXL345_BW_RATE_REG, .value = 0x0A },
{ .reg = ADXL345_INT_ENABLE_REG, .value = 0 },
{ .reg = ADXL345_DATA_FORMAT_REG, .value = 0 },
{ .reg = ADXL345_FIFO_CTL_REG, .value = 0 },
{ .reg = ADXL345_POWER_CTL_REG, .value = 1 << 3 },
};
return ret;
}
19
static int ADXL345_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
printk(KERN_INFO "ADXL345 connected\n");
uint8_t data;
int ret = ADXL345_read_reg(client, ADXL345_DEVID_REG, &data);
ret = ADXL345_setup(client);
if (ret < 0) {
printk(KERN_WARNING "ADXL345 is not responding, error %d\n", ret);
return ret;
}
20
3 TP3: Driver access from user space
3.1 Intro
During this TP we are going to fulfill the dedication of our ADXL345 driver, described in the
previous part: make its data accessible to the user of our Linux system. In addition, we are going
to make the driver configuration accessible by the user, allowing him to change some parameters,
e. g. measure frequency or axis measured.
To do that, we are going to use the Misc framework, allowing us to create a device pseudo
file. Of course, we are using our device as a character one, and we are profiting of Misc framework,
because we do not have to define the major version (it is always 10), and using a dynamic minor.
struct adxl345_device {
struct miscdevice miscdev;
};
Since the structure of device is defined, we have to update the * probe() function. The updated
part is presented in the listing 10. We dropped the error code treatment and debug prints in this
snipped to increase readability. In fact, the code does following things:
• Allocate the memory and print filename in it, which go to the /dev/*
• Set the misc driver parameters: dynamic minor, name, parent (which is I2 C device actually),
and structure of available operations.
The same manner, * release() function was updated, to ensure that the driver cleanup after
himself: kernel memory leakage is too dangerous!
Updated part is presented in the listing 11. Mainly it just retrieves the actual devices and do
two kfree() to free the memory, allocated before.
21
struct adxl345_device* dev =
kmalloc(sizeof(struct adxl345_device), GFP_KERNEL);
char* name_buf = kasprintf(GFP_KERNEL, "adxl345-%d", dev_count);
/*...*/
dev->miscdev.minor = MISC_DYNAMIC_MINOR;
dev->miscdev.name = name_buf;
dev->miscdev.parent = &(client->dev);
dev->miscdev.fops = &fops;
i2c_set_clientdata(client, dev);
int misc = misc_register(&(dev->miscdev));
/*...*/
dev_count += 1;
dev_count -= 1;
struct adxl345_device* dev = i2c_get_clientdata(client);
misc_deregister(&(dev->miscdev));
kfree(dev->miscdev.name);
kfree(dev);
This code reads 2 bytes from the pseudo file, since each measurement contents 16-bit 2’s com-
plement value. Moreover, it should be run in a loop with 0.01s, or 10 ms delay, since the data rate
is set to 100 Hz.
So some kind of following code snippet should work:
while :
do
dd bs=2 count=1 if=/dev/adxl345-0 status=none | xxd
sleep 0.01
done
22
That does not always provide coherent results: sometimes the driver reads axis measurements
from different iterations, which is quite understandable since we are running asynchronous code
without any interruptions. This could be observed as signs on the values obtained for X, Y, and Z
axes. Obtained output is presented in figure 11. Note that we don’t really interested in the debug
print, but in the xxd output.
Normally, we already ensured that our driver returns valid values, but to make sure we also
wrote a simple C program to test the system calls. Its contents is presented in listing 12. As usual,
it should be complied with static library linking and put in the driver folder, to be accessed in /mnt
in simulated system.
typedef enum {
ADXL345_AXIS_X = 0,
ADXL345_AXIS_Y = 2,
ADXL345_AXIS_Z = 4
} adxl_axis_t;
int main(){
int fd, ret;
fd = open("/dev/adxl345-0", O_RDWR);
/*...*/
char buffer[2];
const int count = 2;
msleep(40);
}
/*...*/
return 0;
}
At this point we could be sure that our driver is working correctly and outputs the right values
from the registers. It is left only configuring the axis that should be read. This should be done by
the second callback that we are going to write: the ioctl one.
23
Figure 11: ADXL345 X-axis output
24
long adxl345_ioctl(struct file* file, unsigned int cmd, unsigned long arg) {
printk(KERN_INFO "Command data: %x\n", cmd);
switch (cmd) {
case _IOW(MISC_MAJOR, ADXL345_AXIS_X, uint8_t):
axis = (adxl_axis_t) ADXL345_AXIS_X;
return 0;
case _IOW(MISC_MAJOR, ADXL345_AXIS_Y, uint8_t):
axis = (adxl_axis_t) ADXL345_AXIS_Y;
return 0;
case _IOW(MISC_MAJOR, ADXL345_AXIS_Z, uint8_t):
axis = (adxl_axis_t) ADXL345_AXIS_Z;
return 0;
default:
printk(KERN_WARNING "ADXL345 illegal ioctl command: %d\n", cmd);
return -EINVAL;
}
}
• add subscriber(sub)
• remove subscriber(sub)
• find subscriber(pid)
• last subscriber().
This list is managed in the * open() and * release() functions, to add and remove process
pids with their settings.
The result of execution of the same test program is presented in figure X.
It can be seen that actual file release happens not at the end of the program, but at its startup.
Actually it doesn’t matter for the kernel, when exactly it happens. The most important thing is
that memory should be returned to the allocator.
However, our current test program contains only one thread, which is not good to test the
current driver. Program need to start 2 or more threads at the same time to verify that axes are
25
int main(){
int fd, ret;
fd = open("/dev/adxl345-0", O_RDWR);
/*...*/
adxl_axis_t testcase[3] = {ADXL345_AXIS_X, ADXL345_AXIS_Y, ADXL345_AXIS_Z};
char buffer[2];
const int count = 2;
msleep(40);
}
}
}
selected correctly. This could be done by the fork() function, updated test program snippet is
presented in the listing 16.
As could be seen in the image 14, the driver works as expected, giving different axis to the
different processes.
However, it seems like it is not a casual way to give processes proper information. It seems like
more proper approach is to use single process (daemon) in user space that connects to the driver
and retrieve all needed information from it. The same way it collects the information from the client
processes, which may connect to the daemon however they want, with pipes, sockets, files... The
improvements of that approach is that we are not creating potentially dangerous leaking structures
in kernel space, but in user one. In the worth case scenario, kernel would kill the daemon because
of segmentation fault, but kernel would remain consistent. In current approach, if something goes
wrong in driver – kernel sinks with it.
3.7 Conclusion
In this section we are learned how to use the Misc framework and how it eases driver development.
We added read function to our driver, allowing different processes to read the measurements from
the pseudo file in /dev. Moreover, we added driver control via ioctl() call, allowing processes to
choose the axis of measurements.
As a final step, we separated measurements for different processes, that way that ioctl() of
one process does not affect others.
26
Figure 12: Calls to ioctl
27
int adxl345_open(struct inode * inode, struct file * file) {
pid_t pid;
struct adxl_association_s* assoc;
pid = current->pid;
return 0;
}
pid = current->pid;
sub = find_subscriber(pid);
/*...*/
remove_subscriber(sub);
kfree(sub);
return 0;
}
28
Figure 13: Driver with memorization request
/*...*/
pid_t pid = fork();
int ret;
adxl_axis_t testcase_p[3] = {ADXL345_AXIS_X, ADXL345_AXIS_Y, ADXL345_AXIS_Z};
adxl_axis_t testcase_c[3] = {ADXL345_AXIS_Y, ADXL345_AXIS_X, ADXL345_AXIS_Z};
29
Figure 14: Different measures for different processes
30
4 TP4: Interrupts
4.1 Intro
This part of the driver development is concerned with the interrupts.
We are going to fulfill this last contract of any type of embedded peripheral and will see how
the interrupts are treated by the Linux kernel. The final goal consists of the following steps:
adxl_reg_pair_t setup[5] = {
{ .reg = ADXL345_BW_RATE_REG, .value = 0x0A },
{ .reg = ADXL345_INT_ENABLE_REG, .value = 1 << 1 },
{ .reg = ADXL345_DATA_FORMAT_REG, .value = 0, },
{ .reg = ADXL345_FIFO_CTL_REG, .value = 1 << 7 | WATERFILL_LIMIT },
{ .reg = ADXL345_POWER_CTL_REG, .value = 1 << 3 },
};
In addition, on the driver’s side, we have to organize a FIFO too. This is necessary to economize
the number of interruption calls. The idea is to wait for the waterfall interrupt, copy the data in
the interrupt handler into the internal driver buffer, and then use it in the * read() function.
Thanks to this, we minimize the number of accesses to the accelerometer from the driver, allowing
the treatment of other devices on the I2 C bus effectively. Otherwise, we would have to read each
measurement in the * read() function.
To implement this buffer, we are going to use the kfifo implementation, adding the corre-
sponding field in the device structure, as shown in the listing 18. We set the size of the buffer to 64
31
struct adxl345_measurement { uint16_t x; uint16_t y; uint16_t z; };
struct adxl345_device {
struct miscdevice miscdev;
struct wait_queue_head queue;
DECLARE_KFIFO(fifo_samples, struct adxl345_measurement, 64);
};
elements, representing the measurements of 3 axes (6 bytes total). The size is dictated by the idea
that the intermediate buffer should have a size at least twice larger than the waterfall limit. Due
to the optimizations, kfifo size should be a power of 2, so instead of 40, we can choose only 64.
read_offset += 2;
}
return count;
}
First of all, the * read() callback should be modified, as it is present in the listing 19. Basically,
it’s just checking the return value of kfifo get(...) macro, which is 0 if nothing was read from
32
the FIFO. In the wait event interruptible(...) macro, a wrapper data is avaliable(...)
is used, which is basically just a negation of kfifo is empty(...), since wait macro does not
accept another macro as a parameter.
As a result, the reading process will be blocked in the callback, until the data is available, which
will be preceded by the wake up(...) macro in the interrupt handler. Then, data is just copied
by two bytes to the userspace.
We are going to use the Threaded IRQ mechanism to interrupt treatment since this approach
allows to block/wait with no restrictions because it’s done in the process of application. Since we
are using the I2 C bus, which is relatively slow, all interactions should be preferably done in the
bottom half of the interrupt, because during the interrupt handling, all other interrupts are disabled.
In fact, in our driver we don’t even need the top half of the handler, because the accelerometer
measurement rate is slow too, so we only have to write the bottom part, since we already introduced
the bufferisation to our driver. Of course, if we needed more reactivity of the driver, we would write
the top half too, but the accelerometer measurement rate is only 100 Hz.
i2c_set_clientdata(client, dev);
misc = misc_register(&(dev->miscdev));
init_waitqueue_head(&(dev->queue));
ret = devm_request_threaded_irq(
&(client->dev),
client->irq,
NULL,
adxl345_irq,
IRQF_ONESHOT,
dev->miscdev.name,
dev
);
return 0;
}
33
In the listing 20 you can see the initialization of the interrupt: device is our I2 C one, interrupt
number is retrieved via the I2 C client since it is a bit difficult to calculate it by hand, the top half
interrupt handler is NULL since we don’t use it, the bottom half function is adxl345 irq(), type of
interrupt is ONESHOT (on the front of signal), name of the interrupt in the proc/interrupts is the
same as the name of the device in the /dev folder.
Finally, the interrupt handler (bottom half) is presented in the listing 21.
size_t i;
ADXL345_read_axis(client, buf);
meas.x = buf[0];
meas.y = buf[1];
meas.z = buf[2];
kfifo_put(&(dev->fifo_samples), meas);
}
wake_up(&(dev->queue));
return IRQ_HANDLED;
}
The interrupt handler reads the 20 records from the FIFO of the accelerometer, since the
interrupt is generated only if the amount of elements in the queue is set to 20, so at this point
driver is safe. All the data is passed to the driver FIFO. At the end of the interrupt, we are calling
wake up(...) to resume the blocked in the * read(...) callback, if present.
It worth note the following points: first, we are not protecting the queue, since it is always
accessible for one concurrent read and write. Second, the data for the accelerometer should be read
in the same transaction for all axes. This function was implemented in the previous parts, so we
didn’t have to debug it.
34
printk(KERN_INFO "In FIFO: %d\n", kfifo_avail(&(dev->fifo_samples)));
int main(){
int fd = open("/dev/adxl345-0", O_RDWR);
/* Test the fd */
close(fd);
}
The test program code is presented in the listing 22. Basically, it does the same thing as before,
except it is reading 100 values for each axis, to ensure that FIFO is being read several times.
As usual, we are running QEMU and installing the compiled module of the driver. Right after
we check if the interrupt is correctly registered, we can execute the cat /proc/interrupts bash
command, which output is presented in figure 15. As you can see, the kernel is registered correctly
as the interrupt handler and added it to the list of the CPU0.
Finally, as usual, we are running our test program with /mnt/main in the terminal. The result
of execution is presented in figure 16. As can be seen, the FIFO is being read correctly, with an
increasing number of free slots, and sleep messages could be observed. Note that the difference in the
number of free slots before and after waiting is not exactly 20, but 19, because the kfifo get(...)
pulls one element from the queue before the print of the free available space.
35
Figure 15: Registration of interrupt
4.5 Conclusion
In this part of the work, we have added the proper treatment of the data stream, coming from
the accelerometer, using the interrupts. We have implemented the bottom half of Threaded IRQ
to perform the I2 C transactions to read the FIFO of the accelerometer. We also used the kfifo
module in the kernel to add data buffering on the driver level. Finally, we added a wait function
for the read callback, which blocks the process, resumed by the wake up() call in the interrupt
handler, when new data is available, passing the data from the accelerometer to the user program
at the rate of accelerometer measurements.
36
Figure 16: Output of test program
37
References
• Linux documentation, from https://docs.kernel.org/
38