The Kernel and DTS files
The main target of this book is to give several suggestions for rapid programming methods to be used on an embedded GNU/Linux system. However, the main target of every embedded developer is to realize programs to manage peripherals, to monitor or to control devices, and other similar tasks to interact with the real world. So, we mainly need to know the techniques useful to get access to the peripheral's data and settings.
That's why, we need to know how to recompile the kernel and how to configure it.
Recompiling the kernel
Our developer kits are well supported, and in this situation, it is quite rare that we need a complete kernel recompilation. However, knowing how to do this step is quite essential for every embedded developer (it may happen that we need to add some external peripherals or modify the default configuration).
Since we decided to use the Robert C. Nelson repositories, we can still continue using them. However, some words must be spent to clarify some basic commands useful to manage the kernel code even if we use a generic kernel repository.
Referring to the SAMA5D3 Xplained kernel compilation in SAMA5D3 Xplained section in Chapter 1 , Installing the Developing System, we used the build_kernel.sh
script to operate on the sources. However, this is not the standard way to manage the kernel code. In fact, Robert C. Nelson did a very good job, but we want to learn how to manage the kernel even without using his tools! So, let's take a look at what steps build_kernel.sh
follows to do its job.
Like every good program, the main procedure is at the end of the file, and there, we will see the following code:
... . "${DIR}/version.sh" export LINUX_GIT unset FULL_REBUILD #FULL_REBUILD=1 if [ "${FULL_REBUILD}" ] ; then /bin/sh -e "${DIR}/scripts/git.sh" || { exit 1 ; } if [ "${RUN_BISECT}" ] ; then /bin/sh -e "${DIR}/scripts/bisect.sh" || { exit 1 ; } fi if [ ! -f "${DIR}/.yakbuild" ] ; then patch_kernel fi copy_defconfig fi if [ ! "${AUTO_BUILD}" ] ; then make_menuconfig fi if [ -f "${DIR}/.yakbuild" ] ; then BUILD=$(echo ${kernel_tag} | sed 's/[^-]*//'|| true) fi make_kernel make_modules_pkg make_firmware_pkg if grep -q dtbs "${DIR}/KERNEL/arch/${KERNEL_ARCH}/Makefile"; then make_dtbs_pkg fi echo "-----------------------------" echo "Script Complete" echo "${KERNEL_UTS}" > kernel_version echo "eewiki.net: [user@host:~$ export kernel_version=${KERNEL_UTS}]" echo "-----------------------------"
So, the following steps are performed:
FULL_REBUILD
: Where the sources are checkout out form the Git repositories and then pachedAUTO_BUILD
: Where we configure the kernelmake_kernel
: Where we effectively build up the kernel and its components (modules, firmware, and device trees)make_modules_pkg
,make_firmware_pkg
, andmake_dtbs_pkg
: Where we create some packages holding the kernel's modules, the firmware binaries, and the device trees
The first step is not related to kernel compilation since it's needed to get the kernel's sources only.
Tip
At this point, we should notice that in order to avoid download the kernel sources each time we execute the script, we should disable this step by unsetting the FULL_REBUILD
variable with the following patch:
--- a/build_kernel.sh
+++ b/build_kernel.sh
@@ -227,8 +227,7 @@ fi
. "${DIR}/version.sh"
export LINUX_GIT
-#unset FULL_REBUILD
-FULL_REBUILD=1
+unset FULL_REBUILD
if [ "${FULL_REBUILD}" ] ; then
/bin/sh -e "${DIR}/scripts/git.sh" || { exi
t 1 ; }
Again, the fourth step is just to pack the modules and other kernel's components, and it's not relevant to us. So, the second and third steps are the ones we have to look at carefully:
The make_menuconfig
function is defined here:
make_menuconfig () { cd "${DIR}/KERNEL" || exit make ARCH=${KERNEL_ARCH} CROSS_COMPILE="${CC}" menuconfig if [ ! -f "${DIR}/.yakbuild" ] ; then cp -v .config "${DIR}/patches/defconfig" fi cd "${DIR}/" || exit }
This tells to us that the command to execute the kernel configuration menu is as follows:
make ARCH=${KERNEL_ARCH} CROSS_COMPILE="${CC}" menuconfig
Of course, we have to specify the KERNEL_ARCH
and CROSS_COMPILE
variables, but considering what we did before, it's quite obvious that the command changes to this:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
To test it, we can go into the directory where SAMA5D3 Xplained's kernel sources are placed (directory A5D3/armv7_devel/KERNEL
) and then execute the make
command:
$ cd A5D3/armv7_devel/KERNEL $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
Great, it works! This is the configuration kernel command to use in every situation. When we look at the make_menuconfig
function, we see that the result of the kernel configuration is stored in the .config
file. This is the file where every setting we do in the kernel configuration stage is placed.
The make_kernel
function is a bit more complex, but the relevant code is shown here:
##uImage, if you really really want a uImage, zreladdr needs to be ##defined on the build line going forward... ##make sure to install your distro's version of mkimage #image="uImage" #address="LOADADDR=${ZRELADDR}" cd "${DIR}/KERNEL" || exit echo "-----------------------------" echo "make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CROSS_ COMPILE="${CC}" ${address} ${image} modules" echo "-----------------------------" make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CROSS_COMPIL E="${CC}" ${address} ${image} modules echo "-----------------------------" if grep -q dtbs "${DIR}/KERNEL/arch/${KERNEL_ARCH}/Makefile"; then echo "make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CR OSS_COMPILE="${CC}" dtbs" echo "-----------------------------" make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CROSS_CO MPILE="${CC}" dtbs echo "-----------------------------" fi
We can find the make
commands again that do the job. They're reported here:
make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CROSS_COMPIL E="${CC}" ${address} ${image} modules make -j${CORES} ARCH=${KERNEL_ARCH} LOCALVERSION=${BUILD} CROSS_COMPIL E="${CC}" dtbs
The former compiles the kernel and its modules, while the latter generates the device tree files (see the next section for further information on the device tree).
Again, we have to substitute the proper values to the variables mentioned earlier, but this is a tricky task. In fact, the CORES
variable is just a number equal to the core number of our host PC. KERNEL_ARCH
and CROSS_COMPILE
must be obviously set to arm
and arm-linux-gnueabihf-
, while BUILD
is just a descriptive string. So, we can choose whatever we wish. The last variables address
and image
need some more explanation. In fact, we can choose to compile the kernel in two main formats: uImage and zImage. For the former compilation, we need to specify a load address also (the LOADADDR
variable), while for the latter, we don't. Since it's the form we used in our developer kits, the two commands mentioned earlier can be rewritten as follows:
make -j8 ARCH=arm LOCALVERSION="dummy" CROSS_COMPILE=arm-linux-gnueabi hf- zImage modules dtbs
Then, to verify that everything is well configured, we just have to test the command:
$ make -j8 ARCH=arm LOCALVERSION="dummy" CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs CHK include/config/kernel.release ... LD kernel/built-in.o LINK vmlinux LD vmlinux.o MODPOST vmlinux.o GEN .version CHK include/generated/compile.h UPD include/generated/compile.h CC init/version.o LD init/built-in.o KSYM .tmp_kallsyms1.o KSYM .tmp_kallsyms2.o LD vmlinux SORTEX vmlinux SYSMAP System.map Building modules, stage 2. OBJCOPY arch/arm/boot/Image Kernel: arch/arm/boot/Image is ready ...
Yes, it works! Now, we're ready to recompile every kernel tree.
The device tree
The device tree is a data structure to describe hardware. That's all. Rather than hard coding every kernel setting into the code, it can be described in a well-defined data structure that is passed to the kernel at boot time.
The difference between the device tree and the kernel configuration file (the .config
file) is that while the .config
file tells us which components of the kernel are enabled and which are not, the device tree holds their configurations. So, if we wish to add a driver from the kernel's sources to our system, we have to specify it into the .config
file. On the other hand, if we wish to specify the driver settings (memory addresses, special settings, and so on), we have to specify them in the device tree.
In all our developer kits, during the boot stage, we can see some messages from U-Boot's serial console as shown here:
... reading /dtbs/at91-sama5d3_xplained.dtb 34918 bytes read in 11 ms (3 MiB/s) reading zImage 3810568 bytes read in 244 ms (14.9 MiB/s) Kernel image @ 0x22000000 [ 0x000000 - 0x3a2508 ] ## Flattened Device Tree blob at 21000000 Booting using the fdt blob at 0x21000000 Loading Device Tree to 2fadc000, end 2fae7865 ... OK Starting kernel ... ...
Here, we can see that U-Boots loads a DTB file (the binary form of a device tree) and that it passes the file to the kernel.
So, it's time to take a look at how a device tree source (DTS) file is done and how we can generate a device tree binary (DTB) file from it in order to be used to our kernel. As a very simple example, let's see how the LEDs driver (will see this special driver in LEDs and triggers section, Chapter 6 , General Purposes Input Output signals – GPIO ) can be enabled and how it's defined into the default SAMA5D3 Xplained's DTS file.
To enable the LED driver compilation, we must open the Kernel Configuration menu and then navigate to Device Drivers | LED Support sub menu to enable the proper entries, as shown in the following screenshot:
These settings result in the following output in the.config
file:
$ grep '^CONFIG_LEDS_' .config CONFIG_LEDS_CLASS=y CONFIG_LEDS_GPIO=y CONFIG_LEDS_PWM=y CONFIG_LEDS_TRIGGERS=y CONFIG_LEDS_TRIGGER_TIMER=y CONFIG_LEDS_TRIGGER_HEARTBEAT=y CONFIG_LEDS_TRIGGER_GPIO=y
This enabled the de-facto kernel compilation. However, to effectively define the LEDs present into the SAMA5D3 Xplained, we have to use the following code hold in the arch/arm/boot/dts/at91-sama5d3_xplained.dts
file from the kernel's tree:
leds { compatible = "gpio-leds"; d2 { label = "d2"; gpios = <&pioE 23 GPIO_ACTIVE_LOW>; linux,default-trigger = "heartbeat"; };
d3 { label = "d3"; gpios = <&pioE 24 GPIO_ACTIVE_HIGH>; }; };
The scope of this book does not explain how the device tree works. However, we must explain the preceding code a bit since we're going to use the device tree in several parts of this book.
Tip
You can get more information on the device tree at the project's home site at: http://www.devicetree.org/ .
Text between the characters pairs /*
and */
are comments, while the leds
string on the first line of the preceding code is the label of a new block related to the LEDs driver. In fact, this is specified by the next line where the compatible
property is. The gpio-leds
string defines the LEDs driver as specified in the drivers/leds/leds-gpio.c
file:
static const struct of_device_id of_gpio_leds_match[] = { { .compatible = "gpio-leds", }, {}, };
Then, the sub-blocks labeled as d2
and d3
define two LEDs labeled with the same names. The gpios
lines definition follows, that is, the specification of the GPIOs where the LEDs are physically connected.
As a final note, we see that in the d2
block definition, there is specified a special setting:
linux,default-trigger = "heartbeat";
This is to define the trigger to be used for the LED.
Tip
For further information regarding the LEDs driver and its trigger, you can take a look at the Documentation/leds/
directory in the Linux's code repository.
The preceding device tree code is very simple, and it cannot explain all the device tree's components. However, don't worry. Each time, we'll use a device tree settings, we're going to explain it as best as possible.