0% found this document useful (0 votes)
33 views

Embedded Linux Labs

Uploaded by

issam sayyaf
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views

Embedded Linux Labs

Uploaded by

issam sayyaf
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 71

Embedded Linux System Development

STM32MP1 variant

Practical Labs

https://bootlin.com

April 02, 2024


Embedded Linux System Development

About this document


Updates to this document can be found on https://bootlin.com/doc/training/embedded-linux.
This document was generated from LaTeX sources found on https://github.com/bootlin/training-materials.
More details about our training sessions can be found on https://bootlin.com/training.

Copying this document


© 2004-2024, Bootlin, https://bootlin.com.
This document is released under the terms of the Creative Commons CC BY-SA 3.0 license
. This means that you are free to download, distribute and even modify it, under certain
conditions.
Corrections, suggestions, contributions and translations are welcome!

2 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Training setup
Download files and directories used in practical labs

Install lab data


For the different labs in this course, your instructor has prepared a set of data (kernel images, kernel config-
urations, root filesystems and more). Download and extract its tarball from a terminal:

$ cd
$ wget https://bootlin.com/doc/training/sessions/online.embedded-linux.apr2024/\
embedded-linux-labs.tar.xz
$ tar xvf embedded-linux-labs.tar.xz

Lab data are now available in an embedded-linux-labs directory in your home directory. This directory
contains directories and files used in the various practical labs. It will also be used as working space, in
particular to keep generated files separate when needed.

Update your distribution


To avoid any issue installing packages during the practical labs, you should apply the latest updates to the
packages in your distro:

$ sudo apt update


$ sudo apt dist-upgrade

You are now ready to start the real practical labs!

Install extra packages


Feel free to install other packages you may need for your development environment. In particular, we
recommend to install your favorite text editor and configure it to your taste. The favorite text editors of
embedded Linux developers are of course Vim and Emacs, but there are also plenty of other possibilities,
such as Visual Studio Code1 , GEdit, Qt Creator, CodeBlocks, Geany, etc.
It is worth mentioning that by default, Ubuntu comes with a very limited version of the vi editor. So if you
would like to use vi, we recommend to use the more featureful version by installing the vim package.

More guidelines
Can be useful throughout any of the labs
• Read instructions and tips carefully. Lots of people make mistakes or waste time because they missed
an explanation or a guideline.
• Always read error messages carefully, in particular the first one which is issued. Some people stumble
on very simple errors just because they specified a wrong file path and didn’t pay enough attention to
the corresponding error message.
• Never stay stuck with a strange problem more than 5 minutes. Show your problem to your colleagues
or to the instructor.
• You should only use the root user for operations that require super-user privileges, such as: mounting
a file system, loading a kernel module, changing file ownership, configuring the network. Most regular
1 This tool from Microsoft is Open Source! To try it on Ubuntu: sudo snap install code --classic

© 2004-2024 Bootlin, CC BY-SA license 3


Embedded Linux System Development

tasks (such as downloading, extracting sources, compiling...) can be done as a regular user.
• If you ran commands from a root shell by mistake, your regular user may no longer be able to handle
the corresponding generated files. In this case, use the chown -R command to give the new files back
to your regular user.
Example: $ sudo chown -R myuser.myuser linux/

4 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Building a cross-compiling toolchain


Objective: Learn how to compile your own cross-compiling toolchain for the
musl C library

After this lab, you will be able to:

• Configure the crosstool-ng tool

• Execute crosstool-ng and build up your own cross-compiling toolchain

Setup
Go to the $HOME/embedded-linux-labs/toolchain directory.

For this lab, you need a system or VM with a least 4 GB of RAM.

Install needed packages


Install the packages needed for this lab:

$ sudo apt install build-essential git autoconf bison flex texinfo help2man gawk libtool-bin \
libncurses5-dev unzip

Getting Crosstool-ng
Let’s download the sources of Crosstool-ng, through its git source repository, and switch to a commit that
we have tested:
$ git clone https://github.com/crosstool-ng/crosstool-ng
$ cd crosstool-ng/
$ git checkout crosstool-ng-1.26.0

Building and installing Crosstool-ng


As we are not building Crosstool-ng from a release archive but from a git repository, we first need to generate
a configure script and more generally all the generated files that are shipped in the source archive for a
release:
$ ./bootstrap

We can then either install Crosstool-ng globally on the system, or keep it locally in its download direc-
tory. We’ll choose the latter solution. As documented at https://crosstool-ng.github.io/docs/install/
#hackers-way, do:

$ ./configure --enable-local
$ make

Then you can get Crosstool-ng help by running

$ ./ct-ng help

© 2004-2024 Bootlin, CC BY-SA license 5


Embedded Linux System Development

Configure the toolchain to produce


A single installation of Crosstool-ng allows to produce as many toolchains as you want, for different archi-
tectures, with different C libraries and different versions of the various components.
Crosstool-ng comes with a set of ready-made configuration files for various typical setups: Crosstool-ng calls
them samples. They can be listed by using ./ct-ng list-samples.
We will load the Cortex A5 sample, as Crosstool-ng doesn’t have any sample for Cortex A7 yet . Load it
with the ./ct-ng command.
Then, to refine the configuration, let’s run the menuconfig interface:

$ ./ct-ng menuconfig

In Path and misc options:


• If not set yet, enable Try features marked as EXPERIMENTAL
In Target options:
• Set Emit assembly for CPU (ARCH_CPU) to cortex-a7.
• Set Use specific FPU (ARCH_FPU) to vfpv4.
In Toolchain options:
• Set Tuple's vendor string (TARGET_VENDOR) to training.
• Set Tuple's alias (TARGET_ALIAS) to arm-linux. This way, we will be able to use the compiler as
arm-linux-gcc instead of arm-training-linux-musleabihf-gcc, which is much longer to type.
In Operating System:
• Set Version of linux to the 6.1.x version that is proposed. We choose this version because this matches
the version of the kernel we will run on the board. At least, the version of the kernel headers are not
more recent.
In C-library:
• If not set yet, set C library to musl (LIBC_MUSL)
• Keep the default version that is proposed
In C compiler:
• Set Version of gcc to 12.3.0.
• Make sure that C++ (CC_LANG_CXX) is enabled
In Debug facilities:
• Remove all options here. Some debugging tools can be provided in the toolchain, but they can also be
built by filesystem building tools.
Explore the different other available options by traveling through the menus and looking at the help for some
of the options. Don’t hesitate to ask your trainer for details on the available options. However, remember
that we tested the labs with the configuration described above. You might waste time with unexpected issues
if you customize the toolchain configuration.

Produce the toolchain


Nothing is simpler:

$ ./ct-ng build

6 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

The toolchain will be installed by default in $HOME/x-tools/. That’s something you could have changed in
Crosstool-ng’s configuration.
And wait!

Testing the toolchain


You can now test your toolchain by adding $HOME/x-tools/arm-training-linux-musleabihf/bin/ to your
PATH environment variable and compiling the simple hello.c program in your main lab directory with arm-
linux-gcc:

$ arm-linux-gcc -o hello hello.c

You can use the file command on your binary to make sure it has correctly been compiled for the ARM
architecture.
Did you know that you can still execute this binary from your x86 host? To do this, install the QEMU user
emulator, which just emulates target instruction sets, not an entire system with devices:

$ sudo apt install qemu-user

Now, try to run QEMU ARM user emulator:

$ qemu-arm hello
qemu-arm: Could not open '/lib/ld-musl-armhf.so.1': No such file or directory

What’s happening is that qemu-arm is missing the shared library loader (compiled for ARM) that this binary
relies on. Let’s find it in our newly compiled toolchain:

$ find ~/x-tools -name ld-musl-armhf.so.1

/home/tux/x-tools/arm-training-linux-musleabihf/arm-training-linux-musleabihf/sysroot/lib/
ld-musl-armhf.so.1
We can now use the -L option of qemu-arm to let it knowwhere shared libraries are:

$ qemu-arm -L ~/x-tools/arm-training-linux-musleabihf/arm-training-linux-musleabihf/sysroot \
hello

Hello world!

Cleaning up
Do this only if you have limited storage space. In case you made a mistake in the toolchain configuration,
you may need to run Crosstool-ng again, keeping generated files would save a significant amount of time.
To save about 9 GB of storage space, do a ./ct-ng clean in the Crosstool-NG source directory. This will
remove the source code of the different toolchain components, as well as all the generated files that are now
useless since the toolchain has been installed in $HOME/x-tools.

© 2004-2024 Bootlin, CC BY-SA license 7


Embedded Linux System Development

Bootloader - TF-A and U-Boot


Objectives: Set up serial communication, compile and install the U-Boot boot-
loader, use basic U-Boot commands, set up TFTP communication with the
development workstation.

As the bootloader is the first piece of software executed by a hardware platform, the installation procedure
of the bootloader is very specific to the hardware platform. There are usually two cases:
• The processor offers nothing to ease the installation of the bootloader, in which case the JTAG has to
be used to initialize flash storage and write the bootloader code to flash. Detailed knowledge of the
hardware is of course required to perform these operations.
• The processor offers a monitor, implemented in ROM, and through which access to the memories is
made easier.
The STM32MP1 SoC, falls into the second category. The monitor integrated in the ROM reads the SD card
to search for a valid bootloader (the boot mode is actually configurable via a few input pins). In case no
bootloader is found, it will operate in a fallback mode, that will allow to use an external tool to reflash some
executable through USB. Therefore, either by using an MMC/SD card or that fallback mode, we can start
up an STM32MP1-based board without having anything installed on it.

Setup
Go to the $HOME/embedded-linux-labs/bootloader directory.

Setting up serial communication with the board


Plug the USB-A to micro USB-B cable on the Discovery board. There is only one micro USB port on the
board, it is CN11, also named ST-LINK. This is a debug interface and exposes multiple debugging interfaces,
including a serial interface. When plugged in your computer, a serial port should appear, /dev/ttyACM0.
You can also see this device appear by looking at the output of sudo dmesg.
To communicate with the board through the serial port, install a serial communication program, such as
picocom:

$ sudo apt install picocom

If you run ls -l /dev/ttyACM0, you can also see that only root and users belonging to the dialout group
have read and write access to the serial console. Therefore, you need to add your user to the dialout group:

$ sudo adduser $USER dialout

Important: for the group change to be effective, you have to reboot your computer (at least on Ubuntu
22.04) and log in again. A workaround is to run newgrp dialout, but it is not global. You have to run it in
each terminal.
Run picocom -b 115200 /dev/ttyACM0, to start serial communication on /dev/ttyACM0, with a baudrate of
115200. If you wish to exit picocom, press [Ctrl][a] followed by [Ctrl][x].
Don’t be surprised if you don’t get anything on the serial console yet, even if you reset the board. That’s
because the SoC has nothing to boot on yet. We will prepare a micro SD card to boot on in the next
paragraphs.

8 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

TF-A and U-Boot relationship


The boot process is done in two steps with the ROM monitor trying to execute a first piece of software,
called fsbl, from its internal SRAM, that will initialize the DRAM, and a second program, ssbl that will in
turn load Linux and execute it.

In our case, fsbl is provided by TF-A BL2 and ssbl is provided by U-Boot.

TF-A BL2 is loading U-Boot from the Firmware Image Package (FIP), that will also contain the configuration
for this second part. The FIP is generated from TF-A sources, so first we are going to build U-Boot.

U-Boot setup
Download U-Boot:
$ git clone https://gitlab.denx.de/u-boot/u-boot
$ cd u-boot
$ git checkout v2023.04

Get an understanding of U-Boot’s configuration and compilation steps by reading the README file, and specif-
ically the Building the Software section.

Basically, you need to:

1. Specify the cross-compiler prefix (the part before gcc in the cross-compiler executable name):

$ export CROSS_COMPILE=arm-linux-

2. Run $ make <NAME>_defconfig , where the list of available configurations can be found in the configs/
directory. There are multiple stm32mp15 configurations. We will use the standard one (stm32mp15).

3. Now that you have a valid initial configuration, you can now run $ make menuconfig to further edit
your bootloader features.

• In the Environment submenu, we will configure U-Boot so that it stores its environment inside a
file called uboot.env in an ext4 filesystem:

– Disable Environment is not stored. We want changes to variables to be persistent across


reboots

– Enable Environment is in a EXT4 filesystem. Disable all other options for environment
storage (e.g. MMC, SPI, UBI)

– Name of the block device for the environment: mmc

– Device and partition for where to store the environment in EXT4: 0:4

– Name of the EXT4 file to use for the environment: /uboot.env

• In the Device Drivers → Watchdog Timer Support submenu, disable IWDG watchdog driver for
STM32 MP's family, so that U-Boot doesn’t start the watchdog.

Install the following packages which should be needed to compile U-Boot for your board:

$ sudo apt install libssl-dev device-tree-compiler swig \


python3-distutils python3-dev python3-setuptools

4. Finally, run

make DEVICE_TREE=stm32mp157a-dk1

© 2004-2024 Bootlin, CC BY-SA license 9


Embedded Linux System Development

which will build U-Boot 2 . The DEVICE_TREE variable specifies the specific Device Tree that describes
our hardware board. You can see that in this case, U-Boot only ships a Device Tree for the board with
the previous version of the chip (stm32mp157a instead of stm32mp157d). Alternatively, if you wish to
run just make, specify our board’s device tree name on Device Tree Control → Default Device Tree
for DT Control option.

TF-A setup
Get the mainline TF-A sources:
$ cd ..
$ git clone https://git.trustedfirmware.org/TF-A/trusted-firmware-a.git
$ cd trusted-firmware-a/
$ git checkout v2.9

Several configuration parameters have to be passed to the Makefile:


• Specify the cross-compiler prefix (the part before gcc in the cross-compiler executable name), either
using the environment variable: $ export CROSS_COMPILE=arm-linux- , or just by adding it to the make
commande line.
• The architecture has to be selected: ARCH=aarch32, as well as the major version of Arm Architecture,
here the Cortex A7 is an Armv7, so we need to use ARM_ARCH_MAJOR=7
• The STM32MP1 platform is selected too with PLAT=stm32mp1
• Specify the AArch32 Secure Payload component, we are going to use a minimal monitor implementation
provided by TF-A: the SP-MIN. For this we need to add the following variable: AARCH32_SP=sp_min
• For this specific board, the device tree is generated and then needs to be specifed: DTB_FILE_NAME=
stm32mp157a-dk1.dtb
• Specify the configuration of this firmware which is actually the Device Tree passed to U-Boot: BL33_
CFG=../u-boot/u-boot.dtb
• Specify that the fsbl will be located on the SD card with STM32MP_SDMMC=1.
• Specify the location of the BL33 (Boot loader stage 3-3): BL33=../u-boot/u-boot-nodtb.bin
We can now generate the bl32, dtb, and fip targets with a single command line:

$ make ARM_ARCH_MAJOR=7 ARCH=aarch32 PLAT=stm32mp1 AARCH32_SP=sp_min \


DTB_FILE_NAME=stm32mp157a-dk1.dtb BL33=../u-boot/u-boot-nodtb.bin \
BL33_CFG=../u-boot/u-boot.dtb STM32MP_SDMMC=1 fip all

At the end of the build, the important output files generated are located in build/stm32mp1/release/. We
will find there:
• tf-a-stm32mp157a-dk1.stm32, which is TF-A BL2, serving as our first stage bootloader
• fip.bin, which is the FIP image, which itself includes U-Boot. This image will serve as the second
stage bootloader.

Flashing the bootloaders


