Mastering Embedded Linux Programming: Configuring and Building the Kernel (3/16)



Configuring and Building the Kernel

The kernel is the third element of embedded Linux. It is the component that is responsible for managing resources and interfacing with hardware, and so affects almost every aspect of your final software build. It is usually tailored to your particular hardware configuration, although, as we saw in 2, All About Bootloaders, device trees allow you to create a generic kernel that is tailored to particular hardware by the contents of the device tree.

In this , we will look at how to get a kernel for a board, and how to configure and compile it. We will look again at bootstrap, this time focusing on the part the kernel plays. We will also look at device drivers and how they pick up information from the device tree.

In this , we will cover the following topics:

What does the kernel do?

Choosing a kernel.
Building the kernel.
Booting the kernel.
Porting Linux to a new board.

What does the kernel do?


Linux began in 1991, when Linus Torvalds started writing an operating system for Intel 386- and 486-based personal computers. He was inspired by the Minix operating system written by Andrew S. Tanenbaum four years earlier. Linux differed in many ways from Minix; the main differences being that it was a 32-bit virtual memory kernel and the code was open source, later released under the GPL v2 license. He announced it on 25th August, 1991, on the comp.os.minix newsgroup in a famous post that began with:

Hello everybody out there using minix—I'm doing a (free) operating system (just a hobby, won't be big and professional like GNU) for 386(486) AT clones. This has been brewing since April, and is starting to get ready. I'd like any feedback on things people like/dislike in minix, as my OS resembles it somewhat (same physical layout of the filesystem (due to practical reasons) among other things).

To be strictly accurate, Linus did not write an operating system, rather he wrote a kernel, which is only one component of an operating system. To create a complete operating system with user space commands and a shell command interpreter, he used components from the GNU project, especially the toolchain, the C-library, and basic command-line tools. That distinction remains today, and gives Linux a lot of flexibility in the way it is used. It can be combined with a GNU user space to create a full Linux distribution that runs on desktops and servers, which is sometimes called GNU/Linux; it can be combined with an Android user space to create the well-known mobile operating system, or it can be combined with a small BusyBox-based user space to create a compact embedded system. Contrast this with the BSD operating systems, FreeBSD, OpenBSD, and NetBSD, in which the kernel, the toolchain, and the user space are combined into a single code base.

The kernel has three main jobs: to manage resources, to interface with hardware, and to provide an API that offers a useful level of abstraction to user space programs, as summarized in the following diagram:


Applications running in User space run at a low CPU privilege level. They can do very little other than make library calls. The primary interface between the User space and the Kernel space is the C library, which translates user level functions, such as those defined by POSIX, into kernel system calls. The system call interface uses an architecture-specific method, such as a trap or a software interrupt, to switch the CPU from low privilege user mode to high privilege kernel mode, which allows access to all memory addresses and CPU registers.

The System call handler dispatches the call to the appropriate kernel subsystem: memory allocation calls go to the memory manager, filesystem calls to the filesystem code, and so on. Some of those calls require input from the underlying hardware and will be passed down to a device driver. In some cases, the hardware itself invokes a kernel function by raising an interrupt.

The preceding diagram shows that there is a second entry point into kernel code: hardware interrupts. Interrupts can only be handled in a device driver, never by a user space application.

In other words, all the useful things that your application does, it does them through the kernel. The kernel, then, is one of the most important elements in the system.



Choosing a kernel


The next step is to choose the kernel for your project, balancing the desire to always use the latest version of software against the need for vendor-specific additions and an interest in the long term support of the code base.

Kernel development cycle


Linux is developed at a fast pace, with a new version being released every 8 to 12 weeks. The way that the version numbers are constructed has changed a bit in recent years. Before July 2011, there was a three number version scheme with version numbers that looked like 2.6.39. The middle number indicated whether it was a developer or stable release; odd numbers (2.1.x, 2.3.x, 2.5.x) were for developers and even numbers were for end users. From version 2.6 onwards, the idea of a long-lived development branch (the odd numbers) was dropped, as it slowed down the rate at which new features were made available to the users. The change in numbering from 2.6.39 to 3.0 in July 2011 was purely because Linus felt that the numbers were becoming too large; there was no huge leap in the features or architecture of Linux between those two versions. He also took the opportunity to drop the middle number. Since then, in April 2015, he bumped the major from 3 to 4, again purely for neatness, not because of any large architectural shift.

Linus manages the development kernel tree. You can follow him by cloning the

Git tree like so:

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

This will check out into subdirectory linux. You can keep up to date by running the command git pull in that directory from time to time.

Currently, a full cycle of kernel development begins with a merge window of two weeks, during which Linus will accept patches for new features. At the end of the merge window, a stabilization phase begins, during which Linus will produce weekly release candidates with version numbers ending in -rc1, -rc2, and so on, usually up to -rc7 or -rc8. During this time, people test the candidates and submit bug reports and fixes. When all significant bugs have been fixed, the kernel is released.

The code incorporated during the merge window has to be fairly mature already. Usually, it is pulled from the repositories of the many subsystem and architecture maintainers of the kernel. By keeping to a short development cycle, features can


