- How To Compile Kernel Module
- How To Load And Unload Kernel Module
- Init And Exit Function Overview
- Char Drivers
- Important Data Structures
- Devicetree
- Concurrency And Race Conditions
To compile kernel module for a target platform (e.g. BeagleBone Black) you
have to specify additional flags like ARCH
and CROSS_COMPILE
:
$ make ARCH=<processor architecture> CROSS_COMPILE=<compiler> -C
<path to Linux kernel directory> M=<kernel module directory> modules
Example:
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -C
~/workspace/beaglebone_linux/ M=${PWD} modules
To compile kernel module for a host PC you don't have to specify any additional flags like in case of cross compilation:
$ make -C <path to built Linux kernel source of your host PC>
M=<kernel module directory> modules
Example:
$ make -C /lib/modules/$(shell uname -r)/build/ M=${PWD} modules
To remove all the files built during module compilation process replace
modules
with clean
:
Cross compilation cleanup:
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -C
~/workspace/embedded_linux/beaglebone_linux/ M=${PWD} clean
Host PC compilation cleanup:
$ make -C /lib/modules/$(shell uname -r)/build/ M=${PWD} clean
The most important line inside Makefile
is this one:
obj-m := <module name>.o
It states that there is one module to be built from the object file
<module name>.o
. The result of this built will be the <module name>.ko
.
If your module is generated from e.g. two source files (called file1.o
and
file2.o
) then the it should look like this:
obj-m := <module name>.o
module-objs := file1.o file2.o
If you want to generate two (or more) modules, then:
obj-m := <1st module name>.o <2nd module name>.o
To load the module you have to use insmod
command:
$ sudo insmod <module name>.ko
To unload the module you have to use rmmod
command:
$ sudo rmmod <module name>.ko
To check which modules are currently loaded in the kernel use lsmod
command:
$ lsmod
lsmod
works by reading the /proc/modules
virtual file. More information can
be found inside /sys/module
.
The basic initialization function should look like this:
static int __init my_module_init(void)
{
/* Initialization code. */
}
module_init(my_function_init);
The best practice is to make that function static
since it's not meant to be
visible outside the specific file.
The __init
token is a hint to the kernel that the given function is used only
at initialization time so it may be removed freeing up some memory.
The basic exit (cleanup) function should look like this:
static void __exit my_module_exit(void)
{
/* Exit (cleanup) code. */
}
module_exit(my_module_exit);
As in the case of init function, the best practice is to make that function
static
.
The __exit
modifier marks code of that function as being for module unload
only. If your module is built directly into the kernel or the kernel is
configured to disallow the unloading of modules, then functions prepended with
__exit
token are simply discarded.
If during initialization a particular error occurs, which doesn't allow to load a module, you must undo any registration activites performed before the failure. If you don't do that, the kernel will be left in an unstable state, which may cause many unexpected problems.
Unfortunately sometimes error recovery is best handled with the goto
statement, which you normally should avoid because it's the worst thing in
the entire programming world. Nevertheless it can eliminate a great deal of
complicated, highly-indented logic.
On the other hand, if your initialization and exit (cleanup) functions are more
complex than dealing with a few items, then goto
approach may be replaced
with something more sophisticated.
It means that the initialization functions calls exit (cleanup) function from
itself if any errors occur.
Remember that exit (cleanup) function can't be prepended with __exit
token then.
A character (char) device is one that can be accessed as a stream of bytes
(like a file) and the char driver is in charge of implementing this behavior.
It implements system calls like open
, close
, read
, write
and more
depending on requirements.
Char devices are acessed through names in the filesystem, called:
- special files;
- device files;
- nodes of the filesystem tree.
Special files for char devices are located in the /dev/
directory and are
identified by a c
in the first column of the output of ls -l
command, e.g.
crw-rw-rw- 1 root tty 4, 64 Jul 4 1998 ttyS0
If you issue ls -l /dev/
command, you will see many strings like this one:
crw-rw-rw- 1 root tty 4, 64 Jul 4 1998 ttyS0
Such a string includes two numbers (separated by a comma) before the date of the last modification (for most of other files, this place is occupied by the file length information).
These numbers are the major
and minor
device number for the particular
device (in this case major
is 4
and minor
is 64
).
Major
number identifies the driver associated with the device (in the above
mentioned case ttyS0 is managed by driver 4).
Minor
number is used by the kernel to determine exactly which device is being
referred to. The kernel itself knows almost nothing about minor numbers beyond
the fact that they refer to device implemented by your driver.
dev_t
type (defined in <linux/types.h>
) is used to hold device numbers
(both the major and minor parts).
To obtain the major or minor parts of a dev_t
, use (defined in
<linux/kdev_t.h>
):
MAJOR(dev_t dev);
MINOR(dev_t dev);
The first thing which should be done by your driver is to obtain device
numbers. There are two functions necessary for this task (declared in
<linux/fs.h>
).
If you know ahead of time exactly which device numbers you want, then use:
int register_chrdev_region(dev_t first, unsgined int count, char *name);
However, probably you will not know which major numbers your device will use, so you should use:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);
-
dev
is an output-only parameter that will hold the first number in your allocated range. -
firstminor
should be the first minor number to use (usually 0). -
count
is the total number of contiguous device numbers you are requesting. -
name
is the name fo the device that should be associated with this number range (it will appear in/proc/devices
file and sysfs).
If allocated device numbers are no longer required (due to some init error or module removal) they should be freed with:
void unregister_chrdev_region(dev_t first, unsgined int count);
To represent char devices, the kernel uses structures of type struct cdev
(defined in <linux/cdev.h>
).
To initialize cdev
structure, you should call:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
Regardless of calling that function you have to initialize one struct cdev
field manually, i.e. cdev.owner
field with THIS_MODULE
.
Once the cdev
structure is set up, the final step is to tell the kernel about
it:
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
Where:
-
dev
is thecdev
structure. -
num
is the first device number to which this device responds. -
count
is the number of device numbers that should be associated with the device (often set to1
).
To remove a char device from the system, simply call:
void cdev_del(struct cdev *dev);
struct file_operations
(defined in <linux/fs.h>
) sets up connection
between device numbers and driver's operations. It is a collection of function
pointers. Each open file is associated with its own set of functions
(file
structure includes f_op
field that points to a file_operations
structure). Those driver's operations are mostly system calls, e.g. open
,
read
, write
and so on.
The very basic set of file operations for random char device may look like this:
struct file_operations my_device_fops = {
.owner = THIS_MODULE,
.llseek = my_device_llseek,
.read = my_device_read,
.write = my_device_write,
.ioctl = my_device_ioctl,
.open = my_device_open,
.release = my_device_release
};
struct file
(defined in <linux/fs.h>
) is the second most important data
structure used in device drivers.
It represents an open file descriptor (it is not specific to device drivers;
every open file in the system has an associated struct file
in kernel space).
It is created by the kernel on open
and is passed to any function that
operates on the file. After all instances of the file are closed, the kernel
releases the data structure.
It is worth mentioning, that drivers never create file
structures, they only
access structures created elsewhere.
struct inode
is used by the kernel internally to represent files, but it is
not the same as struct file
- there can be numerous file
structures
representing multiple open descriptors on a single file, but they all point to
a single inode
structure.
inode
structure provides great deal of information about the file, but in
case of device driver code only a few fields are worth of interest:
dev_t i_rdev
- actual device number;struct cdev *i_cdev
- kernel's internal structure that represents char devices.
Due to the fact that kernel changes "from time to time", a good practice is to use predefined macros to obtain the major and minor number from an inode:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
Devicetree
is a data structure describing the hardware components of a
particular computer/board so that the operating system's kernel (e.g. Linux
kernel) can use and manage those components, including CPU (or CPUs),
the memory, the buses, and the peripherals.
Devicetree files are not monolithic, which means they can be split into several files.
.dts
file is the final Devicetree, it contains the board-level information,
e.g. for BeagleBone Black there is am335x-boneblack.dts
file, which includes
a few .dtsi
files.
.dtsi
file is the included file, which typically contains definition of
SoC level information or other information common for similar boards, e.g.
information about AM33XX SoC is included inside am33xx.dtsi
and
am33xx-l4.dtsi
files.
To compile devicetree overlay against given Linux source, you may use either
device-tree-compiler (dtc)
by yourself or copy the overlay file to the
Linux source directory and edit respective Makefile
to perform compilation.
Using device-tree-compiler (dtc)
by yourself is very simple, but it may get
problematic while trying to use it against respective Linux source, anyway
to compile devicetree overlay simply run the following command:
$ dtc -@ -I dts -O dtb -o overlay_name.dtbo overlay_name.dts
The second approach requires a bit more work, but it's not hard at all. Follow these steps to perfom whole process correctly:
-
Copy your devicetree overlay source file (e.g.
overlay_name.dts
) to the respective directory under Linux source, (e.g.beaglebone_linux/arch/arm/boot/dts/overlays
). -
Edit
Makefile
inside above mentioned directory to createoverlay_name.dtbo
, e.g. this is the part of theMakefile
from Beaglebone Linux source directory underarch/arm/boot/dts/overlays
:
# Overlays for the BeagleBone platform
dtbo-$(CONFIG_ARCH_OMAP2PLUS) += \
BB-ADC-00A0.dtbo \
BB-BBBW-WL1835-00A0.dtbo \
BB-BBGG-WL1835-00A0.dtbo \
BB-BBGW-WL1835-00A0.dtbo \
BB-BONE-4D5R-01-00A1.dtbo \
BB-BONE-eMMC1-01-00A0.dtbo \
BB-BONE-LCD4-01-00A1.dtbo \
BB-BONE-NH7C-01-A0.dtbo \
BB-CAPE-DISP-CT4-00A0.dtbo \
BB-HDMI-TDA998x-00A0.dtbo \
BB-SPIDEV0-00A0.dtbo \
BB-SPIDEV1-00A0.dtbo \
BBORG_COMMS-00A2.dtbo \
BBORG_FAN-A000.dtbo \
BBORG_RELAY-00A2.dtbo \
BONE-ADC.dtbo \
M-BB-BBG-00A0.dtbo \
M-BB-BBGG-00A0.dtbo \
PB-HACKADAY-2021.dtbo \
overlay_name.dtbo
targets += dtbs dtbs_install
targets += $(dtbo-y)
always-y := $(dtbo-y)
clean-files := *.dtbo
Take a look at that Makefile
, it adds overlay_name.dtbo
file to be
generated while compiling all other devicetree files.
- After copying your
overlay_name.dts
file and updatingMakefile
, it's time to call compiler (in this case cross compiler) to generateoverlay_name.dtbo
file. From the Linux source directory invoke the following command:
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs
That's all, you may find your overlay_name.dtbo
file under
arch/arm/boot/dts/overlays
.
Note that ARCH
and CROSS_COMPILE
may be different depending on your device
architecture and respective cross compiler.
Cell
is a different word for integer values represented as 32-bit integers.
#address-cells
and #size-cells
describes how many cells are used in
sub-nodes (child nodes) to encode the address and size in the reg
property,
e.g.
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells <1>;
i2c0: i2c@f1001000 {
compatible = "ti,omap2-i2c";
reg = <0xf1001000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
eeprom_ext: eeprom@52 {
reg = <0x52>;
};
};
};
interrupt-controller
is a boolean property indicating that the current node
is an interrupt controller, e.g. (from armv7-m.dtsi
):
/ {
nvic: interrupt-controller@e000e100 {
compatible = "arm,armv7m-nvic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0xe000e100 0xc00>;
};
};
#interrupt-cells
indicates the number of cells in the interrupts
property
for the interrupts managed by the selected interrupt controller.
interrupt-parent
is a phandle pointing to the interrupt controller for
the current node, e.g. (from armv7-m.dtsi
):
/ {
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&nvic>;
ranges;
};
};
Use mutex
if:
- critical section is in "process/user context" and is atomic;
- critical section is in "process/user context" and may sleep holding the lock.
Use spinlock
if:
- critical section is in "interrupt context" (which means it cannot sleep).