The ROM monitor will look for the first stage bootloader in a partition named fsbl1. If it cannot find a
valid bootloader in this partition, it will then try to load it from a partition named fslb2. This first stage
bootloader (in our case the TF-A BL2) will load the second bootloader (U-Boot) from the Firmware Image
Package located in the partition named fip. At the same time, BL2 will also load the BL32 monitor (SP-min)
from the FIP. Finally, U-Boot will store its environment in the fourth partition, which we’ll name bootfs.
2 You can speed up the compiling by using the -jX option with make, where X is the number of parallel jobs used for compiling.

Twice the number of CPU cores is a good value.

10 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

So, as far as bootloaders are concerned, the SD card partitioning will look like:
Number Start End Size File system Name Flags
1 2048s 4095s 2048s fsbl1
2 4096s 6143s 2048s fsbl2
3 6144s 10239s 4096s fip
4 10240s 131071s 120832s bootfs
On your workstation, plug in the SD card your instructor gave you. Type the sudo dmesg command to see
which device is used by your workstation. In case the device is /dev/mmcblk0, you will see something like
[46939.425299] mmc0: new high speed SDHC card at address 0007
[46939.427947] mmcblk0: mmc0:0007 SD16G 14.5 GiB
The device file name may be different (such as /dev/sdb if the card reader is connected to a USB bus (either
internally or using a USB card reader).
In the following instructions, we will assume that your SD card is seen as /dev/mmcblk0 by your PC work-
station.
Type the mount command to check your currently mounted partitions. If SD partitions are mounted, unmount
them:

$ sudo umount /dev/mmcblk0p*

We will erase the existing partition table and partition contents by simply zero-ing the first 128 MiB of the
SD card:

$ sudo dd if=/dev/zero of=/dev/mmcblk0 bs=1M count=128


Now, let’s use the parted command to create the partitions that we are going to use:

$ sudo parted /dev/mmcblk0

The ROM monitor handles GPT partition tables, let’s create one:
(parted) mklabel gpt
Then, the 4 partitions are created with:
(parted) mkpart fsbl1 0% 4095s
(parted) mkpart fsbl2 4096s 6143s
(parted) mkpart fip 6144s 10239s
(parted) mkpart bootfs 10240s 131071s
You can verify everything looks right with:
(parted) print
Model: SD SA08G (sd/mmc)
Disk /dev/mmcblk0: 7747MB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:

Number Start End Size File system Name Flags


1 1049kB 2097kB 1049kB fsbl1
2 2097kB 3146kB 1049kB fsbl2
3 3146kB 5243kB 2097kB fip
4 5243kB 67.1MB 61.9MB bootfs

(parted)

© 2004-2024 Bootlin, CC BY-SA license 11


Embedded Linux System Development

Once done, quit:


(parted) quit
Note: parted is definitely not very user friendly compared to other tools to manipulate partitions (such as
cfdisk), but that’s the only tool which supports assigning names to GPT partitions. In your projects, you
could use gparted, which is a more friendly graphical front-end on top of parted.
Now, format the boot partition as an ext4 filesystem. This is where U-Boot saves its environment:

$ sudo mkfs.ext4 -L boot -O ^metadata_csum /dev/mmcblk0p4

The -O ^metadata_csum option allows to create the filesystem without enabling metadata checksums, which
U-Boot doesn’t seem to support yet.
Now write the TF-A binary in both fsbl partitions:

$ sudo dd if=build/stm32mp1/release/tf-a-stm32mp157a-dk1.stm32 of=/dev/mmcblk0p1 bs=1M \


conv=fdatasync
$ sudo dd if=build/stm32mp1/release/tf-a-stm32mp157a-dk1.stm32 of=/dev/mmcblk0p2 bs=1M \
conv=fdatasync

Then flash the fip partition with the Firmware Image Package containing U-Boot, the BL32 monitor and
their configuration (device tree):

$ sudo dd if=build/stm32mp1/release/fip.bin of=/dev/mmcblk0p3 bs=1M conv=fdatasync

Testing the bootloaders


Insert the SD card in the board slot. You can now power-up the board by connecting the USB-C cable to
the board, in CN6, PWR_IN and to your PC at the other end. Check that it boots your new bootloaders. You
can verify this by checking the build dates:
NOTICE: CPU: STM32MP157DAC Rev.Z
NOTICE: Model: STMicroelectronics STM32MP157A-DK1 Discovery Board
NOTICE: Board: MB1272 Var3.0 Rev.C-02
NOTICE: BL2: v2.9(release):v2.9.0
NOTICE: BL2: Built : 14:35:59, Jul 6 2023
NOTICE: BL2: Booting BL32
NOTICE: SP_MIN: v2.9(release):v2.9.0
NOTICE: SP_MIN: Built : 14:35:51, Jul 6 2023

U-Boot 2023.04 (Jul 06 2023 - 14:31:56 +0200)

CPU: STM32MP157DAC Rev.Z


Model: STMicroelectronics STM32MP157A-DK1 Discovery Board
Board: stm32mp1 in trusted mode (st,stm32mp157a-dk1)
Board: MB1272 Var3.0 Rev.C-02
DRAM: 512 MiB
Clocks:
- MPU : 650 MHz
- MCU : 208.878 MHz
- AXI : 266.500 MHz
- PER : 24 MHz
- DDR : 533 MHz
Core: 275 devices, 36 uclasses, devicetree: board
NAND: 0 MiB

12 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

MMC: STM32 SD/MMC: 0


Loading Environment from EXT4... ** File not found /uboot.env **
** Unable to read "/uboot.env" from mmc0:4 **
In: serial
Out: serial
Err: serial
Previous ADC measurements was not the one expected, retry in 20ms
****************************************************
* WARNING 500mA power supply detected *
* Current too low, use a 3A power supply! *
****************************************************

Net: eth0: ethernet@5800a000


Hit any key to stop autoboot: 0
STM32MP>
In U-Boot, type the help command, and explore the few commands available.

Adding a new command to the U-Boot shell


Check whether the config command is available. This command allows to dump the configuration settings
U-Boot was compiled from.
If it’s not, go back to U-Boot’s configuration and enable it.
Re-run the build of U-Boot, and then re-run the build of TF-A so that a new version of the fip.bin with
the updated U-Boot is generated.
Update the fip partition on the SD card with the new fip.bin image and test that the command is now
available and works as expected.

Playing with the U-Boot environment


Display the U-Boot environment using printenv.
Set a new U-Boot variable foo to a value of your choice, using setenv, and verify it has been set. Reset the
board, and check if foo is still defined: it should not.
Now repeat this process, but before resetting the board, use saveenv. After the reset, check the foo variable
is still defined.
Now reset the environment to its default settings using env default -a, and save these changes using saveenv.

Setting up networking
The next step is to configure U-boot and your workstation to let your board download files, such as the
kernel image and Device Tree Binary (DTB), using the TFTP protocol through a network connection.
With a network cable, connect the Ethernet port of your board to the one of your computer. If your computer
already has a wired connection to the network, your instructor will provide you with a USB Ethernet adapter.
A new network interface should appear on your Linux system.

Network configuration on the target


Let’s configure networking in U-Boot:
• ipaddr: IP address of the board
• serverip: IP address of the PC host

© 2004-2024 Bootlin, CC BY-SA license 13


Embedded Linux System Development

=> setenv ipaddr 192.168.0.100


=> setenv serverip 192.168.0.1

Of course, make sure that this address belongs to a separate network segment from the one of the main
company network.
To make these settings permanent, save the environment:

=> saveenv

Network configuration on the PC host


To configure your network interface on the workstation side, we need to know the name of the network
interface connected to your board.
Find the name of this interface by typing:

=> ip a

The network interface name is likely to be enxxx3 . If you have a pluggable Ethernet device, it’s easy to
identify as it’s the one that shows up after pluging in the device.
Then, instead of configuring the host IP address from NetworkManager’s graphical interface, let’s do it
through its command line interface, which is so much easier to use:

$ nmcli con add type ethernet ifname en... ip4 192.168.0.1/24

Setting up the TFTP server


Let’s install a TFTP server on your development workstation:
sudo apt install tftpd-hpa
You can then test the TFTP connection. First, put a small text file in the directory exported through TFTP
on your development workstation. Then, from U-Boot, do:

=> tftp 0xc2000000 textfile.txt

The tftp command should have downloaded the textfile.txt file from your development workstation into
the board’s memory at location 0xc20000004 .
You can verify that the download was successful by dumping the contents of the memory:

=> md 0xc2000000

We will see in the next labs how to use U-Boot to download, flash and boot a kernel.

Known issues on stmp32mp157d-dk1


If your board doesn’t boot any more, even after making no changes to the micro SD card, even after unplugging
the USB-C power cable, you may need to unplug both the USB micro-B (used for serial) and USB-C power
cables to get the board to boot again. It’s probably because the USB micro-B is also a power source.
3 Following the Predictable Network Interface Names convention: https://www.freedesktop.org/wiki/Software/systemd/

PredictableNetworkInterfaceNames/
4 This location is part of the board DRAM. If you want to check where this value comes from, you can check the SoC

datasheet at https://www.st.com/resource/en/reference_manual/dm00327659.pdf. It’s a big document (more than 4,000 pages).


In this document, look for Memory organization and you will find the SoC memory map. You will see that the address range for
the memory controller (DDRC) starts at the address we are looking for. You can also try with other values in the RAM address
range.

14 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Rescue binaries
If you have trouble generating binaries that work properly, or later make a mistake that causes you to lose
your bootloader binaries, you will find working versions under data/ in the current lab directory.

© 2004-2024 Bootlin, CC BY-SA license 15


Embedded Linux System Development

Fetching Linux kernel sources


Objective: learn how to fetch the Linux kernel sources from git, from both the
master and stable branches.

After this lab, you will be able to:

• Get the kernel sources from git, using the official Linux source tree.

• Fetch the sources for the stable Linux releases, by declaring a remote tree and getting stable branches
from it.

Setup
Create the $HOME/embedded-linux-labs/kernel directory and go into it.

Since the Linux kernel git repository is huge, our goal here is to start downloading it right now, before starting
the lectures about the Linux kernel.

Cloning the mainline Linux tree


To begin working with the Linux kernel sources, we need to clone its reference git tree, the one managed by
Linus Torvalds.

However, this requires downloading more than 2.7 GB of data. If you are running this command from home,
or if you have very fast access to the Internet at work (and if you are not 256 participants in the training
room), you can do it directly by connecting to https://git.kernel.org:

git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux


cd linux

If Internet access is not fast enough and if multiple people have to share it, your instructor will give you a
USB flash drive with a tar.gz archive of a recently cloned Linux source tree.

You will just have to extract this archive in the current directory, and then pull the most recent changes over
the network:

tar xf linux-git.tar.gz
cd linux
git checkout master
git pull

Of course, if you directly ran git clone, you won’t have to run git pull, as git clone already retrieved the
latest changes. You may need to run git pull in the future though, if you want to update a newer Linux
version.

Accessing stable releases


The Linux kernel repository from Linus Torvalds contains all the main releases of Linux, but not the stable
versions: they are maintained by a separate team, and hosted in a separate repository.

We will add this separate repository as another remote to be able to use the stable releases:
git remote add stable https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux
git fetch stable

16 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

As this still represents many git objects to download (450 MiB when 5.9 was the latest version), if you are
using an already downloaded git tree, your instructor will probably have fetched the stable branch ahead of
time for you too. You can check by running:
git branch -a
We will choose a particular stable version in the next labs.
Now, let’s continue the lectures. This will leave time for the commands that you typed to complete their
execution (if needed).

© 2004-2024 Bootlin, CC BY-SA license 17


Embedded Linux System Development

Kernel - Cross-compiling
Objective: Learn how to cross-compile a kernel for an ARM target platform.

After this lab, you will be able to:


• Checkout a stable version of the Linux kernel
• Set up a cross-compiling environment
• Cross compile the kernel for the STM32MP157D-DK1 Discovery kit
• Use U-Boot to download the kernel
• Check that the kernel you compiled starts the system

Setup
Stay in the $HOME/embedded-linux-labs/kernel directory.

Choose a particular stable version of Linux


We will use linux-6.1.x, which corresponds to an LTS release, and which this lab was tested with.
First, let’s get the list of branches we have available:
cd linux
git branch -a
As we will do our labs with the Linux 6.1, the remote branch we are interested in is
remotes/stable/linux-6.1.y.
First, execute the following command to check which version you currently have:
make kernelversion
You can also open the Makefile and look at the beginning of it to check this information.
Now, let’s create a local branch starting from that remote branch:

git checkout stable/linux-6.1.y

Check the version again using the make kernelversion command to make sure you now have a 6.1.x version.

Cross-compiling environment setup


To cross-compile Linux, you need to have a cross-compiling toolchain. We will use the cross-compiling
toolchain that we previously produced, so we just need to make it available in the PATH:

$ export PATH=$HOME/x-tools/arm-training-linux-musleabihf/bin:$PATH

Also, don’t forget to either:


• Define the value of the ARCH and CROSS_COMPILE variables in your environment (using export)
• Or specify them on the command line at every invocation of make, i.e.: make ARCH=... CROSS_COMPILE=
... <target>

18 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Linux kernel configuration


By running make help, look for the proper Makefile target to configure the kernel for your processor.
The standard configuration for this kernel is actually multi_v7_defconfig, but this will generate a pretty
big kernel with support for many other SoCs. However, we can reduce it to compile faster and get a small
kernel.
So, apply this configuration, and then run make menuconfig.
• Disable CONFIG_GCC_PLUGINS if it is set. This will skip building special gcc plugins, which would require
extra dependencies for the build.
• In the System Type menu, remove support for all the SoCs except the STM32MP157 ones. Don’t forget
to disable the TI ones too which are in a submenu.
• Disable CONFIG_DRM, which will skip support for many display controller and GPU drivers.
Please note that this will definitely not build the smallest and most optimized kernel for STM32MP1: multi_
v7_defconfig enables plenty of features and drivers that will not be useful on our particular board.

Cross compiling
You’re now ready to cross-compile your kernel. Simply run:

$ make
and wait a while for the kernel to compile. Don’t forget to use make -j<n> if you have multiple cores on your
machine!
Look at the kernel build output to see which file contains the kernel image.
Also look in the Device Tree Source directory to see which .dtb files got compiled. Find which .dtb file
corresponds to your board.

Load and boot the kernel using U-Boot


As we are going to boot the Linux kernel from U-Boot, we need to set the bootargs environment corresponding
to the Linux kernel command line:
=> setenv bootargs console=ttySTM0,115200
=> saveenv

We will use TFTP to load the kernel image on the board:


• On your workstation, copy the zImage and DTB (stm32mp157a-dk1.dtb) to the directory exposed by
the TFTP server.
• On the target (in the U-Boot prompt), load zImage from TFTP into RAM:

=> tftp 0xc2000000 zImage

• Now, also load the DTB file into RAM:

=> tftp 0xc4000000 stm32mp157a-dk1.dtb

• Boot the kernel with its device tree:


=> bootz 0xc2000000 - 0xc4000000

You should see Linux boot and finally panicking. This is expected: we haven’t provided a working root
filesystem for our device yet.

© 2004-2024 Bootlin, CC BY-SA license 19


Embedded Linux System Development

You can now automate all this every time the board is booted or reset. Reset the board, and customize
bootcmd:
=> setenv bootcmd 'tftp 0xc2000000 zImage; tftp 0xc4000000 stm32mp157a-dk1.dtb; bootz
0xc2000000 - 0xc4000000'
=> saveenv

Restart the board to make sure that booting the kernel is now automated.

20 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Tiny embedded system with BusyBox


Objective: making a tiny yet full featured embedded system

After this lab, you will:


• be able to configure and build a Linux kernel that boots on a directory on your workstation, shared
through the network by NFS.
• be able to create and configure a minimalistic root filesystem from scratch (ex nihilo, out of nothing,
entirely hand made...) for your target board.
• understand how small and simple an embedded Linux system can be.
• be able to install BusyBox on this filesystem.
• be able to create a simple startup script based on /sbin/init.
• be able to set up a simple web interface for the target.

Lab implementation
While (s)he develops a root filesystem for a device, a developer needs to make frequent changes to the
filesystem contents, like modifying scripts or adding newly compiled programs.
It isn’t practical at all to reflash the root filesystem on the target every time a change is made. Fortunately,
it is possible to set up networking between the development workstation and the target. Then, workstation
files can be accessed by the target through the network, using NFS.
Unless you test a boot sequence, you no longer need to reboot the target to test the impact of script or
application updates.

Setup
Go to the $HOME/embedded-linux-labs/tinysystem/ directory.

Kernel configuration
We will re-use the kernel sources from our previous lab, in $HOME/embedded-linux-labs/kernel/.
In the kernel configuration built in the previous lab, verify that you have all options needed for booting the
system using a root filesystem mounted over NFS. Also check that CONFIG_DEVTMPFS_MOUNT is enabled (we
will explain it later in this lab). If necessary, rebuild your kernel.

© 2004-2024 Bootlin, CC BY-SA license 21


Embedded Linux System Development

Setting up the NFS server


Create a nfsroot directory in the current lab directory. This nfsroot directory will be used to store the
contents of our new root filesystem.

Install the NFS server by installing the nfs-kernel-server package if you don’t have it yet. Once installed,
edit the /etc/exports file as root to add the following line, assuming that the IP address of your board will
be 192.168.0.100:

/home/<user>/embedded-linux-labs/tinysystem/nfsroot 192.168.0.100(rw,no_root_squash,
no_subtree_check)

Of course, replace <user> by your actual user name.

Make sure that the path and the options are on the same line. Also make sure that there is no space between
the IP address and the NFS options, otherwise default options will be used for this IP address, causing your
root filesystem to be read-only.

Then, make the NFS server use the new configuration:

$ sudo exportfs -r

Booting the system


First, boot the board to the U-Boot prompt. Before booting the kernel, we need to tell it that the root
filesystem should be mounted over NFS, by setting some kernel parameters.

So add settings to the bootargs environment variable, in just 1 line:

=> setenv bootargs ${bootargs} root=/dev/nfs ip=192.168.0.100


nfsroot=192.168.0.1:/home/<user>/embedded-linux-labs/tinysystem/nfsroot,nfsvers=3,tcp rw

Once again, replace <user> by your actual user name.

Of course, you need to adapt the IP addresses to your exact network setup. Save the environment variables
(with saveenv).

Now, boot your system. The kernel should be able to mount the root filesystem over NFS:

VFS: Mounted root (nfs filesystem) on device 0:16.

If the kernel fails to mount the NFS filesystem, look carefully at the error messages in the console. If this
doesn’t give any clue, you can also have a look at the NFS server logs in /var/log/syslog.

However, at this stage, the kernel should stop because of the below issue:

[ 7.476715] devtmpfs: error mounting -2

This happens because the kernel is trying to mount the devtmpfs filesystem in /dev/ in the root filesystem.
This virtual filesystem contains device files (such as ttyS0) for all the devices known to the kernel, and with
CONFIG_DEVTMPFS_MOUNT, our kernel tries to automatically mount devtmpfs on /dev.

To address this, just create a dev directory under nfsroot and reboot.

Now, the kernel should complain for the last time, saying that it can’t find an init application:

Kernel panic - not syncing: No working init found. Try passing init= option to
kernel. See Linux Documentation/admin-guide/init.rst for guidance.

Obviously, our root filesystem being mostly empty, there isn’t such an application yet. In the next paragraph,
you will add BusyBox to your root filesystem and finally make it usable.

22 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Root filesystem with BusyBox


Download the sources of the latest BusyBox 1.36.x release:

git clone https://git.busybox.net/busybox


cd busybox/
git checkout 1_36_stable

Now, configure BusyBox with the configuration file provided in the data/ directory (remember that the
BusyBox configuration file is .config in the BusyBox sources).

Then, you can use $ make menuconfig to further customize the BusyBox configuration. At least, keep the
setting that builds a static BusyBox. Compiling BusyBox statically in the first place makes it easy to set
up the system, because there are no dependencies on libraries. Later on, we will set up shared libraries and
recompile BusyBox.
Build BusyBox using the toolchain that you used to build the kernel.
Going back to the BusyBox configuration interface, check the installation directory for BusyBox5 . Set it to
the path to your nfsroot directory.
Now run $ make install to install BusyBox in this directory.
Try to boot your new system on the board. You should now reach a command line prompt, allowing you to
execute the commands of your choice.

Virtual filesystems
Run the $ ps command. You can see that it complains that the /proc directory does not exist. The ps
command and other process-related commands use the proc virtual filesystem to get their information from
the kernel.
From the Linux command line in the target, create the proc, sys and etc directories in your root filesystem.
Now mount the proc virtual filesystem. Now that /proc is available, test again the ps command.
Note that you can also now halt your target in a clean way with the halt command, thanks to proc being
mounted6 .

System configuration and startup


The first user space program that gets executed by the kernel is /sbin/init and its configuration file is
/etc/inittab.
In the BusyBox sources, read details about /etc/inittab in the examples/inittab file.
Then, create a /etc/inittab file and a /etc/init.d/rcS startup script declared in /etc/inittab. In this
startup script, mount the /proc and /sys filesystems.
Any issue after doing this?

Starting the shell in a proper terminal


Before the shell prompt, you probably noticed the below warning message:
/bin/sh: can't access tty; job control turned off
This happens because the shell specified in the /etc/inittab file in started by default in /dev/console:
::askfirst:/bin/sh
5 You will find this setting in Settings -> Install Options -> Destination path for 'make install'.
6 halt can find the list of mounted filesystems in /proc/mounts, and unmount each of them in a clean way before shutting
down.

© 2004-2024 Bootlin, CC BY-SA license 23


Embedded Linux System Development

When nothing is specified before the leading ::, /dev/console is used. However, while this device is fine for
a simple shell, it is not elaborate enough to support things such as job control ([Ctrl][c] and [Ctrl][z]),
allowing to interrupt and suspend jobs.
So, to get rid of the warning message, we need init to run /bin/sh in a real terminal device:
ttySTM0::askfirst:/bin/sh
Reboot the system and the message will be gone!

Switching to shared libraries


Take the hello.c program supplied in the lab data directory. Cross-compile it for ARM, dynamically-linked
with the libraries7 , and run it on the target.
You will first encounter a very misleading not found error, which is not because the hello executable is not
found, but because something else was not found while trying to execute this executable.
You can find it by running file hello on the host:
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-musl-armhf.so.1, not stripped
So, what’s missing is the /lib/ld-musl-armhf.so.1 executable, which is the dynamic linker required to
execute any program compiled with shared libraries. Using the find command, look for this file in the
toolchain install directory, and copy it to the lib/ directory on the target.
Then, running the executable again and see that the loader executes and finds out which shared libraries are
missing.
In our case with the Musl C library, the dynamic linker also contains the C library, so the program should
execute fine, as no further shared libraries are required.
If you still get the same error message, work, just try again a few seconds later. Such a delay can be needed
because the NFS client can take a little time (at most 30-60 seconds) before seeing the changes made on the
NFS server.
Now that the small test program works, we are going to recompile BusyBox without the static compilation
option, so that BusyBox takes advantages of the shared libraries that are now present on the target.
Before doing that, measure the size of the busybox executable.
Then, build BusyBox with shared libraries, and install it again on the target filesystem. Make sure that the
system still boots and see how much smaller the busybox executable got.

Implement a web interface for your device


Replicate data/www/ to the /www directory in your target root filesystem.
Now, run the BusyBox http server from the target command line:

=> /usr/sbin/httpd -h /www/


It will automatically background itself.
If you use a proxy, configure your host browser so that it doesn’t go through the proxy to connect to the
target IP address, or simply disable proxy usage. Now, test that your web interface works well by opening
http://192.168.0.100/index.html on the host.
See how the dynamic pages are implemented. Very simple, isn’t it?
Finish by adding the command that starts the web server to your startup script, so that it is always started
on your target.
7 Invoke your cross-compiler in the same way you did during the toolchain lab

24 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Going further
If you have time before the others complete their labs...

Initramfs booting
Configure your kernel to include the contents of the nfsroot directory as an initramfs.
Before doing this, you will need to create an init link in the toplevel directory to sbin/init, because the
kernel will try to execute /init.
You will also need to mount devtmpfs from the rcS script, it cannot be mounted automatically by the kernel
when you’re booting from an initramfs.
Note: you won’t need to modify your root= setting in the kernel command line. It will just be ignored if you
have an initramfs.
When this works, go back to booting the system through NFS. This will be much more convenient in the
next labs.

© 2004-2024 Bootlin, CC BY-SA license 25


Embedded Linux System Development

Accessing Hardware Devices


Objective: learn how to access hardware devices and declare new ones.

Goals
Now that we have access to a command line shell thanks to a working root filesystem, we can now explore
existing devices and make new ones available. In particular, we will make changes to the Device Tree and
compile an out-of-tree Linux kernel module.

Setup
Go to the $HOME/embedded-linux-labs/hardware directory, which provides useful files for this lab.
However, we will go on booting the system through NFS, using the root filesystem built by the previous lab.

Exploring /dev
Start by exploring /dev on your target system. Here are a few noteworthy device files that you will see:
• Terminal devices: devices starting with tty. Terminals are user interfaces taking text as input and
producing text as output, and are typically used by interactive shells. In particular, you will find
console which matches the device specified through console= in the kernel command line. You will
also find the ttySTM0 device file.
• Pseudo-terminal devices: devices starting with pty, used when you connect through SSH for example.
Those are virtual devices, but there are so many in /dev that we wanted to give a description here.
• MMC device(s) and partitions: devices starting with mmcblk. You should here recognize the MMC
device(s) on your system and the associated partitions.
• If you have a real board (not QEMU) and a USB stick, you could plug it in and if your kernel was built
with USB host and mass storage support, you should see a new sda device appear, together with the
sda<n> devices for its partitions.
Don’t hesitate to explore /dev on your workstation too and ask any questions to your instructor.

Exploring /sys
The next thing you can explore is the Sysfs filesystem.
A good place to start is /sys/class, which exposes devices classified by the kernel frameworks which manage
them.
For example, go to /sys/class/net, and you will see all the networking interfaces on your system, whether
they are internal, external or virtual ones.
Find which subdirectory corresponds to the network connection to your host system, and then check device
properties such as:
• speed: will show you whether this is a gigabit or hundred megabit interface.
• address: will show the device MAC address. No need to get it from a complex command!
• statistics/rx_bytes will show you how many bytes were received on this interface.
Don’t hesitate to look for further interesting properties by yourself!

26 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

You can also check whether /sys/class/thermal exists and is not empty on your system. That’s the thermal
framework, and it allows to access temperature measures from the thermal sensors on your system.

Next, you can now explore all the buses (virtual or physical) available on your system, by checking the
contents of /sys/bus.

In particular, go to /sys/bus/mmc/devices to see all the MMC devices on your system. Go inside the directory
for the first device and check several files (for example):

• serial: the serial number for your device.

• preferred_erase_size: the preferred erase block for your device. It’s recommended that partitions
start at multiples of this size.

• name: the product name for your device. You could display it in a user interface or log file, for example.

• date: apparently the manufacturing date for the device.

Don’t hesitate to spend more time exploring /sys on your system and asking questions to your instructor.

Driving GPIOs
At this stage, we can only explore GPIOs through the legacy interface in /sys/class/gpio, because the
libgpiod interface commands are provided through a dedicated project which we have to build separately, and
Busybox does not provide a re-implementation for the libgpiod tools. In a later lab, we will build libgpiod
tools which use the modern /dev/gpiochipX interface.

The first thing to do is to enable this legacy interface by enabling CONFIG_GPIO_SYSFS in the kernel configu-
ration. Also make sure Debugfs is enabled (CONFIG_DEBUG_FS and CONFIG_DEBUG_FS_ALLOW_ALL).

After rebooting the new kernel, the first thing to do is to mount the Debugfs filesystem:

# mount -t debugfs debugfs /sys/kernel/debug/

Then, you can check information about available GPIOs banks and which GPIOs are already in use:

# cat /sys/kernel/debug/gpio

We are now going to use one of the Arduino Uno header pins at the back of the board, which is not already
used by another device.

Take one of the M-M breadboard wires provided by your instructor and:

• Connect one end to pin D2 of connector CN14

• Connect the other end to pin 7 (GND) of connector CN16

© 2004-2024 Bootlin, CC BY-SA license 27


Embedded Linux System Development

If you check the Pinout of the Arduino™ connectors table in the board documentation 8 , you will see that
the ARD_D2 pin on the board is connected to the PE1 STM32 pin. PE1 is actually a GPIO pin on GPIO
bank E, and is configured as a GPIO by default (no need to change pin muxing to use this pin as a GPIO).
If you get back to the contents of /sys/kernel/debug/gpio, you’ll find that GPIO bank E corresponds to
gpiochip4 and to GPIO numbers 64 to 79. Hence, PE1, the second pin on this bank corresponds to GPIO
number 65.
We now have everything we need to drive this GPIO using the legacy interface. First, let’s enable it:

# cd /sys/class/gpio
# echo 65 > export

If indeed the pin is still available, this should create a new gpio65 file should appear in /sys/class/gpio.
We can now configure this pin as input:

# echo in > gpio65/direction

And check its value:


# cat gpio65/value
0

The value should be 0 as the pin is connected to a ground level.


Now, let’s connect our GPIO pin to pin 2 (IOREF 3V3) of connector CN16. That’s the same connector as
before, just the second pin from the top (in the board picture above), instead of the seventh one.
Let’s check the value again:

# cat gpio65/value
1

The value is 1 because our pin is connected to a 3.3V level now.


You could use this GPIO to add a button switch to your board, for example.
8 https://www.st.com/resource/en/user_manual/dm00591354-discovery-kits-with-stm32mp157-mpus-stmicroelectronics.pdf

28 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Note that you could also configure the pin as output and set its value through the value file. This way, you
could add an external LED to your board, for example.
Before moving on to the next section, you can also check /sys/kernel/debug/gpio again, and see that
gpio-65 is now in use, through the sysfs interface, and is configured as an input pin.
When you’re done, you can see your GPIO free:

# echo 65 > unexport

Driving LEDs
First, make sure your kernel is compiled with CONFIG_LEDS_CLASS=y, CONFIG_LEDS_GPIO=y and CONFIG_LEDS_
TRIGGER_TIMER=y.
Then, go to /sys/class/leds to see all the LEDs that you are allowed to control.
Let’s control the LED which is called heartbeat.
Go into the directory for this LED, and check its trigger (what routine is used to drive its value):

# cat trigger

As you can see, there are many triggers to choose from, the current being heartbeat, corresponding to the
CPU activity.
You can disable all triggers by:

# echo none > trigger

And then directly control the LED:

# echo 1 > brightness


# echo 0 > brightness

You could also use the timer trigger to light the LED with specified time on and time off:

# echo timer > trigger


# echo 10 > delay_on
# echo 200 > delay_off

Managing the I2C buses and devices


Enabling an I2C bus
The next thing we want to do is connect an Nunchuk joystick to an I2C bus on our board. The I2C bus is
very frequently used to connect all sorts of external devices. That’s why we’re covering it here.
The first task is to find a suitable bus. If you study the same document about the board, you will find that
only I2C5 is conveniently available through the Arduino headers. Let’s try to use this one!
First, let’s see which I2C buses are already enabled:

# i2cdetect -l
i2c-1 i2c STM32F7 I2C(0x5c002000) I2C adapter
i2c-0 i2c STM32F7 I2C(0x40012000) I2C adapter

i2c-0 is the I2C controller with registers at 0x40012000, which is I2C1 in the STM32MP1 nomenclature.
i2c-1 is the I2C controller with registers at 0x5c002000, which is I2C4 in the STM32MP1 nomenclature.
Refer to the STM32MP1 memory map in the datasheet for details. Pay attention to the numbering difference:

© 2004-2024 Bootlin, CC BY-SA license 29


Embedded Linux System Development

i2c-0, i2c-1 is the Linux numbering, based on the registration order of enabled I2C busses. Here, because
only I2C1 and I2C4 are enabled, they are called i2c-0 and i2c-1.
Using the datasheet for the SoC 9 , we can find what is the base address of the registers for the I2C5 controller:
it is 0x40015000.

Customizing the Device Tree


To enable I2C5 on our system, we need to assign set status = "okay"; in the corresponding Device Tree
node.
Fortunately, I2C5 is already defined in the one of the DTS includes used by the Device Tree for our board.
In our case, that’s in arch/arm/boot/dts/stm32mp151.dtsi. Look by yourself in this file, and you will find
its definition, but with status = "disabled";. This means that this I2C controller is not enabled yet, and
it’s up to boards using it to do so.
We could modify the arch/arm/boot/dts/stm32mp157a-dk1.dts file for our board, but that’s not a very good
idea as this file is maintained by the kernel developers. The changes that you make could collide with future
changes made by the maintainers for this file.
A more futureproof idea is to create a new Device Tree file which includes the standard one, and adds custom
definitions. So, create a new arch/arm/boot/dts/stm32mp157a-dk1-custom.dts file containing:
/dts-v1/;
#include "stm32mp157a-dk1.dts"

&i2c5 {
status = "okay";
/delete-property/ pinctrl-names;
};
As you can see, it’s also possible to include dts files, and not only dtsi ones.
Why the /delete-property/ statement? That’s because we want to see what happens when a device doesn’t
have associated pin definitions yet.
A device like an I2C controller node is typically declared in the DTSI files for the SoC, without pin settings
as these are board specific. Pin definitions are then usually defined at board level.
In our case, we don’t see such definitions, but they are actually found in the arch/arm/boot/dts/stm32mp15xx-
dkx.dtsi file, shared between multiple stm32mp15 DK boards, which is included by the toplevel Device Tree
for our board.
Modify the arch/arm/boot/dts/Makefile file to add your custom Device Tree, and then have it compiled
(make dtbs).
Reboot your board with the update.
Back to the running system, we can now see that there is one more I2C bus. We can also recognize the I2C5
address (0x40015000) though it’s now associated to the i2c-1 device name, which already existed previously,
but mapped to a different physical device:

# i2cdetect -l
i2c-1 i2c STM32F7 I2C(0x40015000) I2C adapter
i2c-2 i2c STM32F7 I2C(0x5c002000) I2C adapter
i2c-0 i2c STM32F7 I2C(0x40012000) I2C adapter

Now, let’s use i2cdetect’s capability to probe a bus for devices. Let’s start by the bus associated to i2c-2:
9 https://www.st.com/resource/en/reference_manual/dm00327659-stm32mp157-advanced-arm-based-32-bit-mpus-

stmicroelectronics.pdf

30 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

# i2cdetect -r 2
i2cdetect: WARNING! This program can confuse your I2C bus
Continue? [y/N] y
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- 28 -- -- -- -- -- -- --
30: -- -- -- UU -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

We can see two devices here:

• One at address 0x33, indicated by UU, which means that there is a kernel driver actively driving this
device.

• Another one at address 0x28. We just know that it’s currently not bound to a kernel driver.

Now try to probe I2C5 through i2cdetect -r 1.

You will see that the command will fail to connect to the bus. That’s because the corresponding signals are
not exposed yet to the outside connectors through pin muxing.

So, get back to your Device Tree and remove the /delete-property/ line. Recompile your Device Tree and
reboot.

You should now be able to probe your bus:

# i2cdetect -r 1
i2cdetect: WARNING! This program can confuse your I2C bus
Continue? [y/N] y
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

No device is detected yet, because this bus is just used for external devices. It’s time to add one though.

Adding and enabling an I2C device


Let’s connect the Nunchuk provided by your instructor to the I2C5 bus on the board, using breadboard
wires:

© 2004-2024 Bootlin, CC BY-SA license 31


Embedded Linux System Development

SCL
PWR

GND SDA

Nunchuk i2c pinout


(UEXT connector from Olimex, front view)

• Connect the Nunchuk PWR pin to pin 4 (3V3) of connector CN16


• Connect the Nunchuk GND pin to pin 6 (GND) of connector CN16
• Connect the Nunchuk SCL pin to pin 10 (D15 - I2C5_SCL) of connector CN13
• Connect the Nunchuk SDA pin to pin 9 (D14 - I2C5_SDA) of connector CN13
If you didn’t do any mistake, your new device should be detected at address 0x52:

# i2cdetect -r 1
i2cdetect: WARNING! This program can confuse your I2C bus
Continue? [y/N] y
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

32 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- 52 -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
We will later compile an out-of-tree kernel module to support this device.

Plugging a USB audio headset


In the next labs, we are going to play audio using a USB audio headset. Let’s see whether our kernel supports
such hardware by plugging the headset provided by your instructor.
Before plugging the device, look at the output of lsusb:

# lsusb
Bus 002 Device 002: ID 0424:2514
Bus 001 Device 001: ID 1d6b:0002
Bus 002 Device 001: ID 1d6b:0002

Now, when you plug the USB headset, a number of messages should appear on the console, and running
lsusb again should show an additional device:

# lsusb
Bus 002 Device 004: ID 0d8c:0014
Bus 002 Device 002: ID 0424:2514
Bus 001 Device 001: ID 1d6b:0002
Bus 002 Device 001: ID 1d6b:0002

The device of vendor ID 0d8c and product ID 0014 has appeared. Of course, this depends on the actual USB
audio device that you used.
The device also appears in /sys/bus/usb/devices/, in a directory whose name depends on the topology of
the USB bus. When the device is plugged in the kernel messages show:

usb 1-1.3: new full-speed USB device number 3 using ehci-platform

So if we go in /sys/bus/usb/devices/1-1.3, we get the sysfs representation of this USB device:

# cd /sys/bus/usb/devices/1-1.3
# cat idVendor
0d8c
# cat idProduct
0014
# cat manufacturer
C-Media Electronics Inc.
# cat product
USB Audio Device

However, while the USB device is detected, we currently do not have any driver for this device, so no actual
sound card is detected.

Enabling, installing and using in-tree kernel modules


Go back to the kernel source directory.
The Linux kernel has a generic driver supporting all USB audio devices supporting the standard USB audio
class. This driver can be enabled using the CONFIG_SND_USB_AUDIO configuration option. Look for this

© 2004-2024 Bootlin, CC BY-SA license 33


Embedded Linux System Development

parameter in the kernel configuration, and you should find that it is already enabled as a module.
So, instead of compiling the corresponding driver as a built-in, that’s a good opportunity to practice with
kernel modules.
So, compile your modules:

make modules

Then, following details given in the lectures, install the modules in our NFS root filesystem ($HOME/embedded-
linux-labs/tinysystem/nfsroot).
Also make sure to update the kernel image (make zImage), and reboot the board. Indeed, due to the
changes we have made to the kernel source code, the kernel version is now 6.1.<x>-dirty, the dirty keyword
indicating that the Git working tree has uncommitted changes. The modules are therefore installed in
/lib/modules/6.1.<x>-dirty/, and the version of the running Linux kernel must match this.
After rebooting, try to load the module that we need:

modprobe snd-usb-audio

By running lsmod, see all the module dependencies that were loaded too.
You can also see that a new USB device driver in /sys/bus/usb/drivers/snd-usb-audio. This directory
shows which USB devices are bound to this driver.
You can check that /proc/asound now exists (thanks to loading modules for the ALSA, the Linux sound
subsystem), and that one sound card is available:

# cat /proc/asound/cards
0 [Device ]: USB-Audio - USB Audio Device
C-Media Electronics Inc. USB Audio Device at usb-5800d000.usb-1.1, full \
speed

Check also the /dev/snd directory, which should now contain some character device files. These will be used
by the user-space libraries and applications to access the audio devices.
Modify your startup scripts so that the snd-usb-audio module is always loaded at startup.
We cannot test the sound card yet, as we will need to build some software first. Be patient, this is coming
soon.

Compiling and installing an out-of-tree kernel module


The next device we want to support is the I2C Nunchuk. There is a driver in the kernel to support it when
connected to a Wiimote controller, but there is no such driver to support it as an I2C device.
Fortunately, one is provided in $HOME/embedded-linux-labs/hardware/data/nunchuk/nunchuk.c. You can
check Bootlin’s Linux kernel and driver development course to learn how to implement all sorts of device
drivers for Linux.
Go to this directory, and compile the out-of-tree module as follows:

make -C $HOME/embedded-linux-labs/kernel/linux M=$PWD

Here are a few explanations:


• The -C option lets make know which Makefile to use, here the toplevel Makefile in the kernel sources.
• M=$PWD tells the kernel Makefile to build external module(s) from the file(s) in the current directory.
Now, you can install the compiled module in the NFS root filesystem by passing the modules_install target
and specifying the target directory through the INSTALL_MOD_PATH variable:

34 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

make -C $HOME/embedded-linux-labs/kernel/linux \
M=$PWD \
INSTALL_MOD_PATH=$HOME/embedded-linux-labs/tinysystem/nfsroot \
modules_install

You can see that this installs out-of-tree kernel modules under lib/modules/<version>/extra/.
Back on the target, you can now check that your custom module can be loaded:

# modprobe nunchuk
[ 4317.737978] nunchuk: loading out-of-tree module taints kernel.

See kbuild/modules in kernel documentation for details about building out-of-tree kernel modules.
However, run i2cdetect -r 1 again. You will see that the Nunchuk is still detected, but still not driven by
the kernel. Otherwise, it would be signaled by the UU character. You may also look at the nunchuk.c file and
notice a Nunchuk device probed successfully message that you didn’t see when loading the module.
That’s because the Linux kernel doesn’t know about the Nunchuk device yet, even though the driver for this
kind of devices is already loaded. Our device also has to be described in the Device Tree.
You can confirm this by having a look at the contents of the /sys/bus/i2c directory. It contains two
subdirectories: devices and drivers.
In drivers, there should be a nunchuk subdirectory, but no symbolic link to a device yet. In devices you
should see some devices, but not the Nunchuk one yet.

Declaring an I2C device


To allow the kernel to manage our Nunchuk device, let’s declare the device in the custom Device Tree for
our board. The declaration of the I2C5 bus will then look as follows:
&i2c5 {
status = "okay";
clock-frequency = <100000>;

nunchuk: joystick@52 {
compatible = "nintendo,nunchuk";
reg = <0x52>;
};
};
Here are a few notes:
• The clock-frequency property is used to configure the bus to operate at 100 KHz. This is supposed
to be required for the Nunchuk.
• The Nunchuk device is added through a child node in the I2C controller node.
• For the kernel to probe and drive our device, it’s required that the compatible string matches one of
the compatible strings supported by the driver.
• The reg property is the address of the device on the I2C bus. If it doesn’t match, the driver will probe
the device but won’t be able to communicate with it.
Recompile your Device Tree and reboot your kernel with the new binary.
You can now load your module again, and this time, you should see that the Nunchuk driver probed the
Nunchuk device:
# modprobe nunchuk

© 2004-2024 Bootlin, CC BY-SA license 35


Embedded Linux System Development

[ 66.680455] nunchuk: loading out-of-tree module taints kernel.


[ 66.687645] input: Wii Nunchuk as /devices/platform/soc/40015000.i2c/i2c-1/1-0052/input/\
input3
[ 66.695421] Nunchuk device probed successfully
List the contents of /sys/bus/i2c/drivers/nunchuk once again. You should now see a symbolic link corre-
sponding to our new device.
Also list /sys/bus/i2c/devices/ again. You should now see the Nunchuk device, which can be recognized
through its 0052 address. Follow the link and you should see a symbolic link back to the Nunchuk driver!
We are not ready to use this input device yet, but at least we can test that we get bytes when buttons or
the joypad are used. In the below command, use the same number as in the message you got in the console
(event3 for input3 for example):

# cat /dev/input/event3 | od -x

Caution: using od directly on input event files should work but is currently broken with the Musl library.
We are investigating this issue.
We will use the Nunchuk to control audio playback in an upcoming lab.

Setting the board’s model name


Modify the custom Device Tree file one last time to override the model name for your system. Set the model
property to STM32MP1 media player. Don’t hesitate to ask your instructor if you’re not sure how.
Recompile the device tree, and reboot the board with it. You should see the new model name in two different
places:
• In the first kernel messages on the serial console.
• In /sys/firmware/devicetree/base/model. This can be handy for a distribution to identify the de-
vice it’s running on. By the way, you can explore /sys/firmware/devicetree and find that every
subdirectory corresponds to a DT node, and every file corresponds to a DT property.

Committing kernel tree changes


Now that our changes to the kernel sources are over, create a branch for your changes and create a patch for
them. Please don’t skip this step as we need it for the next labs.
First, if not done yet, you should set your identity and e-mail address in git:

git config --global user.email "[email protected]"


git config --global user.name "Linus Torvalds"

This is necessary to create a commit with the git commit -s command, as required by the Linux kernel
contribution guidelines.
Let’s create the branch and the patch now:

git checkout -b bootlin-labs


git add arch/arm/boot/dts/stm32mp157a-dk1-custom.dts
git commit -as -m "Custom DTS for Bootlin lab"

We can now create the patch:


git format-patch stable/linux-6.1.y
This should generate a 0001-Custom-DTS-for-Bootlin-lab.patch file.
Creating the branch will impact the versions of the kernel and the modules. Compile your kernel and install
your modules again (not necessary for the Nunchuk one for the moment) and see the version changes through

36 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

the new base directory for modules.


To save space for the next lab, remove the old directory under lib/modules containing the ”dirty” modules.
Don’t forget to update the kernel your board boots.
That’s all for now!

© 2004-2024 Bootlin, CC BY-SA license 37


Embedded Linux System Development

Filesystems - Block file systems


Objective: configure and boot an embedded Linux system relying on block
storage

After this lab, you will be able to:


• Produce file system images.
• Configure the kernel to use these file systems
• Use the tmpfs file system to store temporary files
• Load the kernel and DTB from a FAT partition

Goals
After doing the A tiny embedded system lab, we are going to copy the filesystem contents to the SD card.
The storage will be split into several partitions, and your board will boot on an root filesystem on this SD
card, without using NFS anymore.

Setup
Throughout this lab, we will continue to use the root filesystem we have created in the $HOME/embedded-
linux-labs/tinysystem/nfsroot directory, which we will progressively adapt to use block filesystems.

Filesystem support in the kernel


Recompile your kernel with support for SquashFS and ext410 .
Update your kernel image in the boot partition.
Boot your board with this new kernel and on the NFS filesystem you used in this previous lab.
Now, check the contents of /proc/filesystems. You should see that ext4 and SquashFS are now supported.

Add partitions to the SD card


Plug the SD card in your workstation.
Using parted /dev/mmcblk0, add two partitions, starting from the beginning of the remaining space, with
the following properties:
• One partition, for the root filesystem, 8 MB big:
mkpart rootfs 131072s 147455s
• One partition, that fills the rest of the SD card, that will be used for the data filesystem:
mkpart data 147456s 100%
Use quit when you are done.

Data partition on the SD card


Using the mkfs.ext4 create a journaled file system on the sixth partition of the SD card:
10 Basic configuration options for these filesystems will be sufficient. No need for things like extended attributes.

38 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

$ sudo mkfs.ext4 -L data -E nodiscard /dev/mmcblk0p6

• -L assigns a volume name to the partition


• -E nodiscard disables bad block discarding. While this should be a useful option for cards with bad
blocks, skipping this step saves long minutes in SD cards.
Now, mount this new partition and move the contents of the /www/upload/files directory (in your target
root filesystem) into it. The goal is to use the data partition of the SD card as the storage for the uploaded
images.
Insert the SD card in your board and boot. You should see the partitions in /proc/partitions.
Mount this data partition on /www/upload/files.
Once this works, modify the startup scripts in your root filesystem to do it automatically at boot time.
Reboot your target system and with the mount command, check that /www/upload/files is now a mount
point for the last SD card partition. Also make sure that you can still upload new images, and that these
images are listed in the web interface.

Adding a tmpfs partition for log files


For the moment, the upload script was storing its log file in /www/upload/files/upload.log. To avoid seeing
this log file in the directory containing uploaded files, let’s store it in /var/log instead.
Add the /var/log/ directory to your root filesystem and modify the startup scripts to mount a tmpfs
filesystem on this directory. You can test your tmpfs mount command line on the system before adding it to
the startup script, in order to be sure that it works properly.
Modify the www/cgi-bin/upload.cfg configuration file to store the log file in /var/log/upload.log. You will
lose your log file each time you reboot your system, but that’s OK in our system. That’s what tmpfs is for:
temporary data that you don’t need to keep across system reboots.
Reboot your system and check that it works as expected.

Making a SquashFS image


We are going to store the root filesystem in a SquashFS filesystem in the fifth partition of the SD card.
In order to create SquashFS images on your host, you need to install the squashfs-tools package. Now
create a SquashFS image of your NFS root directory.
Finally, using the dd command, copy the file system image to the fifth partition of the SD card.

Booting on the SquashFS partition


In the U-boot shell, configure the kernel command line to use the fifth partition of the SD card as the root
file system. Also add the rootwait boot argument, to wait for the SD card to be properly initialized before
trying to mount the root filesystem. Since the SD cards are detected asynchronously by the kernel, the kernel
might try to mount the root filesystem too early without rootwait.
Check that your system still works.

Loading the kernel and DTB from the SD card


In order to let the kernel boot on the board autonomously, we can copy the kernel image and DTB in the
boot partition we created previously.
Insert the SD card in your PC, it will get auto-mounted. Copy the kernel and device tree to the boot
partition.
Insert the SD card back in the board and reset it. You should now be able to load the DTB and kernel image
from the SD card and boot with:

© 2004-2024 Bootlin, CC BY-SA license 39


Embedded Linux System Development

=> load mmc 0:4 0xc2000000 zImage


=> load mmc 0:4 0xc4000000 stm32mp157a-dk1-custom.dtb
=> bootz 0xc2000000 - 0xc4000000

You are now ready to modify bootcmd to boot the board from SD card. But first, save the settings for booting
from tftp:

=> setenv bootcmdtftp ${bootcmd}

This will be useful to switch back to tftp booting mode later in the labs.
Finally, using editenv bootcmd, adjust bootcmd so that the board starts using the kernel from the SD card.
Now, reset the board to check that it boots in the same way from the SD card.
Now, the whole system (bootloader, kernel and filesystems) is stored on the SD card. That’s very useful
for product demos, for example. You can switch demos by switching SD cards, and the system depends on
nothing else. In particular, no networking is necessary.

40 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Third party libraries and applications


Objective: Learn how to leverage existing libraries and applications: how to
configure, compile and install them

To illustrate how to use existing libraries and applications, we will extend the small root filesystem built in
the A tiny embedded system lab to add the ALSA libraries and tools to run basic sound support tests, and
the libgpiod library and executables to manage GPIOs. ALSA stands for Advanced Linux Sound Architecture,
and is the Linux audio subsystem.
We’ll see that manually re-using existing libraries is quite tedious, so that more automated procedures are
necessary to make it easier. However, learning how to perform these operations manually will significantly
help you when you face issues with more automated tools.

Figuring out library dependencies


We’re going to integrate the alsa-utils, libgpiod and ipcalc executables. In our case, the dependency chain for
alsa-utils is quite simple, it only depends on the alsa-lib library. libgpiod and ipcalc are standalone and don’t
have any dependency.

Of course, all these libraries rely on the C library, which is not mentioned here, because it is already part
of the root filesystem built in the A tiny embedded system lab. You might wonder how to figure out this
dependency tree by yourself. Basically, there are several ways, that can be combined:
• Read the library documentation, which often mentions the dependencies;
• Read the help message of the configure script (by running ./configure --help).
• By running the configure script, compiling and looking at the errors.
To configure, compile and install all the components of our system, we’re going to start from the bottom of
the tree with alsa-lib, then continue with alsa-utils. Then, we will also build libgpiod and ipcalc.

Preparation
For our cross-compilation work, we will need two separate spaces:
• A staging space in which we will directly install all the packages: non-stripped versions of the libraries,
headers, documentation and other files needed for the compilation. This staging space can be quite big,
but will not be used on our target, only for compiling libraries or applications;
• A target space, in which we will only copy the required files from the staging space: binaries and
libraries, after stripping, configuration files needed at runtime, etc. This target space will take a lot
less space than the staging space, and it will contain only the files that are really needed to make the
system work on the target.

© 2004-2024 Bootlin, CC BY-SA license 41


Embedded Linux System Development

To sum up, the staging space will contain everything that’s needed for compilation, while the target space
will contain only what’s needed for execution.
Create the $HOME/embedded-linux-labs/thirdparty directory, and inside, create two directories: staging
and target.
For the target, we need a basic system with BusyBox and initialization scripts. We will re-use the system
built in the A tiny embedded system lab, so copy this system in the target directory:

$ cp -a $HOME/embedded-linux-labs/tinysystem/nfsroot/* target/

Note that for this lab, a lot of typing will be required. To save time typing, we advise you to copy and paste
commands from the electronic version of these instructions.

Testing
Make sure the target/ directory is exported by your NFS server to your board by modifying /etc/exports
and restarting your NFS server.
Make your board boot from this new directory through NFS.

alsa-lib
alsa-lib is a library supposed to handle the interaction with the ALSA subsystem. It is available at https:
//alsa-project.org. Download version 1.2.9, and extract it in $HOME/embedded-linux-labs/thirdparty/.
Tip: if the website for any of the source packages that we need to download in the next sections is down, a
great mirror that you can use is http://sources.buildroot.net/.
Back to alsa-lib sources, look at the configure script and see that it has been generated by autoconf (the
header contains a sentence like Generated by GNU Autoconf 2.69). Most of the time, autoconf comes with
automake, that generates Makefiles from Makefile.am files. So alsa-lib uses a rather common build system.
Let’s try to configure and build it:

$ ./configure
$ make

If you look at the generated binaries, you’ll see that they are x86 ones because we compiled the sources with
gcc, the default compiler. This is obviously not what we want, so let’s clean-up the generated objects and
tell the configure script to use the ARM cross-compiler:

$ make clean
$ CC=arm-linux-gcc ./configure

Of course, the arm-linux-gcc cross-compiler must be in your PATH prior to running the configure script. The
CC environment variable is the classical name for specifying the compiler to use.
Quickly, you should get an error saying:
checking whether we are cross compiling... configure: error: in `/home/tux/embedded-linux-labs
/thirdparty/alsa-lib-1.2.9':
configure: error: cannot run C compiled programs.
If you meant to cross compile, use `--host'.
See `config.log' for more details
If you look at the config.log file, you can see that the configure script compiles a binary with the cross-
compiler and then tries to run it on the development workstation. This is a rather usual thing to do for a
configure script, and that’s why it tests so early that it’s actually doable, and bails out if not.
Obviously, it cannot work in our case, and the scripts exits. The job of the configure script is to test the
configuration of the system. To do so, it tries to compile and run a few sample applications to test if this

42 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

library is available, if this compiler option is supported, etc. But in our case, running the test examples is
definitely not possible.

We need to tell the configure script that we are cross-compiling, and this can be done using the --build
and --host options, as described in the help of the configure script:

System types:
--build=BUILD configure for building on BUILD [guessed]
--host=HOST cross-compile to build programs to run on HOST [BUILD]

The --build option allows to specify on which system the package is built, while the --host option allows
to specify on which system the package will run. By default, the value of the --build option is guessed
and the value of --host is the same as the value of the --build option. The value is guessed using the
./config.guess script, which on your system should return x86_64-pc-linux-gnu. See https://www.gnu.
org/software/autoconf/manual/html_node/Specifying-Names.html for more details on these options.

So, let’s override the value of the --host option:

$ ./configure --host=arm-linux

Note that CC is not required anymore. It is implied by --host.

The configure script should end properly now, and create a Makefile.

However, there is one subtle issue to handle. We need to tell alsa-lib to disable a feature called alsa topology.
alsa-lib will build fine but we will encounter some problems afterwards, during alsa-utils building. So you
should configure alsa-lib as follows:

$ ./configure --host=arm-linux --disable-topology

Run the make command, which should run just fine.

Look at the result of compiling in src/.libs: a set of object files and a set of libasound.so* files.

The libasound.so* files are a dynamic version of the library. The shared library itself is libasound.so.2.0.0,
it has been generated by the following command line:

$ arm-linux-gcc -shared conf.o confmisc.o input.o output.o async.o error.o dlmisc.o socket.o \
shmarea.o userfile.o names.o -lm -ldl -lpthread -lrt -Wl,-soname -Wl,libasound.so.2 -o \
libasound.so.2.0.0

And creates the symbolic links libasound.so and libasound.so.2.

$ ln -s libasound.so.2.0.0 libasound.so.2
$ ln -s libasound.so.2.0.0 libasound.so

These symlinks are needed for two different reasons:

• libasound.so is used at compile time when you want to compile an application that is dynamically
linked against the library. To do so, you pass the -lLIBNAME option to the compiler, which will look for
a file named lib<LIBNAME>.so. In our case, the compilation option is -lasound and the name of the
library file is libasound.so. So, the libasound.so symlink is needed at compile time;

• libasound.so.2 is needed because it is the SONAME of the library. SONAME stands for Shared Object
Name. It is the name of the library as it will be stored in applications linked against this library. It
means that at runtime, the dynamic loader will look for exactly this name when looking for the shared
library. So this symbolic link is needed at runtime.

To know what’s the SONAME of a library, you can use:

© 2004-2024 Bootlin, CC BY-SA license 43


Embedded Linux System Development

$ arm-linux-readelf -d libasound.so.2.0.0
and look at the (SONAME) line. You’ll also see that this library needs the C library, because of the (NEEDED)
line on libc.so.0.
The mechanism of SONAME allows to change the library without recompiling the applications linked with this
library. Let’s say that a security problem is found in the alsa-lib release that provides libasound 2.0.0, and
fixed in the next alsa-lib release, which will now provide libasound 2.0.1.
You can just recompile the library, install it on your target system, change the libasound.so.2 link so that
it points to libasound.so.2.0.1 and restart your applications. And it will work, because your applications
don’t look specifically for libasound.so.2.0.0 but for the SONAME libasound.so.2.
However, it also means that as a library developer, if you break the ABI of the library, you must change the
SONAME: change from libasound.so.2 to libasound.so.3.
Finally, the last step is to tell the configure script where the library is going to be installed. Most configure
scripts consider that the installation prefix is /usr/local/ (so that the library is installed in /usr/local/lib,
the headers in /usr/local/include, etc.). But in our system, we simply want the libraries to be installed in
the /usr prefix, so let’s tell the configure script about this:

$ ./configure --host=arm-linux --disable-topology --prefix=/usr


$ make

For this library, this option may not change anything to the resulting binaries, but for safety, it is always
recommended to make sure that the prefix matches where your library will be running on the target system.
Do not confuse the prefix (where the application or library will be running on the target system) from the
location where the application or library will be installed on your host while building the root filesystem.
For example, libasound will be installed in $HOME/embedded-linux-labs/thirdparty/target/usr/lib/ be-
cause this is the directory where we are building the root filesystem, but once our target system will be
running, it will see libasound in /usr/lib.
The prefix corresponds to the path in the target system and never on the host. So, one should never pass
a prefix like $HOME/embedded-linux-labs/thirdparty/target/usr, otherwise at runtime, the application or
library may look for files inside this directory on the target system, which obviously doesn’t exist! By default,
most build systems will install the application or library in the given prefix (/usr or /usr/local), but with
most build systems (including autotools), the installation prefix can be overridden, and be different from the
configuration prefix.
We now only have the installation process left to do.
First, let’s make the installation in the staging space:

$ make DESTDIR=$HOME/embedded-linux-labs/thirdparty/staging install

Now look at what has been installed by alsa-lib:


• Some configuration files in /usr/share/alsa
• The headers in /usr/include
• The shared library and its libtool (.la) file in /usr/lib
• A pkgconfig file in /usr/lib/pkgconfig. We’ll come back to these later
Finally, let’s install the library in the target space:
1. Create the target/usr/lib directory, it will contain the stripped version of the library
2. Copy the dynamic version of the library. Only libasound.so.2 and libasound.so.2.0.0 are needed,
since libasound.so.2 is the SONAME of the library and libasound.so.2.0.0 is the real binary:

44 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

• $ cp -a staging/usr/lib/libasound.so.2* target/usr/lib

3. Measure the size of the target/usr/lib/libasound.so.2.0.0 library before stripping.


4. Strip the library:
• $ arm-linux-strip target/usr/lib/libasound.so.2.0.0

5. Measure the size of the target/usr/lib/libasound.so.2.0.0 library library again after stripping. How
many unnecessary bytes were saved?
Then, we need to install the alsa-lib configuration files:

$ mkdir -p target/usr/share
$ cp -a staging/usr/share/alsa target/usr/share

Now, we need to adjust one small detail in one of the configuration files. Indeed, /usr/share/alsa/alsa.conf
assumes a UNIX group called audio exists, which is not the case on our very small system. So edit this file,
and replace defaults.pcm.ipc_gid audio by defaults.pcm.ipc_gid 0 instead.
And we’re done with alsa-lib!

Alsa-utils
Download alsa-utils from the ALSA offical webpage. We tested the lab with version 1.2.9.
Once uncompressed, we quickly discover that the alsa-utils build system is based on the autotools, so we will
work once again with a regular configure script.
As we’ve seen previously, we will have to provide the prefix and host options and the CC variable:

$ ./configure --host=arm-linux --prefix=/usr

Now, we should quiclky get an error in the execution of the configure script:
checking for libasound headers version >= 1.2.5 (1.2.5)... not present.
configure: error: Sufficiently new version of libasound not found.
Again, we can check in config.log what the configure script is trying to do:
configure:15855: checking for libasound headers version >= 1.2.5 (1.2.5)
configure:15902: arm-linux-gcc -c -g -O2 conftest.c >&5
conftest.c:24:10: fatal error: alsa/asoundlib.h: No such file or directory
Of course, since alsa-utils uses alsa-lib, it includes its header file! So we need to tell the C compiler where
the headers can be found: there are not in the default directory /usr/include/, but in the /usr/include
directory of our staging space. The help text of the configure script says:
CPPFLAGS (Objective) C/C++ preprocessor flags, e.g. -I<include dir> if
you have headers in a nonstandard directory <include dir>
Let’s use it:
$ CPPFLAGS=-I$HOME/embedded-linux-labs/thirdparty/staging/usr/include \
./configure --host=arm-linux --prefix=/usr

Now, it should stop a bit later, this time with the error:
checking for snd_ctl_open in -lasound... no
configure: error: No linkable libasound was found.
The configure script tries to compile an application against libasound (as can be seen from the -lasound
option): alsa-utils uses alsa-lib, so the configure script wants to make sure this library is already installed.
Unfortunately, the ld linker doesn’t find it. So, let’s tell the linker where to look for libraries using the -L

© 2004-2024 Bootlin, CC BY-SA license 45


Embedded Linux System Development

option followed by the directory where our libraries are (in staging/usr/lib). This -L option can be passed
to the linker by using the LDFLAGS at configure time, as told by the help text of the configure script:
LDFLAGS linker flags, e.g. -L<lib dir> if you have libraries in a
nonstandard directory <lib dir>
Let’s use this LDFLAGS variable:
$ LDFLAGS=-L$HOME/embedded-linux-labs/thirdparty/staging/usr/lib \
CPPFLAGS=-I$HOME/embedded-linux-labs/thirdparty/staging/usr/include \
./configure --host=arm-linux --prefix=/usr

Once again, it should fail a bit further down the tests, this time complaining about a missing curses helper
header. curses or ncurses is a graphical framework to design UIs in the terminal. This is only used by
alsamixer, one of the tools provided by alsa-utils, that we are not going to use. Hence, we can just disable
the build of alsamixer.
Of course, if we wanted it, we would have had to build ncurses first, just like we built alsa-lib.

$ LDFLAGS=-L$HOME/embedded-linux-labs/thirdparty/staging/usr/lib \
CPPFLAGS=-I$HOME/embedded-linux-labs/thirdparty/staging/usr/include \
./configure --host=arm-linux --prefix=/usr \
--disable-alsamixer

Then, run the compilation with make. You may hit a final error:
Making all in po
make[2]: Entering directory '/home/tux/embedded-linux-labs/
thirdparty/alsa-utils-1.2.9/alsaconf/po'
mv: cannot stat 't-ja.gmo': No such file or directory
This can be fixed by disabling support for alsaconf too:

$ LDFLAGS=-L$HOME/embedded-linux-labs/thirdparty/staging/usr/lib \
CPPFLAGS=-I$HOME/embedded-linux-labs/thirdparty/staging/usr/include \
./configure --host=arm-linux --prefix=/usr \
--disable-alsamixer --disable-alsaconf

You can now run make again. It should work this time.
Let’s now begin the installation process. Before really installing in the staging directory, let’s install in a
dummy directory, to see what’s going to be installed (this dummy directory will not be used afterwards, it
is only to verify what will be installed before polluting the staging space):

$ make DESTDIR=/tmp/alsa-utils/ install

The DESTDIR variable can be used with all Makefiles based on automake. It allows to override the installation
directory: instead of being installed in the configuration prefix directory, the files will be installed in DESTDIR/
configuration-prefix.
Now, let’s see what has been installed in /tmp/alsa-utils/ (run tree /tmp/alsa-utils):
/tmp/alsa-utils/
|-- lib
| `-- udev
| `-- rules.d
| `-- 90-alsa-restore.rules
|-- usr
| |-- bin
| | |-- aconnect

46 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

| | |-- alsabat
| | |-- alsaloop
| | |-- alsaucm
| | |-- amidi
| | |-- amixer
| | |-- aplay
| | |-- aplaymidi
| | |-- arecord -> aplay
| | |-- arecordmidi
| | |-- aseqdump
| | |-- aseqnet
| | |-- axfer
| | |-- iecset
| | `-- speaker-test
| |-- sbin
| | |-- alsabat-test.sh
| | |-- alsactl
| | `-- alsa-info.sh
| `-- share
| |-- alsa
| | `-- init
| | |-- 00main
| | |-- ca0106
| | |-- default
| | |-- hda
| | |-- help
| | |-- info
| | `-- test
| |-- locale
| | |-- de
| | | `-- LC_MESSAGES
| | | `-- alsa-utils.mo
| | |-- eu
| | | `-- LC_MESSAGES
| | | `-- alsa-utils.mo
| | |-- fr
| | | `-- LC_MESSAGES
| | | `-- alsa-utils.mo
| | |-- ja
| | | `-- LC_MESSAGES
| | | `-- alsa-utils.mo
| | |-- ka
| | | `-- LC_MESSAGES
| | | `-- alsa-utils.mo
| | `-- sk
| | `-- LC_MESSAGES
| | `-- alsa-utils.mo
| |-- man
| | |-- man1
| | | |-- aconnect.1
| | | |-- alsabat.1
| | | |-- alsactl.1
| | | |-- alsa-info.sh.1
| | | |-- alsaloop.1

© 2004-2024 Bootlin, CC BY-SA license 47


Embedded Linux System Development

| | | |-- amidi.1
| | | |-- amixer.1
| | | |-- aplay.1
| | | |-- aplaymidi.1
| | | |-- arecord.1 -> aplay.1
| | | |-- arecordmidi.1
| | | |-- aseqdump.1
| | | |-- aseqnet.1
| | | |-- axfer.1
| | | |-- axfer-list.1
| | | |-- axfer-transfer.1
| | | |-- iecset.1
| | | `-- speaker-test.1
| | `-- man7
| `-- sounds
| `-- alsa
| |-- Front_Center.wav
| |-- Front_Left.wav
| |-- Front_Right.wav
| |-- Noise.wav
| |-- Rear_Center.wav
| |-- Rear_Left.wav
| |-- Rear_Right.wav
| |-- Side_Left.wav
| `-- Side_Right.wav
`-- var
`-- lib
`-- alsa

30 directories, 59 files
So, we have:
• The udev rules in lib/udev
• The alsa-utils binaries in /usr/bin and /usr/sbin
• Some sound samples in /usr/share/sounds
• The various translations in /usr/share/locale
• The manual pages in /usr/share/man/, explaining how to use the various tools
• Some configuration samples in /usr/share/alsa.
Now, let’s make the installation in the staging space:

$ make DESTDIR=$HOME/embedded-linux-labs/thirdparty/staging/ install


Then, let’s manually install only the necessary files in the target space. We are only interested in speaker-
test:
$ cd ..
$ cp -a staging/usr/bin/speaker-test target/usr/bin/
$ arm-linux-strip target/usr/bin/speaker-test

And we’re finally done with alsa-utils!


Now test that all is working fine by running the speaker-test util on your board, with the headset provided
by your instructor plugged in. You may need to add the missing libraries from the toolchain install directory.

48 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Now you can use:


• speaker-test with no arguments to generate pink noise
• speaker-test -t sine to generate a sine wave, optionally with -f <freq> for a specific frequency
There you are: you built and ran your first program depending on a library different from the C library.

libgpiod
Compiling libgpiod
We are now going to use libgpiod (instead of the deprecated interface in /sys/class/gpio, whose executables
(gpiodetect, gpioset, gpioget...) will allow us to drive and manage GPIOs from shell scripts.
Here, we will be using the 2.0.x version of libgpiod.

git clone https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git


cd libgpiod
git checkout v2.0.x

As we are not starting from a release, we will need to install further development tools to generate some files
like the configure script:

sudo apt install autoconf-archive pkg-config


Now let’s generate the files which are present in a release:

./autogen.sh
Run ./configure --help script, and see that this script provides a --enable-tools option which allows to
build the userspace executables that we want.
As this project doesn’t have any external library dependency, let’s configure libgpiod in a similar way as
alsa-utils:
$ ./configure --host=arm-linux --prefix=/usr --enable-tools

Now, compile the software:

$ make
Installation to the staging space can be done using the classical DESTDIR mechanism:

$ make DESTDIR=$HOME/embedded-linux-labs/thirdparty/staging/ install

And finally, only manually install and strip the files needed at runtime in the target space:

$ cd ..
$ cp -a staging/usr/lib/libgpiod.so.3* target/usr/lib/
$ arm-linux-strip target/usr/lib/libgpiod*
$ cp -a staging/usr/bin/gpio* target/usr/bin/
$ arm-linux-strip target/usr/bin/gpio*

Testing libgpiod
First, connect GPIO PE1 (pin D2 of connector CN14) connected to ground (pin 7 of connector CN16), as in
the Accessing Hardware Devices lab.
Now, let’s run the gpiodetect command on the target, and check that you can list the various GPIO banks
on your system.

© 2004-2024 Bootlin, CC BY-SA license 49


Embedded Linux System Development

# gpiodetect
gpiochip0 [GPIOA] (16 lines)
gpiochip1 [GPIOB] (16 lines)
gpiochip2 [GPIOC] (16 lines)
gpiochip3 [GPIOD] (16 lines)
gpiochip4 [GPIOE] (16 lines)
gpiochip5 [GPIOF] (16 lines)
gpiochip6 [GPIOG] (16 lines)
gpiochip7 [GPIOH] (16 lines)
gpiochip8 [GPIOI] (12 lines)
gpiochip9 [GPIOZ] (8 lines)

Again, we can see that GPIOE is represented by gpiochip4.


We can then get details on GPIOE GPIOs by running gpioinfo -c gpiochip4 or on all GPIOs by simply
running gpioinfo.
You can now read the state of your GPIO PE1:

# gpioget -c gpiochip4 1
"1"=inactive

Now, connect your wire to 3V3 (pin 2 of connector CN16). You should now read:

# gpioget -c gpiochip4 1
"1"=active

You see that you didn’t have to configure the GPIO as input. libgpiod did that for you.
If you have an LED and a small breadboard (or M-F breadboard wires), you could also try to drive the
GPIO in output mode. Connect the short pin of the LED to GND, and the long one to the GPIO. Then
then following command should light up the diode:

# gpioset -c gpiochip4 1=1

Here’s how to turn it off:


# gpioset -c gpiochip4 1=0

gpioset offers many more options. Run gpioset -h to check by yourself.

ipcalc
After practicing with autotools based packages, let’s build ipcalc, which is using Meson as build system. We
won’t really need this utility in our system, but at least it has no dependencies and therefore offers an easy
way to build our first Meson based package.
So, first install the meson package:

$ sudo apt install meson

In the main lab directory, then let’s check out the sources through git:

$ git clone https://gitlab.com/ipcalc/ipcalc.git


$ cd ipcalc/
$ git checkout 1.0.3

To cross-compile with Meson, we need to create a cross file. Let’s create the ../cross-file.txt file with
the below contents:

50 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

[binaries]
c = 'arm-linux-gcc'

[host_machine]
system = 'linux'
cpu_family = 'arm'
cpu = 'cortex-a7'
endian = 'little'
We also need to create a special directory for building:

$ mkdir cross-build
$ cd cross-build

We can now have meson create the Ninja build files for us:

$ meson --cross-file ../../cross-file.txt --prefix /usr ..

We are now ready to build ipcalc:

$ ninja

And now install ipcalc to the build space:

$ DESTDIR=$HOME/embedded-linux-labs/thirdparty/staging ninja install

Check that the staging/usr/bin/ipcalc file is indeed an ARM executable.


The last thing to do is to copy it to the target space and strip it:

$ cd ../..
$ cp staging/usr/bin/ipcalc target/usr/bin/
$ arm-linux-strip target/usr/bin/ipcalc

Note that we could have asked ninja install to strip the executable for us when installing it into the staging
directory. To do, this, we would have added a strip entry in the cross file, and passed --strip to Meson.
However, it’s better to keep files unstripped in the staging space, in case we need to debug them.
You can now test that ipcalc works on the target:

# ipcalc 192.168.0.100
Address: 192.168.0.100
Address space: Private Use

Final touch
To finish this lab completely, and to be consistent with what we’ve done before, let’s strip the C library and
its loader too.
First, check the initial size of the binaries:

$ ls -l target/lib

Then strip the binaries in /lib:

$ chmod +w target/lib/*.so.*
$ arm-linux-strip target/lib/*.so.*

And check the final size:

© 2004-2024 Bootlin, CC BY-SA license 51


Embedded Linux System Development

$ ls -l target/lib/

52 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Using a build system, example with Buil-


droot
Objectives: discover how a build system is used and how it works, with the
example of the Buildroot build system. Build a full Linux system, including
the Linux kernel.

Goals
Compared to the previous lab, we are going to build a more elaborate system, still containing alsa-utils (and
of course its alsa-lib dependency), but this time using Buildroot, an automated build system.
The automated build system will also allow us to add more packages and play real audio on our system,
thanks to the Music Player Daemon (mpd) (https://www.musicpd.org/ and its mpc client.
As in a real project, we will also build the Linux kernel from Buildroot, and install the kernel modules in the
root filesystem.

Setup
Go to the $HOME/embedded-linux-labs/buildroot directory.

Get Buildroot and explore the source code


The official Buildroot website is available at https://buildroot.org/. Clone the Git repository:

git clone https://git.buildroot.net/buildroot


cd buildroot

Now checkout the tag corresponding to the latest 2023.02.<n> release (Long Term Support), which we have
tested for this lab.
Several subdirectories or files are visible, the most important ones are:
• boot contains the Makefiles and configuration items related to the compilation of common bootloaders
(GRUB, U-Boot, Barebox, etc.)
• board contains board specific configurations and root filesystem overlays.
• configs contains a set of predefined configurations, similar to the concept of defconfig in the kernel.
• docs contains the documentation for Buildroot.
• fs contains the code used to generate the various root filesystem image formats
• linux contains the Makefile and configuration items related to the compilation of the Linux kernel
• Makefile is the main Makefile that we will use to use Buildroot: everything works through Makefiles
in Buildroot;
• package is a directory that contains all the Makefiles, patches and configuration items to compile
the user space applications and libraries of your embedded Linux system. Have a look at various
subdirectories and see what they contain;
• system contains the root filesystem skeleton and the device tables used when a static /dev is used;

© 2004-2024 Bootlin, CC BY-SA license 53


Embedded Linux System Development

• toolchain contains the Makefiles, patches and configuration items to generate the cross-compiling
toolchain.

Board specific configuration


As we will want Buildroot to build a kernel with a custom configuration, and our custom patch, so let’s add
our own subdirectory under board:

mkdir -p board/bootlin/training

Then, copy your kernel configuration and kernel patch:

cp ../../kernel/linux/.config board/bootlin/training/linux.config
cp ../../kernel/linux/0001-Custom-DTS-for-Bootlin-lab.patch \
board/bootlin/training/

We will configure Buildroot to use this kernel configuration.

Configure Buildroot
In our case, we would like to:
• Generate an embedded Linux system for ARM;
• Use an already existing external toolchain instead of having Buildroot generating one for us;
• Compile the Linux kernel and deploy its modules in the root filesystem;
• Integrate BusyBox, alsa-utils, mpd, mpc and evtest in our embedded Linux system;
• Integrate the target filesystem into a tarball
To run the configuration utility of Buildroot, simply run:

$ make menuconfig

Set the following options. Don’t hesitate to press the Help button whenever you need more details about a
given option:
• Target options
– Target Architecture: ARM (little endian)
– Target Architecture Variant: cortex-A7
– Target ABI: EABIhf
– Floating point strategy: VFPv4
• Toolchain
– Toolchain type: External toolchain
– Toolchain: Custom toolchain
– Toolchain path: use the toolchain you built: /home/<user>/x-tools/arm-training-linux-
musleabihf (replace <user> by your actual user name)
– External toolchain gcc version: 12.x
– External toolchain kernel headers series: 6.1.x or later
– External toolchain C library: musl (experimental)
– We must tell Buildroot about our toolchain configuration, so select Toolchain has SSP support?
and Toolchain has C++ support?. Buildroot will check these parameters anyway.

54 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

• Kernel
– Enable Linux Kernel
– Set Kernel version to Latest version (6.1)
– Set Custom kernel patches to board/bootlin/training/0001-Custom-DTS-for-Bootlin-lab.patch
– Set Kernel configuration to Using a custom (def)config file)
– Set Configuration file path to board/bootlin/training/linux.config
– Select Build a Device Tree Blob (DTB)
– Set In-tree Device Tree Source file names to stm32mp157a-dk1-custom
• Target packages
– Keep BusyBox (default version) and keep the BusyBox configuration proposed by Buildroot;
– Audio and video applications
∗ Select alsa-utils, and in the submenu:
· Only keep speaker-test
∗ Select mpd, and in the submenu:
· Keep only alsa, vorbis and tcp sockets
∗ Select mpd-mpc.
– Hardware handling
∗ Select evtest
This userspace application allows to test events from input devices. This way, we will be able
to test the Nunchuk by getting details about which buttons were pressed.
• Filesystem images
– Select tar the root filesystem
Exit the menuconfig interface. Your configuration has now been saved to the .config file.

Generate the embedded Linux system


Just run:

$ make
Buildroot will first create a small environment with the external toolchain, then download, extract, configure,
compile and install each component of the embedded system.
All the compilation has taken place in the output/ subdirectory. Let’s explore its contents:
• build, is the directory in which each component built by Buildroot is extracted, and where the build
actually takes place
• host, is the directory where Buildroot installs some components for the host. As Buildroot doesn’t
want to depend on too many things installed in the developer machines, it installs some tools needed
to compile the packages for the target. In our case it installed pkg-config (since the version of the host
may be ancient) and tools to generate the root filesystem image (genext2fs, makedevs, fakeroot).
• images, which contains the final images produced by Buildroot. In our case it contains a tarball of the
filesystem, called rootfs.tar, plus the compressed kernel and Device Tree binary. Depending on the
configuration, there could also a bootloader binary or a full SD card image.

© 2004-2024 Bootlin, CC BY-SA license 55


Embedded Linux System Development

• staging, which contains the “build” space of the target system. All the target libraries, with headers
and documentation. It also contains the system headers and the C library, which in our case have been
copied from the cross-compiling toolchain.
• target, is the target root filesystem. All applications and libraries, usually stripped, are installed in
this directory. However, it cannot be used directly as the root filesystem, as all the device files are
missing: it is not possible to create them without being root, and Buildroot has a policy of not running
anything as root.

Run the generated system


Go back to the $HOME/embedded-linux-labs/buildroot/ directory. Create a new nfsroot directory that is
going to hold our system, exported over NFS. Go into this directory, and untar the rootfs using:

$ tar xvf ../buildroot/output/images/rootfs.tar

Add our nfsroot directory to the list of directories exported by NFS in /etc/exports.
Also update the kernel and Device Tree binaries used by your board, from the ones compiled by Buildroot
in output/images/.
Boot the board, and log in (root account, no password).
You should now reach a shell.

Loading the USB audio module


You can check that no kernel module is loaded yet. Try to load the snd_usb_audio module from the command
line.
This should work. Check that Buildroot has deployed the modules for your kernel in /lib/modules.
Let’s automate this now!
Look at the /etc/inittab file generated by Buildroot (ask your instructor if you have any questions), and
at the contents of the /etc/init.d/ directory, in particular of the rcS file.
You can see that rcS executes or sources all the /etc/init.d/S??* files. We can add our own which will load
the toplevel modules that we need.
Let’s do this by creating an overlay directory, typically under our board specific directory, that Buildroot
will add after building the root filesystem:

mkdir -p board/bootlin/training/rootfs-overlay/

Then add a custom startup script, by adding an etc/init.d/S03modprobe executable file to the overlay
directory, with the below contents:
#!/bin/sh
modprobe snd-usb-audio
Then, go back to Buildroot’s configuration interface:
• System configuration
– Set Root filesystem overlay directories to board/bootlin/training/rootfs-overlay
Build your image again. This should be quick as Buildroot doesn’t need to recompile anything. It will just
apply the root filesystem overlay.
Update your nfsroot directory, reboot the board and check that the snd_usb_audio module is loaded as
expected.
You can run speaker-test to check that audio indeed works.

56 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Testing music playback with mpd and mpc


The next thing we want to do is play real sound samples with the Music Player Daemon (MPD). So, let’s
add music files 11 for MPD to play:

mkdir -p board/bootlin/training/rootfs-overlay/var/lib/mpd/music
cp ../data/music/* board/bootlin/training/rootfs-overlay/var/lib/mpd/music

Update your root filesystem. Thanks to NFS, you don’t need to restart your system.
Using the ps command, check that the mpd server was started by the system, as implemented by the /etc/
init.d/S95mpd script.
If that’s the case, you are now ready to run mpc client commands to control music playback. First, let’s make
mpd process the newly added music files. Run this command on the target:

# mpc update

You should see the files getting indexed, by displaying the contents of the /var/log/mpd.log file:
Jan 01 00:04 : exception: Failed to open '/var/lib/mpd/state': No such file or directory
Jan 01 00:15 : update: added /2-arpent.ogg
Jan 01 00:15 : update: added /6-le-baguette.ogg
Jan 01 00:15 : update: added /4-land-of-pirates.ogg
Jan 01 00:15 : update: added /3-chronos.ogg
Jan 01 00:15 : update: added /1-sample.ogg
Jan 01 00:15 : update: added /7-fireworks.ogg
Jan 01 00:15 : update: added /5-ukulele-song.ogg
You can also check the list of available files:
# mpc listall
1-sample.ogg
2-arpent.ogg
5-ukulele-song.ogg
3-chronos.ogg
7-fireworks.ogg
6-le-baguette.ogg
4-land-of-pirates.ogg
To play files, you first need to create a playlist. Let’s create a playlist by adding all music files to it:

# mpc add /

You should now be able to start playing the songs in the playlist:

# mpc play

Here are a few further commands for controlling playback:


• mpc volume +5: increase the volume by 5%
• mpc volume -5: reduce the volume by 5%
• mpc prev: switch to the previous song in the playlist.
• mpc next: switch to the next song in the playlist.
• mpc toggle: toggle between pause and playback modes.
11 For the most part, these are public domain music files, except a small sample file... See the README.txt file in the directory

containing the files.

© 2004-2024 Bootlin, CC BY-SA license 57


Embedded Linux System Development

If you find that changing the volume is not available, you can add a custom configuration for MPD, as the
standard one provided by Buildroot doesn’t support allowing to change the audio playback volume with all
sound cards we have tested. We will simply add this file to our overlay:

cp ../data/mpd.conf board/bootlin/training/rootfs-overlay/etc/

Run Buildroot again and update your root filesystem. Here again, you don’t need to reboot. It’s sufficient
to restart MPD to make it read the new configuration file:

# /etc/init.d/S95mpd restart
You can now make sure that modifying the volume works.
Later, we will compile and debug a custom MPD client application.

Analyzing dependencies
It’s always useful to understand the dependencies drawn by the packages we build.
First we need to install a Graphviz:

$ sudo apt install graphviz


Now, let’s use Buildroot’s target to generate a dependency graph:

$ make graph-depends
We can now study the dependency graph:

$ evince output/graphs/graph-depends.pdf
In particular, you can see that adding MPD and its client required to compile Meson for the host, and in
turn, Python 3 for the host too. This substantially contributed to the build time.

Adding a Buildroot package


We would also like to build our Nunchuk external module with Buildroot. Fortunately, Buildroot has a
kernel-module infrastructure to build kernel modules.
First, create a nunchuk-driver subdirectory under package in Buildroot sources.
The first thing is to create a package/nunchuk-driver/Config.in file for Buildroot’s configuration:
config BR2_PACKAGE_NUNCHUK_DRIVER
bool "nunchuk-driver"
depends on BR2_LINUX_KERNEL
help
Linux Kernel module for the I2C Nunchuk.
Then add a line to package/Config.in to include this file, for example right before the line including package/
nvidia-driver/Config.in, so that the alphabetic order of configuration options is kept.
Then, the next and last thing you need to do is create package/nunchuk-driver/nunchuk-driver.mk describ-
ing how to build the package:
NUNCHUK_DRIVER_VERSION = 1.0
NUNCHUK_DRIVER_SITE = $(HOME)/embedded-linux-labs/hardware/data/nunchuk
NUNCHUK_DRIVER_SITE_METHOD = local
NUNCHUK_DRIVER_LICENSE = GPL-2.0

$(eval $(kernel-module))
$(eval $(generic-package))

58 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

Then, configure Buildroot to build your package, run Buildroot and update your root filesystem.
Can you load the nunchuk module now? If everything’s fine, add a line to /etc/init.d/S03modprobe for this
driver, and update your root filesystem once again.

Testing the Nunchuk


Now that we have the nunchuk driver loaded and that Buildroot compiled evtest for the target, thanks to
Buildroot, we can now test the input events coming from the Nunchuk.

# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0: pmic_onkey
/dev/input/event1: Logitech Inc. Logitech USB Headset H340 Consumer Control
/dev/input/event2: Logitech Inc. Logitech USB Headset H340
/dev/input/event3: Wii Nunchuk
Select the device event number [0-3]:

Enter the number corresponding to the Nunchuk device.


You can now press the Nunchuk buttons, use the joypad, and see which input events are emitted.
By the way, you can also test which input events are exposed by the driver for your audio headset (if any),
which doesn’t mean that they physically exist.

Commit your changes


As we are going to reuse our Buildroot changes in the next labs, let’s commit them into a branch:

git checkout -b bootlin-labs


git add board/bootlin/ package/nunchuk-driver/
git commit -as -m "Bootlin lab changes"

Going further
If you finish your lab before the others
• For more music playing fun, you can install the ario or cantata MPD client on your host machine
(sudo apt install ario, sudo apt install cantata), configure it to connect to the IP address of
your target system with the default port, and you will also be able to control playback from your host
machine.

© 2004-2024 Bootlin, CC BY-SA license 59


Embedded Linux System Development

System Integration - Using systemd


Objectives: Get familiar with the systemd init system.

Goals
Compared to the previous lab, we go on increasing the complexity of the system, this time by using the
systemd init system, and by taking advantage of it to add a few extra features, in particular ones that will
be useful for debugging in the next lab.

Setup
Since systemd requires the GNU C library, we are going to make a new Buildroot build in a new working
directory, and using a different cross-compiling toolchain.
So, create the $HOME/embedded-linux-labs/integration directory and go inside it.
Make a new clone of Buildroot from the existing local Git repository, and checkout our bootlin-labs branch:

git clone $HOME/embedded-linux-labs/buildroot/buildroot


cd buildroot
git checkout bootlin-labs

Root filesystem overlay


Remove etc/init.d/ from the root filesystem overlay. It was adapted to BusyBox init, not to systemd:

rm -r board/bootlin/training/rootfs-overlay/etc/init.d/

Buildroot configuration
Configure Buildroot as follows:
• Target options
– Select the same architecture and CPU settings as in the previous lab.
• Toolchain
– Toolchain type: External toolchain
– Toolchain: Bootlin toolchains
This time, we will use a Bootlin ready-made toolchain for glibc, as this is necessary for using
systemd.
– Toolchain origin: Toolchain to be downloaded and installed
– Bootlin toolchain variant: armv7-eabihf glibc bleeding-edge 2022.08-1
– Select Copy gdb server to the Target
• System configuration
– Init system: systemd
– Root filesystem overlay directories: board/bootlin/training/rootfs-overlay
• Kernel

60 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

– Enable Linux Kernel


– Set Kernel version to Latest version (6.1)
– Set Custom kernel patches to board/bootlin/training/0001-Custom-DTS-for-Bootlin-lab.patch
– Set Kernel configuration to Using a custom (def)config file)
– Set Configuration file path to board/bootlin/training/linux.config
– Select Build a Device Tree Blob (DTB)
– Set In-tree Device Tree Source file names to stm32mp157a-dk1-custom
• Target packages
– Audio and video applications
∗ We won’t need alsa-utils this time.
∗ Select mpd, and in the submenu:
· Keep only alsa, vorbis and tcp sockets
∗ Select mpd-mpc.
– Hardware handling
∗ Select nunchuk driver
– Networking applications
∗ Select dropbear, a lightweight SSH server used instead of OpenSSH in most embedded devices.
You don’t need to enable client support (building an SSH client).
• Filesystem images
– Select tar the root filesystem

Build and test the new system


Now build the full system.
Once the build is over, generate the dependency graph again and find out the new dependencies introduced
by using systemd.
To test the new system, create a new nfsroot directory, extract the new root filesystem into it, and boot
your board on it through NFS.
You should see the system booting through systemd, with all the systemd targets and system services starting
one by one, with a total boot time which looks slower than before. That’s because the system configuration
is more complex, but also more versatile, being ready to run more complex services and applications.
You can ask systemd to show you the various services which were started:

# systemctl status
You can also check all the mounted filesystems and be impressed:

# mount

Inspecting the system


On the target, look at the contents of /lib/systemd. You will see the implementation of most systemd targets
and services.
In particular, check out /lib/systemd/user/ containing some unnecessary targets in our case such as
bluetooth.target.

© 2004-2024 Bootlin, CC BY-SA license 61


Embedded Linux System Development

However, check the mpd.service file for our MPD server. This should help you to realize all the options
provided by systemd to start and control system services, while keeping the system secure and their resources
under control.
You won’t be able to match this level of control and security in a ”hand-made” system.

Understanding automatic module loading with Udev


Check the currently loaded modules on your system. Surprise: both the Nunchuk and USB audio modules are
already loaded. We didn’t have anything to set up and systemd automatically loaded the modules associated
to connected hardware.
Let’s find out why...
On the target, go to /lib/udev/rules.d. You will find all the standard rules for Udev, the part of systemd
which handles hardware events, takes care of the permissions and ownership of device files, notifies other
userspace programs, and among others, load kernel modules.
Open 80-drivers.rules, which is the rule allowing Udev to load kernel modules for detected devices. Here
is its most important line:
ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load '$env{MODALIAS}'"
This is when the modules.alias file comes into play. When a new device is found, the kernel passes a
MODALIAS environment variable to Udev, containing which bus this happened on and the attributes of the
device on this bus. Thanks to the module aliases, the right module gets loaded. We already explained that
in the lectures when talking about the output of make modules_install.
Find where the modules.alias file is located and you will find the two lines that allowed to load our snd_
usb_audio and nunchuk modules:
...
alias usb:v*p*d*dc*dsc*dp*ic01isc01ip*in* snd_usb_audio
alias usb:v2B53p0031d*dc*dsc*dp*ic*isc*ip*in* snd_usb_audio
...
alias of:N*T*Cnintendo,nunchuk nunchuk

For snd_usb_audio, there are many possible matching values, so it’s not straightforward to be sure which
matched your particular device.
However, you can find in sysfs which MODALIAS was emitted for your device:

# cd /sys/class/sound/card0/device
# ls -la
# cat modalias
usb:v1B3Fp2008d0100dc00dsc00dp00ic01isc01ip00in00

With a bit of patience, you could find the matching line in the modules.alias file.
If you want to see the information sent to Udev by the kernel when a new device is plugged in, here are a
few debugging commands.
First unplug your device and run:

# udevadm monitor
Then plug in your headset again. You will find all the events emitted by the kernel, and with the same string
(with UDEV instead of KERNEL), the time when Udev finished processing each event.
You can also see the MODALIAS values carried by these events:

62 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

# udevadm monitor --env


As far as the Nunchuk is concerned, we cannot easily remove it from the Device Tree and add it back, but
it’s easier to find its MODALIAS value:
# cd /sys/bus/i2c/devices
# ls -la

Here you will recognize our Nunchuk device through its 0x52 address.

# cd 1-0052
# ls -la
# cat modalias
of:NjoystickT(null)Cnintendo,nunchuk

Here the bus is of, meaning Open Firmware, which was the former name of the Device Tree. When an event
was emitted by the kernel with this MODALIAS string, the nunchuk module got loaded by Udev thanks to the
matching alias.
This actually happened when systemd ran the coldplugging operation: at system startup, it asked the kernel
to emit hotplug events for devices already present when the system booted:
[ OK ] Finished Coldplug All udev Devices.
On non-x86 platforms, that’s typically for devices described in the Device Tree. This way, both static and
hotplugged devices can be handled in the same way, using the same Udev rules.

Testing your system


Make sure that audio playback still works on your system:

# mpc update
# mpc add /
# mpc play

If it doesn’t, look at the systemd logs in your serial console history. systemd should let you know about the
failing services and the commands to run to get more details.

© 2004-2024 Bootlin, CC BY-SA license 63


Embedded Linux System Development

Application development and applica-


tion debugging
Objective: compile an application against a Buildroot build space and debug
it remotely.

Setup
We will continue to use the same root filesystem.
Our goal is to compile and debug our own MPD client. This client will be driven by the Nunckuk to switch
between audio tracks, and to adjust the playback volume.
However, this client will be used together with mpc, as it won’t be able to create the playlist and start the
playback. It will just be used to control the volume and switch between songs. So, you need to run mpc
commands first before trying the new client:

mpc update
mpc add /
mpc pause

We will use the new client to resume playback.

Compile your own application


Go to the $HOME/embedded-linux-labs/appdev directory.
In the lab directory the file nunchuk-mpd-client.c contains an application which implements a simple MPD
client based on the libmpdclient library. As mpc is also based on this library, Buildroot already compiled
it and added it to our root filesystem. What’s special in this application is that it allows to drive music
playback through our Nunchuk.
Buildroot has generated toolchain wrappers in output/host/bin, which make it easier to use the toolchain,
since these wrappers pass some mandatory flags (especially the --sysroot gcc flag, which tells gcc where to
look for the headers and libraries). This way, we can compile our application outside of Buildroot, as often
as we want.
Let’s add this directory to our PATH:

$ export PATH=$HOME/embedded-linux-labs/integration/buildroot/output/host/bin:$PATH

Let’s try to compile the application:

$ arm-linux-gcc -o nunchuk-mpd-client nunchuk-mpd-client.c

The compiler complains about undefined references to some symbols in libmpdclient. This is normal, since
we didn’t tell the compiler to link with this library. So let’s use pkg-config to query the pkg-config database
about the list of libraries needed to build an application against libmpdclient12 :

12 Again, output/host/bin has a special pkg-config that automatically knows where to look, so it already knows the right paths

to find .pc files and their sysroot.

64 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

$ arm-linux-gcc -o nunchuk-mpd-client nunchuk-mpd-client.c \


$(pkg-config --libs libmpdclient)

Copy the nunchuk-mpd-client executable to the /root directory of the root filesystem, and then strip it.
Back to target system, try to run the program:

# /root/nunchuk-mpd-client
ERROR: didn't manage to find the Nunchuk device in /dev/input. Is the Nunchuk driver loaded?

Enable debugging tools


In order to debug our application, let’s make Buildroot build some debugging tools for our root filesystem.
This is also an opportunity to enable perf, that we are using later on during this lab. Go back to the
Buildroot configuration interface and enable the following options:
• Kernel
– In Linux Kernel Tools, select perf
• Debugging, profiling and benchmark
– Select ltrace
– Select strace
Then rebuild and update your NFS root filesystem.

Using strace
Let’s run the program through the strace command to find out why this happens.
You should see that it’s trying to access files that don’t exist. Once you’ve found what’s wrong, fix the code
(or ask your instructor for help if needed), then rebuild the program and run it again:

# /root/nunchuk-mpd-client
ERROR: didn't manage to find the Nunchuk device in /dev/input. Is the Nunchuk driver loaded?

Ouch, same problem again!


You can run the program again through strace, and check that the right paths are now accessed, but the
cause of the issue won’t be easy to find.

Using ltrace
Let’s run the program through ltrace now. We will be able to see the shared library calls.
Take your time to study the ltrace output. That’s interesting information! Back to our issue, the last lines
of output should make the issue pretty obvious.
Fix the bug in the code, recompile the program, copy it to the target, strip it and start it again.
You should now be able to use the new client, driving the server through the following Nunchuk inputs:
• Joystick up: volume up 5%
• Joystick down: volume down 5%
• Joystick left: previous song
• Joystick right: next song
• Z (big) button: pause / play
• C (small) button: quit client

© 2004-2024 Bootlin, CC BY-SA license 65


Embedded Linux System Development

Have fun with the new client. You’ll just realize that quitting causes the program to crash with a segmentation
fault. Let’s debug this too.

Using gdbserver from the command line


We are going to use gdbserver to understand why the program segfaults.
Compile nunchuk-mpd-client.c again with the -g (g means gdb) option to include debugging symbols. This
time, just keep it on your workstation, as you already have the version without debugging symbols on your
target.
Then, on the target side, run the program under gdbserver. gdbserver will listen on a TCP port for a
connection from gdb on the host, and will control the execution of nunchuk-mpd-client according to the gdb
commands:

=> gdbserver localhost:2345 /root/nunchuk-mpd-client

On the host side, run arm-linux-gdb (also found in your toolchain):

$ arm-linux-gdb nunchuk-mpd-client

gdb starts and loads the debugging information from the nunchuk-mpd-client binary (in the appdev directory)
which has been compiled with -g.
Then, we need to tell where to find our libraries, since they are not present in the default /lib and /usr/lib
directories on your workstation. This is done by setting the gdb sysroot variable (on one line):

(gdb) set sysroot /home/<user>/embedded-linux-labs/integration/\


buildroot/output/staging

Of course, replace <user> by your actual user name.


And tell gdb to connect to the remote system:

(gdb) target remote <target-ip-address>:2345

Then, use gdb as usual to set breakpoints, look at the source code, run the application step by step, etc.
In our case, we’ll just start the program, press the C button to quit to cause the the segmentation fault:

(gdb) continue

After the segmentation fault, you can ask for a backtrace to see where this happened:

(gdb) backtrace

This will tell you that the segmentation fault occurred in a function of the libmpdclient, called by our
program. You will also get the number of the line in the program which caused this. This should help you
to find the bug in our application.
Once you found it, don’t fix it yet. We are going to make further experiments around this segmentation fault.

Post mortem analysis


Following the details in the slides, configure your shell on the target to get a core.xxx file dumped when you
run nunchuk-mpd-client again.
Once you have such a file, inspect it with arm-linux-gdb on the host as explained in the lectures.
Don’t be surprised, the below warnings are expected:

66 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

warning: Can't open file /root/nunchuk-mpd-client during file-backed mapping note processing

warning: Can't open file /usr/lib/libc.so.6 during file-backed mapping note processing

warning: Can't open file /usr/lib/libmpdclient.so.2.20 during file-backed mapping note processing

warning: Can't open file /usr/lib/ld-linux-armhf.so.3 during file-backed mapping note processing

warning: core file may not match specified executable file.

In the gdb shell, set the sysroot setting as previously, and then generate a backtrace to see where the program
crashed. You can even see the value of all variables in the different function contexts of your program:

(gdb) bt full

This way, you can have a lot of information about the crash without running the program through the
debugger.

Editing and remote compiling with VS Code


Installing software
We are going to use Visual Studio Code to do the remote debugging again, and eventually fix and recompile
our program.
The first thing to do is install VS Code. This package is only available as a snap package:

$ sudo snap install --classic code

Accessing your board through SSH


We will use Visual Studio Code to modify and recompile our client program, and also to update and run the
binary on the target. Of course, we will use a simple solution, as we won’t be able to spend too much time
learning about all the possibilities offered by VS Code.
For our purpose, a good solution is SSH, which allows to copy files (through the scp command) and to run
remote commands. We already included the Dropbear SSH server in our root filesystem.
We just need to implement password-less SSH access, to keep things simple:
• If you don’t have an SSH key yet (look at ~/.ssh/), generate a password-less one with the ssh-keygen
command. By default, this creates two files in ~/.ssh/: id_rsa (private key) and id_rsa.pub (public
key).
• Then create the /root/.ssh directory on the target and in it, create an authorized_keys file with
the line in id_rsa.pub.
• Then, fix permissions on the target, as Dropbear is quite strict about them:

# chmod -R go-rwx /root


# chown -R root.root /root

Then, you can test that SSH works without a password:

ssh [email protected]

If you face trouble, you can check the Dropbear logs on the target:

journalctl -fu dropbear

© 2004-2024 Bootlin, CC BY-SA license 67


Embedded Linux System Development

Compiling and debugging the program from VS Code


The appdev directory already contains a prep-debug.sh script and a .vscode directory with ready made
settings for code editing and for compiling and debugging our application. Here are these files:

• prep-debug.sh: script to recompile the program, copy it to the target through SSH, and start it through
the debugger. Open this file and update the target IP and path settings if necessary.

• .vscode/c_cpp_properties.json: settings for the code editor. Modify the paths in this file according
to your setup.

• .vscode/tasks.json: definition of a ”build” task, calling the prep-debug.sh script.

• .vscode/launch.json: these are the settings for remote debugging. Again, open this file, update the
paths, and the target IP address if necessary.

First, start VS Code:

$ code

Use File → Open Folder to open the appdev directory.

The first thing to do is to make sure the C/C++ extension from Microsoft (ms-vscode.cpptools) is installed.
Do this using the Extensions vertical tab:

Then click on the nunchuk-mpd-client.c file in the left column to open it in VS Code.

Now, start by compiling your program from VS Code, copying it to the target, and running it through the
debugger by using the Terminal → Run Build Task... menu entry.

If anything goes wrong, please report issues to the trainer.

Last but not least, you can start debugging the program by clicking on the Run and Debug tab, and then on
the gdb (Launch) at the top:

68 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

In the debug console, you should see that debugging has started. The bottom line of the interface should
turn orange too:

Then, start using the Nunchuk to control playback, and when you try to quit with the C button, VS Code
should now see the segmentation fault:

You can then look at variables, the call stack, browse the code...
To stop debugging, you should use Run → Stop Debugging.
By studing the the code, you should eventually find that what’s causing the segmentation fault is the call
to free() in the test for the C button. Remove this line, save the file through the File menu (otherwise
nothing will change), and then compile and run the application again. This time, there should be no more
segmentation fault when you hit the C button.
If you are ahead of time, don’t hesitate to spend more time with VS Code, for example to add breakpoints
and execute the program step by step.

© 2004-2024 Bootlin, CC BY-SA license 69


Embedded Linux System Development

Profiling the application with perf


Let’s make a quick attempt at profiling our application with the perf command:

perf record /root/nunchuk-mpd-client

Use your application and leave it when you are done.


This stores profiling data in a perf.data file. One way to extract information from it is to run the below
command in the same directory (the one containing perf.data):

perf report

See the time spent in various kernel ([k]) and userspace ([.]) functions.
Now, let’s profile the whole system. First, make sure that the system is currently playing audio. Then SSH
to your board and run perf top (working better through SSH) to see live information about kernel and
userspace functions consuming most CPU time.
This is interactive, but hard to analyze. You can also run perf record for about 30 seconds, followed by
perf report to have a useful summary of system wide activity for a substantial amount of time.
This was a very brief start at practising with perf, which offers many more possibilities than we could see
here.

What to remember
During this lab, we learned that...
• It’s easy to study the behavior of programs and diagnose issues without even having the source code,
thanks to strace, ltrace and perf.
• You can use perf as a system wide profiler too.
• You can leave a small gdbserver program (about 400 KB) on your target that allows to debug target
applications, using a standard gdb debugger on the development host, or a graphical IDE such as VS
Code.
• It is fine to strip applications and binaries on the target machine, as long as the programs and libraries
with debugging symbols are available on the development host.
• Thanks to core dumps, you can know where a program crashed, without having to reproduce the issue
by running the program through the debugger.

Going further: packaging your application with Meson


Now that our application is ready, the next thing to do is to properly integrate it into our root filesystem.
This is a nice opportunity to see how to do this with Meson and leverage Buildroot’s infrastructure to
cross-compile Meson based packages.
Still in the main appdev directory, create a nunchuk-mpd-client-1.0 directory and copy the nunchuk-mpd-
client.c file to it.
In this new directory, all you have to do is create a very simple meson.build file:
project('nunchuk-mpd-client', 'c', version: '1.0')
libmpdclient_dep = dependency('libmpdclient', version: '>= 2.16')
executable('nunchuk-mpd-client', 'nunchuk-mpd-client.c',
dependencies: libmpdclient_dep, install: true)
Note that install: true is necessary to get the executable installed by ninja install.
Now, the next thing is to add a new package to the Buildroot source tree:

70 © 2004-2024 Bootlin, CC BY-SA license


Embedded Linux System Development

• Create a nunchuk-mpd-client directory under package.


• In this directory, create a Config.in file. You can reuse the one from the mpd-mpc package (the mpc
client) which also depends on libmpdclient.
• Modify package/Config.in to source this new file in the Audio and video applications submenu.
• Last but not least, create the nunchuk-mpd-client.mk file with the following contents:
################################################################################
#
# nunchuk-mpd-client
#
################################################################################

NUNCHUK_MPD_CLIENT_VERSION = 1.0
NUNCHUK_MPD_CLIENT_SITE = $(HOME)/embedded-linux-labs/appdev/nunchuk-mpd-client-1.0
NUNCHUK_MPD_CLIENT_SITE_METHOD = local
NUNCHUK_MPD_CLIENT_DEPENDENCIES = host-pkgconf libmpdclient

$(eval $(meson-package))
All you have to do now is to enable the nunchuk-mpd-client package in Buildroot’s configuration, run make,
update the root filesystem and check on the target that /usr/bin/nunchuk-mpd-client exists and runs fine.
All this was pretty straightforward, wasn’t it? Meson rocks!
Congratulations, you’ve reached the end of all our labs. Try to look back, and see how much experience
you’ve gained in these last days.

© 2004-2024 Bootlin, CC BY-SA license 71

You might also like