be merged when they are ready. If a feature is deemed not sufficiently stable or well developed by the kernel maintainers, it can simply be delayed until the next release.

Keeping a track of what has changed from release to release is not easy. You can read the commit log in Linus' Git repository but, with roughly 10,000 or more entries, it is not easy to get an overview. Thankfully, there is the Linux Kernel Newbies website, http://kernelnewbies.org, where you will find a succinct overview of each version at http://kernelnewbies.org/LinuxVersions.



Stable and long term support releases


The rapid rate of change of Linux is a good thing in that it brings new features into the mainline code base, but it does not fit very well with the longer life cycle of embedded projects. Kernel developers address this in two ways, with stable releases and long term releases. After the release of a mainline kernel (maintained by Linus Torvalds) it is moved to the stable tree (maintained by Greg Kroah-Hartman). Bug fixes are applied to the stable kernel, while the mainline kernel begins the next development cycle. Point releases of the stable kernel are marked by a third number, 3.18.1, 3.18.2, and so on. Before version 3, there were four release numbers, 2.6.29.1, 2.6.39.2, and so on.

You can get the stable tree by using the following command:

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git

You can use git checkout to get a particular version, for example version 4.9.13:

$ cd linux-stable

$ git checkout v4.9.13


Usually, the stable kernel is updated only until the next mainline release (8 to 12 weeks later), so you will see that there is just one or sometimes two stable kernels at https://www.kernel.org/. To cater for those users who would like updates for a longer period of time and be assured that any bugs will be found and fixed, some kernels are labeled long term and maintained for two or more years. There is at least one long term kernel release each year. Looking at https://www.kernel.org/ at the time of writing, there are a total of nine long term kernels: 4.9, 4.4, 4.1, 3.18, 3.14, 3.12, 3.10, 3.4, and 3.2. The latter has been maintained for five years and is at version 3.2.86. If you are building a product that you will have to maintain for this length of time, then the latest long term kernel might well be a good choice.



Vendor support


In an ideal world, you would be able to download a kernel from https://www.kernel.or g/ and configure it for any device that claims to support Linux. However, that is not always possible; in fact mainline Linux has solid support for only a small subset of the many devices that can run Linux. You may find support for your board or SoC from independent open source projects, Linaro or the Yocto Project, for example, or from companies providing third party support for embedded Linux, but in many cases you will be obliged to look to the vendor of your SoC or board for a working kernel. As we know, some are better at supporting Linux than others. My only advice at this point is to choose vendors who give good support or who, even better, take the trouble to get their kernel changes into the mainline.



Licensing


The Linux source code is licensed under GPL v2, which means that you must make the source code of your kernel available in one of the ways specified in the license.

The actual text of the license for the kernel is in the file COPYING. It begins with an addendum written by Linus that states that code calling the kernel from user space via the system call interface is not considered a derivative work of the kernel and so is not covered by the license. Hence, there is no problem with proprietary applications running on top of Linux.

However, there is one area of Linux licensing that causes endless confusion and debate: kernel modules. A kernel module is simply a piece of code that is dynamically linked with the kernel at runtime, thereby extending the functionality of the kernel. The GPL makes no distinction between static and dynamic linking, so it would appear that the source for kernel modules is covered by the GPL. But, in the early days of Linux, there were debates about exceptions to this rule, for example, in connection with the Andrew filesystem. This code predates Linux and therefore (it was argued) is not a derivative work, and so the license does not apply. Similar discussions took place over the years with respect to other pieces of code, with the result that it is now accepted practice that the GPL does not necessarily apply to kernel modules. This is codified by the kernel MODULE_LICENSE macro, which may take the value Proprietary to indicate that it is not released under the GPL. If you plan to use the same arguments yourself, you may want to read though an oft-quoted e-mail thread titled "Linux GPL and binary module exception clause?" which is archived at http ://yarchive.net/comp/linux/gpl_modules.html.

The GPL should be considered a good thing because it guarantees that when you and I are working on embedded projects, we can always get the source code for the kernel. Without it, embedded Linux would be much harder to use and more fragmented.



Building the kernel


Having decided which kernel to base your build on, the next step is to build it.



Getting the source


Both of the targets used in this , the BeagleBone Black and the ARM Versatile PB, are well supported by the mainline kernel. Therefore, it makes sense to use the latest long-term kernel available from https://www.kernel.org/, which at the time of writing was 4.9.13. When you come to do this for yourself, you should check to see if there is a later version of the 4.9 kernel and use that instead since it will have fixes for bugs found after 4.9.13 was released. If there is a later long-term release, you may want to consider using that one, but be aware that there may have been changes that mean that the following sequence of commands do not work exactly as given.

Use this command to clone the stable kernel and check out version 4.9.13:

$   git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git

$   cd linux-stable

$   git checkout v4.9.13



There is a lot of code here. There are over 57,000 files in the 4.9 kernel containing C-source code, header files, and assembly code, amounting to a total of over 14 million lines of code, as measured by the SLOCCount utility. Nevertheless, it is worth knowing the basic layout of the code and to know, approximately, where to look for a particular component. The main directories of interest are:

arch: Contains architecture-specific files. There is one subdirectory per architecture.
Documentation: Contains kernel documentation. Always look here first if you want to find more information about an aspect of Linux.
drivers: Contains device drivers, thousands of them. There is a subdirectory for each type of driver.
fs: Contains filesystem code.

include: Contains kernel header files, including those required when building the toolchain.


init: Contains the kernel start-up code.

kernel: Contains core functions, including scheduling, locking, timers, power management, and debug/trace code. mm: Contains memory management.
net: Contains network protocols.

scripts: Contains many useful scripts, including the device tree compiler, DTC, which I described in  3, All About Bootloaders.
tools: Contains many useful tools and including the Linux performance counters tool, perf, which I will describe in  15, Profiling and Tracing.

Over a period of time, you will become familiar with this structure, and realize that if you are looking for the code for the serial port of a particular SoC, you will find it in drivers/tty/serial and not in arch/$ARCH/mach-foo, because it is a device driver and not something central to the running of Linux on that SoC.



Understanding kernel configuration – Kconfig

One of the strengths of Linux is the degree to which you can configure the kernel to suit different jobs, from a small dedicated device such as a smart thermostat to a complex mobile handset. In current versions, there are many thousands of configuration options. Getting the configuration right is a task in itself but, before that, I want to show you how it works so that you can better understand what is going on.

The configuration mechanism is called Kconfig, and the build system that it integrates with is called Kbuild. Both are documented in Documentation/kbuild. Kconfig/Kbuild is used in a number of other projects as well as the kernel, including Crosstool-NG, U-Boot, Barebox, and BusyBox.

The configuration options are declared in a hierarchy of files named Kconfig,

using a syntax described in Documentation/kbuild/kconfig-language.txt. In Linux, the

top level Kconfig looks like this:

mainmenu "Linux/$ARCH $KERNELVERSION Kernel Configuration"

config SRCARCH

string

option env="SRCARCH"

source "arch/$SRCARCH/Kconfig"

The last line includes the architecture-dependent configuration file which sources other Kconfig files, depending on which options are enabled. Having the architecture play such a role has two implications: firstly, that you must specify an architecture when configuring Linux by setting ARCH=[architecture], otherwise it will default to the local machine architecture, and second, that the layout of the top level menu is different for each architecture.

The value you put into ARCH is one of the subdirectories you find in directory arch,

with the oddity that ARCH=i386 and ARCH=x86_64 both source arch/x86/Kconfig.

The Kconfig files consist largely of menus, delineated by menu and endmenu


keywords. Menu items are marked by the keyword config. Here is an example,

taken from drivers/char/Kconfig:

menu "Character devices"

[...]
config DEVMEM

bool "/dev/mem virtual device support"
default y

help

Say Y here if you want to support the /dev/mem device. The /dev/mem device is used to access areas of physical memory.

When in doubt, say "Y".

[...]

endmenu

The parameter following config names a variable that, in this case, is DEVMEM. Since this option is a bool (Boolean), it can only have two values: if it is enabled, it is assigned to y, if it is not enabled, the variable is not defined at all. The name of the menu item that is displayed on the screen is the string following the bool keyword.

This configuration item, along with all the others, is stored in a file named .config (note that the leading dot (.) means that it is a hidden file that will not be shown by the ls command, unless you type ls -a to show all the files). The line corresponding to this configuration item reads:

CONFIG_DEVMEM=y

There are several other data types in addition to bool. Here is the list:

bool: Either y or not defined.

tristate: Used where a feature can be built as a kernel module or built into the main kernel image. The values are m for a module, y to be built in, and not defined if the feature is not enabled.
int: An integer value using decimal notation.

hex: An unsigned integer value using hexadecimal notation.

string: A string value.

There may be dependencies between items, expressed by the depends on construct, as shown here:

config MTD_CMDLINE_PARTS

tristate "Command line partition table parsing"

depends on MTD


If CONFIG_MTD has not been enabled elsewhere, this menu option is not shown and so cannot be selected.

There are also reverse dependencies; the select keyword enables other options if this one is enabled. The Kconfig file in arch/$ARCH has a large number of select statements that enable features specific to the architecture, as can be seen here for ARM:

config ARM

bool

default y

select ARCH_CLOCKSOURCE_DATA

select ARCH_HAS_DEVMEM_IS_ALLOWED

[...]

There are several configuration utilities that can read the Kconfig files and produce a .config file. Some of them display the menus on screen and allow you to make choices interactively. menuconfig is probably the one most people are familiar with, but there are also xconfig and gconfig.

You launch each one via the make command, remembering that, in the case of the kernel, you have to supply an architecture, as illustrated here:

$ make ARCH=arm menuconfig


Here, you can see menuconfig with the DEVMEM config option highlighted in the previous paragraph:


The star (*) to the left of an item means that it is selected (Y) or, if it is an M,

that it has been selected to be built as a kernel module.

You often see instructions like enable CONFIG_BLK_DEV_INITRD, but with so many menus to browse through, it can take a while to find the place where that configuration is set. All configuration editors have a search function. You can access it in menuconfig by pressing the forward slash key, /. In xconfig, it is in the edit menu, but make sure you miss off CONFIG_ part of the configuration item you are searching for.

With so many things to configure, it is unreasonable to start with a clean sheet each time you want to build a kernel, so there are a set of known working configuration files in arch/$ARCH/configs, each containing suitable configuration values for a single SoC or a group of SoCs.

You can select one with the make [configuration file name] command. For example, to configure Linux to run on a wide range of SoCs using the ARMv7-A architecture, you would type:

$ make ARCH=arm multi_v7_defconfig

This is a generic kernel that runs on various different boards. For a more specialized application, for example, when using a vendor-supplied kernel, the default configuration file is part of the board support package; you will need to find out which one to use before you can build the kernel.

There is another useful configuration target named oldconfig. This takes an existing .config file and asks you to supply configuration values for any options that don't have them. You would use it when moving a configuration to a newer kernel version; copy .config from the old kernel to the new source directory and run the make ARCH=arm oldconfig command to bring it up to date. It can also be used to validate a .config file that you have edited manually (ignoring the text ""Automatically generated file; DO NOT EDIT"" that occurs at the top; sometimes it is OK to ignore warnings).

If you do make changes to the configuration, the modified .config file becomes part of your board support package and needs to be placed under source code control.

When you start the kernel build, a header file, include/generated/autoconf.h, is generated, which contains #define for each configuration value so that it can be included in the kernel source.


Using LOCALVERSION to identify your kernel

You can discover the kernel version that you have built using the make

kernelversion target:

$   make ARCH=arm kernelversion

4.9.13

This is reported at runtime through the uname command, and is also used in naming the directory where kernel modules are stored.

If you change the configuration from the default, it is advisable to append your own version information, which you can configure by setting CONFIG_LOCALVERSION. As an example, if I wanted to mark the kernel I am building with the identifier melp and version 1.0, I would define the local version in menuconfig like this:


Running make kernelversion produces the same output as before, but now if I run

make kernelrelease, I see:

$   make ARCH=arm kernelrelease

4.9.13-melp-v1.0


Kernel modules


I have mentioned kernel modules several times already. Desktop Linux distributions use them extensively so that the correct device and kernel functions can be loaded at runtime, depending on the hardware detected and features required. Without them, every single driver and feature would have to be statically linked in to the kernel, making it infeasibly large.

On the other hand, with embedded devices, the hardware and kernel configuration is usually known at the time the kernel is built, and therefore modules are not so useful. In fact, they cause a problem because they create a version dependency between the kernel and the root filesystem, which can cause boot failures if one is updated but not the other. Consequently, it is quite common for embedded kernels to be built without any modules at all.

Here are a few cases where kernel modules are a good idea in embedded systems:

When you have proprietary modules, for the licensing reasons given in the preceding section.
To reduce boot time by deferring the loading of non-essential drivers. When there are a number of drivers that could be loaded and it would take up too much memory to compile them statically. For example, you have a USB interface that supports a range of devices. This is essentially the same argument as is used in desktop distributions.


Compiling – Kbuild


The kernel build system, Kbuild, is a set of make scripts that take the configuration information from the .config file, work out the dependencies, and compile everything that is necessary to produce a kernel image containing all the statically linked components, possibly a device tree binary and possibly one or more kernel modules. The dependencies are expressed in makefiles that are in each directory with buildable components. For instance, the following two lines are taken from drivers/char/Makefile:

obj-y += mem.o random.o

obj-$(CONFIG_TTY_PRINTK) += ttyprintk.o

The obj-y rule unconditionally compiles a file to produce the target, so mem.c and random.c are always part of the kernel. In the second line, ttyprintk.c is dependent on a configuration parameter. If CONFIG_TTY_PRINTK is y, it is compiled as a built-in; if it is m, it is built as a module; and if the parameter is undefined, it is not compiled at all.

For most targets, just typing make (with the appropriate ARCH and CROSS_COMPILE) will do the job, but it is instructive to take it one step at a time.


Finding out which kernel target to build

To build a kernel image, you need to know what your bootloader expects. This is a rough guide:

U-Boot: Traditionally, U-Boot has required uImage, but newer versions can load a zImage file using the bootz command x86 targets: Requires a bzImage file
 Most other bootloaders: Require a zImage file

Here is an example of building a zImage file:

$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- zImage

The -j 4 option tells make how many jobs to run in parallel, which reduces the time taken to build. A rough guide is to run as many jobs as you have CPU cores.

There is a small issue with building a uImage file for ARM with multi-platform support, which is the norm for the current generation of ARM SoC kernels. Multi-platform support for ARM was introduced in Linux 3.7. It allows a single kernel binary to run on multiple platforms and is a step on the road toward having a small number of kernels for all ARM devices. The kernel selects the correct platform by reading the machine number or the device tree passed to it by the bootloader. The problem occurs because the location of physical memory might be different for each platform, and so the relocation address for the kernel (usually 0x8000 bytes from the start of physical RAM) might also be different. The relocation address is coded into the uImage header by the mkimage command when the kernel is built, but it will fail if there is more than one relocation address to choose from. To put it another way, the uImage format is not compatible with multi-platform images. You can still create a uImage binary from a multi-platform build, so long as you give the LOADADDR of the particular SoC you are hoping to boot this kernel on. You can find the load address by looking in mach - [your SoC]/Makefile.boot and noting the value of zreladdr-y:

$   make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-LOADADDR=0x80008000 uImage


Build artifacts


A kernel build generates two files in the top level directory: vmlinux and System.map. The first, vmlinux, is the kernel as an ELF binary. If you have compiled your kernel with debug enabled (CONFIG_DEBUG_INFO=y), it will contain debug symbols which can be used with debuggers like kgdb. You can also use other ELF binary tools, such as size:

$ arm-cortex_a8-linux-gnueabihf-size vmlinux

text data bss dec hex filename 10605896 5291748 351864 16249508 f7f2a4 vmlinux

System.map contains the symbol table in a human readable form.

Most bootloaders cannot handle ELF code directly. There is a further stage of processing which takes vmlinux and places those binaries in arch/$ARCH/boot that are suitable for the various bootloaders:

Image: vmlinux converted to raw binary format.

zImage: For the PowerPC architecture, this is just a compressed version of Image, which implies that the bootloader must do the decompression. For all other architectures, the compressed Image is piggybacked onto a stub of code that decompresses and relocates it.
uImage: zImage plus a 64-byte U-Boot header.

While the build is running, you will see a summary of the commands being executed:

$   make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ zImage

CHK include/config/kernel.release
CHK include/generated/uapi/linux/version.h HOSTCC scripts/basic/fixdep
HOSTCC scripts/kallsyms HOSTCC scripts/dtc/dtc.o [...]


Sometimes, when the kernel build fails, it is useful to see the actual commands being executed. To do that, add V=1 to the command line:

$   make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ V=1 zImage

[...]

arm-cortex_a8-linux-gnueabihf-gcc -Wp,-MD,arch/arm/kernel/.irq.o.d -nostdinc -isyste [...]


Compiling device trees


The next step is to build the device tree, or trees if you have a multi-platform

build. The dtbs target builds device trees according to the rules in

arch/$ARCH/boot/dts/Makefile, using the device tree source files in that directory.

Following is a snippet from building the dtbs target for multi_v7_defconfig:

$   make ARCH=arm dtbs [...]

DTC arch/arm/boot/dts/alpine-db.dtb

DTC arch/arm/boot/dts/artpec6-devboard.dtb DTC arch/arm/boot/dts/at91-kizbox2.dtb
DTC arch/arm/boot/dts/at91-sama5d2_xplained.dtb DTC arch/arm/boot/dts/at91-sama5d3_xplained.dtb DTC arch/arm/boot/dts/sama5d31ek.dtb

[...]

The compiled .dtb files are generated in the same directory as the sources.


Compiling modules


If you have configured some features to be built as modules, you can build them separately using the modules target:

$   make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ modules


The compiled modules have a .ko suffix and are generated in the same directory as the source code, meaning that they are scattered all around the kernel source tree. Finding them is a little tricky, but you can use the modules_install make target to install them in the right place. The default location is /lib/modules in your development system, which is almost certainly not what you want. To install them into the staging area of your root filesystem (we will talk about root filesystems in the next ), provide the path using INSTALL_MOD_PATH:

$ make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ INSTALL_MOD_PATH=$HOME/rootfs modules_install

Kernel modules are put into the directory /lib/modules/[kernel version], relative to the root of the filesystem.


Cleaning kernel sources


There are three make targets for cleaning the kernel source tree:

clean: Removes object files and most intermediates.

mrproper: Removes all intermediate files, including the .config file. Use this target to return the source tree to the state it was in immediately after cloning or extracting the source code. If you are curious about the name, Mr Proper is a cleaning product common in some parts of the world. The meaning of make mrproper is to give the kernel sources a really good scrub. distclean: This is the same as mrproper, but also deletes editor backup files, patch files, and other artifacts of software development.


Building a kernel for the BeagleBone Black

In light of the information already given, here is the complete sequence of commands to build a kernel, the modules, and a device tree for the BeagleBone Black, using the Crosstool-NG ARM Cortex A8 cross compiler:

$ cd linux-stable

$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- mrproper

$   make ARCH=arm multi_v7_defconfig

$   make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- zImage

$   make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- modules

$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- dtbs


These commands are in the script MELP/_04/build-linux-bbb.sh .


Building a kernel for QEMU


Here is the sequence of commands to build Linux for the ARM Versatile PB that is emulated by QEMU, using the Crosstool-NG V5te compiler:

$ cd linux-stable

$   make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- mrproper

$   make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- zImage

$   make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- modules

$   make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- dtbs

These commands are in the script MELP/_04/build-linux-versatilepb.sh.


Booting the kernel


Booting Linux is highly device-dependent. In this section, I will show you how it works for the BeagleBone Black and QEMU. For other target boards, you must consult the information from the vendor or from the community project, if there is one.

At this point, you should have the zImage file and the dtbs targets for the BeagleBone Black or QEMU.


Booting the BeagleBone Black


To begin, you need a microSD card with U-Boot installed, as described in the section Installing U-Boot. Plug the microSD card into your card reader and from the linux-stable directory the files arch/arm/boot/zImage and arch/arm/boot/dts/am335x-
boneblack.dtb to the boot partition. Unmount the card and plug it into the BeagleBone Black. Start a terminal emulator, such as gtkterm, and be prepared to press the space bar as soon as you see the U-Boot messages appear. Next, power on the BeagleBone Black and press the space bar. You should get a U-Boot prompt, . Now enter the following commands to load Linux and the device tree binary:

U-Boot# fatload mmc 0:1 0x80200000 zImage

reading zImage
7062472 bytes read in 447 ms (15.1 MiB/s)

U-Boot# fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb

reading am335x-boneblack.dtb
34184 bytes read in 10 ms (3.3 MiB/s)

U-Boot# setenv bootargs console=ttyO0

U-Boot# bootz 0x80200000 - 0x80f00000

##   Flattened Device Tree blob at 80f00000 Booting using the fdt blob at 0x80f00000

Loading Device Tree to 8fff4000, end 8ffff587 ... OK

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0 [...]


Note that we set the kernel command line to console=ttyO0. That tells Linux which device to use for console output, which in this case is the first UART on the board, device ttyO0. Without this, we would not see any messages after Starting the kernel..., and therefore would not know if it was working or not. The sequence will end in a kernel panic, for reasons I will explain later on.


Booting QEMU


Assuming that you have already installed qemu-system-arm, you can launch with the kernel and the .dtb file for the ARM Versatile PB, as follows:

$ QEMU_AUDIO_DRV=none \

qemu-system-arm -m 256M -nographic -M versatilepb -kernel zImage \

-append "console=ttyAMA0,115200" -dtb versatile-pb.dtb

Note that setting QEMU_AUDIO_DRV to none is just to suppress error messages from QEMU about missing configurations for the audio drivers, which we do not use. As with the BeagleBone Black, this will end with a kernel panic and the system will halt. To exit from QEMU, type Ctrl + A and then x (two separate keystrokes).


Kernel panic


While things started off well, they ended badly:

[    1.886379] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0

[    1.895105] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknow

This is a good example of a kernel panic. A panic occurs when the kernel encounters an unrecoverable error. By default, it will print out a message to the console and then halt. You can set the panic command-line parameter to allow a few seconds before reboots following a panic. In this case, the unrecoverable error is no root filesystem, illustrating that a kernel is useless without a user space to control it. You can supply a user space by providing a root filesystem, either as a ramdisk or on a mountable mass storage device. We will talk about how to create a root filesystem in the next , but first I want to describe the sequence of events that leads up to panic.


Early user space


In order to transition from kernel initialization to user space, the kernel has to mount a root filesystem and execute a program in that root filesystem. This can be achieved via a ramdisk or by mounting a real filesystem on a block device. The code for all of this is in init/main.c, starting with the function rest_init(), which creates the first thread with PID 1 and runs the code in kernel_init(). If there is a ramdisk, it will try to execute the program /init, which will take on the task of setting up the user space.

If fails to find and run /init, it tries to mount a filesystem by calling the function

prepare_namespace() in init/do_mounts.c. This requires a root= command line to give

the name of the block device to use for mounting, usually in the form:

root=/dev/<disk name><partition number>

Or, for SD cards and eMMC:

root=/dev/<disk name>p<partition number>

For example, for the first partition on an SD card, that would be root=/dev/mmcblk0p1. If the mount succeeds, it will try to execute /sbin/init, followed by /etc/init, /bin/init, and then /bin/sh, stopping at the first one that works.

The program can be overridden on the command line. For a ramdisk, use rdinit=, and for a filesystem, use init=.


Kernel messages


Kernel developers are fond of printing out useful information through liberal use of printk() and similar functions. The messages are categorized according to importance, with 0 being the highest:

Level
Value
Meaning



KERN_EMERG
0
The system is unusable



KERN_ALERT
1
Action must be taken immediately



KERN_CRIT
2
Critical conditions



KERN_ERR
3
Error conditions



KERN_WARNING
4
Warning conditions



KERN_NOTICE
5
Normal but significant conditions



KERN_INFO
6
Informational



KERN_DEBUG
7
Debug-level messages







They are first written to a buffer, __log_buf, the size of which is two to the power

of CONFIG_LOG_BUF_SHIFT. For example, if CONFIG_LOG_BUF_SHIFT is 16, then __log_buf is 64

KiB. You can dump the entire buffer using the command dmesg.

If the level of a message is less than the console log level, it is displayed on the console as well as placed in __log_buf. The default console log level is 7, meaning that messages of level 6 and lower are displayed, filtering out KERN_DEBUG, which is level 7. You can change the console log level in several ways, including by using the kernel parameter loglevel=<level>, or the command dmesg -n <level>.


Kernel command line


The kernel command line is a string that is passed to the kernel by the bootloader, via the bootargs variable in the case of U-Boot; it can also be defined in the device tree, or set as part of the kernel configuration in CONFIG_CMDLINE.

We have seen some examples of the kernel command line already, but there are many more. There is a complete list in Documentation/kernel-parameters.txt. Here is a smaller list of the most useful ones:

Name
Description


debug
Sets the console log level to the highest level, 8, to ensure that you
see all the kernel messages on the console.



init=
The init program to run from a mounted root filesystem, which
defaults to /sbin/init.



lpj=
Sets loops_per_jiffy to a given constant. There is a description of the
significance of this in the paragraph following this table.




Behavior when the kernel panics: if it is greater than zero, it gives
panic=
the number of seconds before rebooting; if it is zero, it waits
forever (this is the default); or if it is less than zero, it reboots


without any delay.



Sets the console log level to , suppressing all but emergency
quiet
messages. Since most devices have a serial console, it takes time
to output all those strings. Consequently, reducing the number of


messages using this option reduces boot time.


rdinit=
The init program to run from a ramdisk. It defaults to /init.


ro
Mounts the root device as read-only. Has no effect on a ramdisk,
which is always read/write.



root=
Device to mount the root filesystem.



The number of seconds to wait before trying to mount the root
rootdelay=
device; defaults to zero. Useful if the device takes time to probe

the hardware, but also see rootwait.



The filesystem type for the root device. In many cases, it is auto-rootfstype= detected during mount, but it is required for jffs2 filesystems.

Waits indefinitely for the root device to be detected. Usually
rootwait
necessary with mmc devices.

rw
Mounts the root device as read-write (default).




The lpj parameter is often mentioned in connection with reducing the kernel boot time. During initialization, the kernel loops for approximately 250 ms to calibrate a delay loop. The value is stored in the variable loops_per_jiffy, and reported like this:

Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736)

If the kernel always runs on the same hardware, it will always calculate the same value. You can shave 250 ms off the boot time by adding lpj=4980736 to the command line.


Porting Linux to a new board


Porting Linux to a new board can be easy or difficult, depending on how similar your board is to an existing development board. In  3, All About Bootloaders, we ported U-Boot to a new board, named Nova, which is based on the BeagleBone Black. very few changes to be made to the kernel code and so it very easy. If you are porting to completely new and innovative hardware, there will be more to do. I am only going to consider the simple case.

The organization of architecture-specific code in arch/$ARCH differs from one system to another. The x86 architecture is pretty clean because most hardware details are detected at runtime. The PowerPC architecture puts SoC and board-specific files into sub directory platforms. The ARM architecture, on the other hand, is quite messy, in part because there is a lot of variability between the many ARM-based SoCs. Platform-dependent code is put in directories named mach-*, approximately one per SoC. There are other directories named plat-* which contain code common to several versions of an SoC. In the case of the BeagleBone Black, the relevant directory is arch/arm/mach-omap2. Don't be fooled by the name though; it contains support for OMAP2, 3, and 4 chips, as well as the AM33xx family of chips that the BeagleBone uses.

In the following sections, I am going to explain how to create a device tree for a new board and how to key that into the initialization code of Linux.


A new device tree


The first thing to do is create a device tree for the board and modify it to describe the additional or changed hardware of the Nova board. In this simple case, we will just copy am335x-boneblack.dts to nova.dts and change the board name in nova.dts, as shown highlighted here:

/dts-v1/;

#include "am33xx.dtsi"

#include "am335x-bone-common.dtsi"

#include <dt-bindings/display/tda998x.h>

/ {

model = "Nova";

compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};

[...]

We can build the Nova device tree binary explicitly like this:

$ make ARCH=arm nova.dtb

If we want the device tree for Nova to be compiled by make ARCH=arm dtbs whenever an AM33xx target is selected, we could add a dependency in

arch/arm/boot/dts/Makefile as follows:

[...]

dtb-$(CONFIG_SOC_AM33XX) +=

nova.dtb

[...

We can see the effect of using the Nova device tree by booting the BeagleBone Black, following the same procedure as in the section Booting the BeagleBone Black, with the same zImage file as before, but loading nova.dtb in place of am335x-boneblack.dtb. The following highlighted output is the point at which the machine model is printed out:




Starting kernel ...

[ 0.000000] Booting Linux on physical CPU 0x0

[    0.000000] Linux version 4.9.13-melp-v1.0-dirty (chris@chris-xps) (gcc version 5.2.0 (

[    0.000000] CPU: ARMv7 Processor [413fc082] revision 2 (ARMv7), cr=10c5387d

[    0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache

[    0.000000] OF: fdt:Machine model: Nova [...]


Now that we have a device tree specifically for the Nova board, we could modify it to describe the hardware differences between Nova and the BeagleBone Black. There are quite likely to be changes to the kernel configuration as well, in which case you would create a custom configuration file based on a copy of arch/arm/configs/multi_v7_defconfig.


Setting the board compatible property

Creating a new device tree means that we can describe the hardware on the Nova board, selecting device drivers and setting properties to match. But, suppose the Nova board needs different early initialization code than the BeagleBone Black; how can we link that in?

The board setup is controlled by the compatible property in the root node. This is what we have for the Nova board at the moment:

/ {

model = "Nova";

compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

};

When the kernel parses this node, it will search for a matching machine for each of the values of the compatible property, starting on the left and stopping with the first match found. Each machine is defined in a structure delimited by

DT_MACHINE_START and MACHINE_END macros. In arch/arm/mach-omap2/board-generic.c, we

find:

#ifdef CONFIG_SOC_AM33XX

static const char *const am33xx_boards_compat[] __initconst = { "ti,am33xx",

NULL,

};

DT_MACHINE_START(AM33XX_DT, "Generic AM33XX (Flattened Device Tree)")

.reserve = omap_reserve,

.map_io = am33xx_map_io,

.init_early = am33xx_init_early,

.init_machine = omap_generic_init,

.init_late = am33xx_init_late,

.init_time = omap3_gptimer_timer_init,

.dt_compat = am33xx_boards_compat,

.restart = am33xx_restart,
MACHINE_END

#endif

Note that the string array, am33xx_boards_compat , contains "ti,am33xx" which matches one of the machines listed in the compatible property. In fact, it is the only match possible, since there are none for ti,am335x-bone-black or ti,am335x-bone. The

structure between DT_MACHINE_START and MACHINE_END contains a pointer to the string array, and function pointers for the board setup functions. You may wonder why bother with ti,am335x-bone-black and ti,am335x-bone if they never match anything? The answer is partly that they are place holders for the future, but also that there are places in the kernel that contain runtime tests for the machine using the

function of_machine_is_compatible(). For example, in drivers/net/ethernet/ti/cpsw-common.c:


int ti_cm_get_macid(struct device *dev, int slave, u8 *mac_addr)

{

[...]

if (of_machine_is_compatible("ti,am33xx"))

return cpsw_am33xx_cm_get_macid(dev, 0x630, slave, mac_addr);
[...]

Thus, we have to look through not just the mach-* directories but the entire kernel source code to get a list of all the places that depend on the machine compatible property. In the 4.9 kernel, you will find that there are still no checks for

ti,am335x-bone-black and ti,am335x-bone, but there may be in the future.

Returning to the Nova board, if we want to add machine specific setup, we can

add a machine in arch/arm/mach-omap2/board-generic.c, like this:

#ifdef CONFIG_SOC_AM33XX

[...]

static const char *const nova_compat[] __initconst = { "ti,nova",

NULL,

};

DT_MACHINE_START(NOVA_DT, "Nova board (Flattened Device Tree)")

.reserve = omap_reserve,

.map_io = am33xx_map_io,

.init_early = am33xx_init_early,

.init_machine = omap_generic_init,

.init_late = am33xx_init_late,

.init_time = omap3_gptimer_timer_init,

.dt_compat = nova_compat,

.restart = am33xx_restart,
MACHINE_END

#endif

Then we could change the device tree root node like this:

/ {

model = "Nova";

compatible = "ti,nova", "ti,am33xx";

};

Now, the machine will match ti,nova in board-generic.c. We keep ti,am33xx because

we want the runtime tests, such as the one in drivers/net/ethernet/ti/cpsw-common.c, to continue to work.

TLDR;

Linux is a very powerful and complex operating system kernel that can be married to various types of user space, ranging from a simple embedded device, through increasingly complex mobile devices using Android, to a full server operating system. One of its strengths is the degree of configurability. The definitive place to get the source code is https://www.kernel.org/, but you will probably need to get the source for a particular SoC or board from the vendor of that device or a third-party that supports that device. The customization of the kernel for a particular target may consist of changes to the core kernel code, additional drivers for devices that are not in mainline Linux, a default kernel configuration file, and a device tree source file.

Normally, you start with the default configuration for your target board, and then tweak it by running one of the configuration tools such as menuconfig. One of the things you should consider at this point is whether the kernel features and drivers should be compiled as modules or built-in. Kernel modules are usually no great advantage for embedded systems, where the feature set and hardware are usually well defined. However, modules are often used as a way to import proprietary code into the kernel, and also to reduce boot time by loading non-essential drivers after boot.

Building the kernel produces a compressed kernel image file, named zImage , bzImage , or uImage, depending on the bootloader you will be using and the target architecture. A kernel build will also generate any kernel modules (as .ko files) that you have configured, and device tree binaries (as .dtb files) if your target requires them.

Porting Linux to a new target board can be quite simple or very difficult, depending on how different the hardware is from that in the mainline or vendor supplied kernel. If your hardware is based on a well-known reference design, then it may be just a question of making changes to the device tree or to the platform data.

The kernel is the core of a Linux-based system, but it cannot work by itself. It requires a root filesystem that contains the user space components. The root filesystem can be a ramdisk or a filesystem accessed via a block device, which will be the subject of the next . As we have seen, booting a kernel without a root filesystem results in a kernel panic.

Comments

  1. Build the compressed kernel image.. Thanks

    ReplyDelete
  2. Kernel command line was helpful

    ReplyDelete

Post a Comment