Mastering Embedded Linux Programming : All About Bootloaders (2/16)



All About Bootloaders


The bootloader is the second element of embedded Linux. It is the part that starts the system up and loads the operating system kernel. In this , I will look at the role of the bootloader and, in particular, how it passes control from itself to the kernel using a data structure called a device tree, also known as a flattened device tree or FDT. I will cover the basics of device trees, so that you will be able to follow the connections described in a device tree and relate it to real hardware.

I will look at the popular open source bootloader, U-Boot, and show you how to use it to boot a target device, and also how to customize it to run on a new device, using the BeagleBone Black as an example. Finally, I will take a quick look at Barebox, a bootloader that shares its past with U-Boot, but which has, arguably, a cleaner design.

In this , we will cover the following topics:

What does a bootloader do?

The boot sequence.
Booting with UEFI firmware.
Moving from bootloader to kernel.
Introducing device trees.
Choosing a bootloader.
U-Boot.
Barebox.



What does a bootloader do?


In an embedded Linux system, the bootloader has two main jobs: to initialize the system to a basic level and to load the kernel. In fact, the first job is somewhat subsidiary to the second, in that it is only necessary to get as much of the system working as is needed to load the kernel.

When the first lines of the bootloader code are executed, following a power-on or a reset, the system is in a very minimal state. The DRAM controller would not have been set up, and so the main memory would not be accessible. Likewise, other interfaces would not have been configured, so storage accessed via NAND flash controllers, MMC controllers, and so on, would also not be usable. Typically, the only resources operational at the beginning are a single CPU core and some on-chip static memory. As a result, system bootstrap consists of several phases of code, each bringing more of the system into operation. The final act of the bootloader is to load the kernel into RAM and create an execution environment for it. The details of the interface between the bootloader and the kernel are architecture-specific, but in each case it has to do two things. First, bootloader has to pass a pointer to a structure containing information about the hardware configuration, and second it has to pass a pointer to the kernel command line. The kernel command line is a text string that controls the behavior of Linux. Once the kernel has begun executing, the bootloader is no longer needed and all the memory it was using can be reclaimed.

A subsidiary job of the bootloader is to provide a maintenance mode for updating boot configurations, loading new boot images into memory, and, maybe, running diagnostics. This is usually controlled by a simple command-line user interface, commonly over a serial interface.



The boot sequence



In simpler times, some years ago, it was only necessary to place the bootloader in non-volatile memory at the reset vector of the processor. NOR flash memory was common at that time and, since it can be mapped directly into the address space, it was the ideal method of storage. The following diagram shows such a configuration, with the Reset vector at 0xfffffffc at the top end of an area of flash memory. The bootloader is linked so that there is a jump instruction at that location that points to the start of the bootloader code:


From that point, the bootloader code running in NOR flash memory can initialize the DRAM controller, so that the main memory, the DRAM, becomes available and then it copies itself into the DRAM. Once fully operational, the bootloader can load the kernel from flash memory into DRAM and transfer control to it.

However, once you move away from a simple linearly addressable storage medium like NOR flash, the boot sequence becomes a complex, multi-stage procedure. The details are very specific to each SoC, but they generally follow each of the following phases.


Phase 1 – ROM code


In the absence of reliable external memory, the code that runs immediately after a reset or power-on has to be stored on-chip in the SoC; this is known as ROM code. It is loaded into the chip when it is manufactured, and hence the ROM code is proprietary and cannot be replaced by an open source equivalent. Usually, it does not include code to initialize the memory controller, since DRAM configurations are highly device-specific, and so it can only use Static Random Access Memory (SRAM), which does not require a memory controller.

Most embedded SoC designs have a small amount of SRAM on-chip, varying in size from as little as 4 KB to several hundred KB:




The ROM code is capable of loading a small chunk of code from one of several pre-programmed locations into the SRAM. As an example, TI OMAP and Sitara chips try to load code from the first few pages of NAND flash memory, or from flash memory connected through a Serial Peripheral Interface (SPI), or from the first sectors of an MMC device (which could be an eMMC chip or an SD card), or from a file named MLO on the first partition of an MMC device. If reading from all of these memory devices fails, then it tries reading a byte stream from Ethernet, USB, or UART; the latter is provided mainly as a means of loading code into flash memory during production, rather than for use in normal operation. Most embedded SoCs have a ROM code that works in a similar way. In SoCs where the SRAM is not large enough to load a full bootloader like U-Boot, there has to be an intermediate loader called the secondary program loader, or SPL.

At the end of the ROM code phase, the SPL is present in the SRAM and the ROM code jumps to the beginning of that code.


Phase 2 – secondary program loader


The SPL must set up the memory controller and other essential parts of the system preparatory to loading the Tertiary Program Loader (TPL) into DRAM. The functionality of the SPL is limited by the size of the SRAM. It can read a program from a list of storage devices, as can the ROM code, once again using pre-programmed offsets from the start of a flash device. If the SPL has file system drivers built in, it can read well known file names, such as u-boot.img, from a disk partition. The SPL usually doesn't allow for any user interaction, but it may print version information and progress messages, which you can see on the console. The following diagram explains the phase 2 architecture:




The SPL may be open source, as is the case with the TI x-loader and Atmel AT91Bootstrap, but it is quite common for it to contain proprietary code that is supplied by the manufacturer as a binary blob.

At the end of the second phase, the TPL is present in DRAM, and the SPL can make a jump to that area.


Phase 3 – TPL


Now, at last, we are running a full bootloader, such as U-Boot or BareBox. Usually, there is a simple command-line user interface that lets you perform maintenance tasks, such as loading new boot and kernel images into flash storage, and loading and booting a kernel, and there is a way to load the kernel automatically without user intervention.

The following diagram explains the phase 3 architecture:



At the end of the third phase, there is a kernel in memory, waiting to be started.

Embedded bootloaders usually disappear from memory once the kernel is running, and perform no further part in the operation of the system.


Booting with UEFI firmware


Most embedded x86/x86_64 designs, and some ARM designs, have firmware based on the Universal Extensible Firmware Interface (UEFI) standard. You can take a look at the UEFI website at http://www.uefi.org/ for more information. The boot sequence is fundamentally the same as that described in the preceding section:

Phase 1: The processor loads the platform initialization firmware from flash memory. In some designs, it is loaded directly from NOR flash memory, while in others, there is ROM code on-chip which loads the firmware from SPI flash memory into some on-chip static RAM.
Phase 2: The platform initialization firmware performs the role of SPL. It initializes the DRAM controller and other system interfaces, so as to be able to load an EFI boot manager from the EFI System Partition (ESP) on a local disk, or from a network server via PXE boot. The ESP must be formatted using FAT16 or FAT32 format and it should have the well-known GUID value of C12A7328-F81F-11D2-BA4B-00A0C93EC93B. The path name of the boot
manager code must follow the naming convention

<efi_system_partition>/boot/boot<machine_type_short_name>.efi. For example, the

file path to the loader on an x86_64 system would be /efi/boot/bootx64.efi.

Phase 3: The UEFI boot manager is the tertiary program loader. The TPL in this case has to be a bootloader that is capable of loading a Linux kernel and an optional RAM disk into memory. Common choices are:
 Tummiboot: This is the gummiboot with trusted boot support (Intel's Trusted Execution Technology (TEX)).


Moving from bootloader to kernel


When the bootloader passes control to the kernel it has to pass some basic information, which may include some of the following:

The machine number, which is used on PowerPC, and ARM platforms without support for a device tree, to identify the type of the SoC
Basic details of the hardware detected so far, including at least the size and location of the physical RAM, and the CPU clock speed The kernel command line
Optionally, the location and size of a device tree binary
 Optionally, the location and size of an initial RAM disk, called the initial RAM file system (initramfs)

The kernel command line is a plain ASCII string which controls the behavior of Linux by giving, for example, the name of the device that contains the root filesystem. I will look at the details of this in the next . It is common to provide the root filesystem as a RAM disk, in which case it is the responsibility of the bootloader to load the RAM disk image into memory. I will cover the way you create initial RAM disks in  5, Building a Root Filesystem.

The way this information is passed is dependent on the architecture and has changed in recent years. For instance, with PowerPC, the bootloader simply used to pass a pointer to a board information structure, whereas, with ARM, it passed a pointer to a list of A tags. There is a good description of the format of A tags in the kernel source in Documentation/arm/Booting.

In both cases, the amount of information passed was very limited, leaving the bulk of it to be discovered at runtime or hard-coded into the kernel as platform data. The widespread use of platform data meant that each board had to have a kernel configured and modified for that platform. A better way was needed, and that way is the device tree. In the ARM world, the move away from A tags began in earnest in February 2013 with the release of Linux 3.8. Today, almost all ARM systems use device tree to gather information about the specifics of the hardware platform, allowing a single kernel binary to run on a wide range of those platforms
Introducing device trees


If you are working with ARM or PowerPC SoCs, you are almost certainly going to encounter device trees at some point. This section aims to give you a quick overview of what they are and how they work, but there are many details that are not discussed.

A device tree is a flexible way to define the hardware components of a computer system. Usually, the device tree is loaded by the bootloader and passed to the kernel, although it is possible to bundle the device tree with the kernel image itself to cater for bootloaders that are not capable of loading them separately.

The format is derived from a Sun Microsystems bootloader known as OpenBoot, which was formalized as the Open Firmware specification, which is IEEE standard IEEE1275-1994. It was used in PowerPC-based Macintosh computers and so was a logical choice for the PowerPC Linux port. Since then, it has been adopted on a large scale by the many ARM Linux implementations and, to a lesser extent, by MIPS, MicroBlaze, ARC, and other architectures.

I would recommend visiting https://www.devicetree.org/ for more information.


 9,

Device tree basics


The Linux kernel contains a large number of device tree source files in arch/$ARCH/boot/dts, and this is a good starting point for learning about device trees. There are also a smaller number of sources in the U-boot source code in arch/$ARCH/dts. If you acquired your hardware from a third party, the dts file forms part of the board support package and you should expect to receive one along with the other source files.

The device tree represents a computer system as a collection of components joined together in a hierarchy, like a tree. The device tree begins with a root node, represented by a forward slash, /, which contains subsequent nodes representing the hardware of the system. Each node has a name and contains a number of properties in the form name = "value". Here is a simple example:

/dts-v1/;

/{

model = "TI AM335x BeagleBone";

compatible = "ti,am33xx";

#address-cells = <1>;

#size-cells = <1>;

cpus {

#address-cells = <1>;

#size-cells = <0>;

cpu@0 {

compatible = "arm,cortex-a8";

device_type = "cpu";

reg = <0>;

};

};

memory@0x80000000 {

device_type = "memory";

reg = <0x80000000 0x20000000>; /* 512 MB */

};

};

Here we have a root node which contains a cpus node and a memory node. The cpus node contains a single CPU node named cpu@0. It is a common convention that the names of nodes include an @ followed by an address that distinguishes this node from other nodes of the same type.

Both the root and CPU nodes have a compatible property. The Linux kernel uses this property to find a matching device driver by comparing it with the strings exported by each device driver in a structure of_device_id (more on this in

Interfacing with Device Drivers).

It is a convention that the value is composed of a manufacturer name and a component name, to reduce confusion between similar devices made by different manufacturers; hence, ti,am33xx and arm,cortex-a8. It is also quite common to have more than one value for the compatible property where there is more than one driver that can handle this device. They are listed with the most suitable first.

The CPU node and the memory node have a device_type property which describes the class of device. The node name is often derived from device_type.


The reg property


The memory and cpu nodes have a reg property, which refers to a range of units in a register space. A reg property consists of two values representing the start address and the size (length) of the range. Both are written as zero or more 32-bit integers, called cells. Hence, the memory node refers to a single bank of memory that begins at 0x80000000 and is 0x20000000 bytes long.

Understanding reg properties becomes more complex when the address or size values cannot be represented in 32 bits. For example, on a device with 64-bit addressing, you need two cells for each:

/ {

#address-cells = <2>;

#size-cells = <2>;

memory@80000000 {

device_type = "memory";

reg = <0x00000000 0x80000000 0 0x80000000>;

};

};

The information about the number of cells required is held in the #address-cells and #size_cells properties in an ancestor node. In other words, to understand a reg property, you have to look backwards down the node hierarchy until you find #address-cells and #size_cells. If there are none, the default values are 1 for each– but it is bad practice for device tree writers to depend on fall-backs.

Now, let's return to the cpu and cpus nodes. CPUs have addresses as well; in a quad core device, they might be addressed as 0, 1, 2, and 3. That can be thought of as a one-dimensional array without any depth, so the size is zero. Therefore, you can see that we have #address-cells = <1> and #size-cells = <0> in the cpus node,

and in the child node, cpu@0, we assign a single value to the reg property, reg = <0>.


Labels and interrupts


The structure of the device tree described so far assumes that there is a single hierarchy of components, whereas in fact there are several. As well as the obvious data connection between a component and other parts of the system, it might also be connected to an interrupt controller, to a clock source, and to a voltage regulator. To express these connections, we can add a label to a node and reference the label from other nodes. These labels are sometimes referred to as phandles, because when the device tree is compiled, nodes with a reference from another node are assigned a unique numerical value in a property called phandle. You can see them if you decompile the device tree binary.

Take as an example a system containing an LCD controller which can generate

interrupts and an interrupt-controller:

/dts-v1/;

{

intc: interrupt-controller@48200000 {

compatible = "ti,am33xx-intc";
interrupt-controller;

#interrupt-cells = <1>;
reg = <0x48200000 0x1000>;

};

lcdc: lcdc@4830e000 {

compatible = "ti,am33xx-tilcdc";

reg = <0x4830e000 0x1000>;

interrupt-parent = <&intc>;

interrupts = <36>;
ti,hwmods = "lcdc";

status = "disabled";

};

};

Here we have node interrupt-controller@48200000 with the label intc. The interrupt-controller property identifies it as an interrupt controller. Like all interrupt controllers, it has an #interrupt-cells property, which tells us how many cells are needed to represent an interrupt source. In this case, there is only one which represents the interrupt request (IRQ) number. Other interrupt controllers may use additional cells to characterize the interrupt, for example to indicate whether it is edge or level triggered. The number of interrupt cells and their meanings is described in the bindings for each interrupt controller. The device tree bindings can be found in the Linux kernel source, in the directory

Documentation/devicetree/bindings/.

Looking at the lcdc@4830e000 node, it has an interrupt-parent property, which references the interrupt controller it is connected to, using the label. It also has an interrupts property, 36 in this case. Note that this node has its own label, lcdc, which is used elsewhere: any node can have a label.


Device tree include files


A lot of hardware is common between SoCs of the same family and between boards using the same SoC. This is reflected in the device tree by splitting out common sections into include files, usually with the extension .dtsi. The Open Firmware standard defines /include/ as the mechanism to be used, as in this snippet from vexpress-v2p-ca9.dts:

/include/ "vexpress-v2m.dtsi"

Look through the .dts files in the kernel, though, and you will find an alternative include statement that is borrowed from C, for example in am335x-boneblack.dts:

#include "am33xx.dtsi"

#include "am335x-bone-common.dtsi"

Here is another example from am33xx.dtsi:

#include <dt-bindings/gpio/gpio.h>

#include <dt-bindings/pinctrl/am33xx.h>

Lastly, include/dt-bindings/pinctrl/am33xx.h contains normal C macros:

#define PULL_DISABLE (1 << 3)

#define INPUT_EN (1 << 5)

#define SLEWCTRL_SLOW (1 << 6)

#define SLEWCTRL_FAST 0

All of this is resolved if the device tree sources are built using the Kbuild system, which first runs them through the C pre-processor, CPP, where the #include and #define statements are processed into text that is suitable for the device tree compiler. The motivation is illustrated in the previous example; it means that the device tree sources can use the same definitions of constants as the kernel code.

When we include files, using either syntax, the nodes are overlaid on top of one another to create a composite tree in which the outer layers extend or modify the inner ones. For example, am33xx.dtsi, which is general to all am33xx SoCs, defines the first MMC controller interface like this:


mmc1: mmc@48060000 {

compatible = "ti,omap4-hsmmc";

ti,hwmods = "mmc1";

ti,dual-volt;

ti,needs-special-reset;

ti,needs-special-hs-handling;

dmas = <&edma 24 &edma 25>;

dma-names = "tx", "rx";

interrupts = <64>;

interrupt-parent = <&intc>;

reg = <0x48060000 0x1000>;

status = "disabled";

};

Note that the status is disabled, meaning that no device driver should be bound to it, and also that it has the label mmc1.

Both the BeagleBone and the BeagleBone Black have a microSD card interface attached to mmc1, hence in am335x-bone-common.dtsi, the same node is referenced by its label, &mmc1:

&mmc1 {

status = "okay";

bus-width = <0x4>;

pinctrl-names = "default";

pinctrl-0 = <&mmc1_pins>;

cd-gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;

cd-inverted;

};

The status property is set to okay, which causes the mmc device driver to bind with this interface at runtime on both variants of the BeagleBone. Also, a label is added to the pin control configuration, mmc1_pins. Alas, there is not sufficient space here to describe pin control and pin multiplexing. You will find some information in the Linux kernel source in directory devicetree/bindings/pinctrl.

However, interface mmc1 is connected to a different voltage regulator on the BeagleBone Black. This is expressed in am335x-boneblack.dts, where you will see another reference to mmc1, which associates it with the voltage regulator via label

vmmcsd_fixed:

&mmc1 {

vmmc-supply = <&vmmcsd_fixed>;

};


So, layering device tree source files like this gives flexibility and reduces the need for duplicated code.


Compiling a device tree


The bootloader and kernel require a binary representation of the device tree, so it has to be compiled using the device tree compiler, dtc. The result is a file ending with .dtb, which is referred to as a device tree binary or a device tree blob.

There is a copy of dtc in the Linux source, in scripts/dtc/dtc, and it is also available as a package on many Linux distributions. You can use it to compile a simple device tree (one that does not use #include) like this:

$   dtc simpledts-1.dts -o simpledts-1.dtb DTC: dts->dts on file "simpledts-1.dts"


Be wary of the fact that dtc does not give helpful error messages and it makes no checks other than on the basic syntax of the language, which means that debugging a typing error in a source file can be a lengthy business.

To build more complex examples, you will have to use the kernel Kbuild, as shown in the next .

Choosing a bootloader


Bootloaders come in all shapes and sizes. The kind of characteristics you want from a bootloader are that they be simple and customizable with lots of sample configurations for common development boards and devices. The following table shows a number of bootloaders that are in general use:


Name
Main architectures supported


Das U-Boot
ARC, ARM, Blackfin, Microblaze, MIPS, Nios2, OpenRiec, Pow
SH



Barebox
ARM, Blackfin, MIPS, Nios2, PowerPC


GRUB 2
X86, X86_64


Little
ARM
Kernel



RedBoot
ARM, MIPS, PowerPC, SH


CFE
Broadcom MIPS


YAMON
MIPS






We are going to focus on U-Boot because it supports a good number of processor architectures and a large number of individual boards and devices. It has been around for a long time and has a good community for support.

It may be that you received a bootloader along with your SoC or board. As always, take a good look at what you have and ask questions about where you can get the source code from, what the update policy is, how they will support you if you want to make changes, and so on. You may want to consider abandoning the vendor-supplied loader and using the current version of an open source bootloader instead.


U-Boot


U-Boot, or to give its full name, Das U-Boot, began life as an open source bootloader for embedded PowerPC boards. Then, it was ported to ARM-based boards and later to other architectures, including MIPS and SH. It is hosted and maintained by Denx Software Engineering. There is plenty of information available, and a good place to start is http://www.denx.de/wiki/U-Boot. There is also a mailing list at u-boot@lists.denx.de.

Building U-Boot


Begin by getting the source code. As with most projects, the recommended way is to clone the .git archive and check out the tag you intend to use, which, in this case, is the version that was current at the time of writing:

$   git clone git://git.denx.de/u-boot.git

$   cd u-boot

$   git checkout v2017.01


Alternatively, you can get a tarball from ftp://ftp.denx.de/pub/u-boot.

There are more than 1,000 configuration files for common development boards and devices in the configs/ directory. In most cases, you can make a good guess of which to use, based on the filename, but you can get more detailed information by looking through the per-board README files in the board/ directory, or you can find information in an appropriate web tutorial or forum.

Taking the BeagleBone Black as an example, we find that there is a likely configuration file named configs/am335x_boneblack_defconfig and we find the text The binary produced by this board supports … Beaglebone Black in the board README
files for the am335x chip, board/ti/am335x/README. With this knowledge, building U-Boot for a BeagleBone Black is simple. You need to inform U-Boot of the prefix for your cross compiler by setting the make variable CROSS_COMPILE, and then selecting the configuration file using a command of the type make [board]_defconfig


$ source MELP/_02/set-path-arm-cortex_a8-linux-gnueabihf

$   make CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- am335x_boneblack_defconfig

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


The results of the compilation are:

u-boot: U-Boot in ELF object format, suitable for use with a debugger

u-boot.map: The symbol table

u-boot.bin: U-Boot in raw binary format, suitable for running on your device

u-boot.img: This is u-boot.bin with a U-Boot header added, suitable for uploading to a running copy of U-Boot
u-boot.srec: U-Boot in Motorola S-record (SRECORD or SRE) format, suitable for transferring over a serial connection

The BeagleBone Black also requires a secondary program loader (SPL), as described earlier. This is built at the same time and is named MLO:

$ ls -l MLO u-boot*

-rw-rw-r-- 1 chris chris 78416 Mar 9 10:13 u-boot/MLO
-rwxrwxr-x 1 chris chris 2943940 Mar 9 10:13 u-boot/u-boot

-rwxrwxr-x 1 chris chris 368348 Mar 9 10:13 u-boot/u-boot.bin -rw-rw-r-- 1 chris chris 368412 Mar 9 10:13 u-boot/u-boot.img -rw-rw-r-- 1 chris chris 520741 Mar 9 10:13 u-boot/u-boot.map -rwxrwxr-x 1 chris chris 1105162 Mar 9 10:13 u-boot/u-boot.srec


The procedure is similar for other targets.


Installing U-Boot


Installing a bootloader on a board for the first time requires some outside assistance. If the board has a hardware debug interface, such as JTAG, it is usually possible to load a copy of U-Boot directly into RAM and set it running. From that point, you can use U-Boot commands to copy itself into flash memory. The details of this are very board-specific and outside the scope of this .

Many SoC designs have a boot ROM built in, which can be used to read boot code from various external sources, such as SD cards, serial interfaces, or USB mass storage. This is the case with the am335x chip in the BeagleBone Black, which makes it easy to try out new software.

You will need an SD card reader to write the images to a card. There are two types: external readers that plug into a USB port, and the internal SD readers that are present on many laptops. A device name is assigned by Linux when a card is plugged into the reader. The command lsblk is a useful tool to find out which device has been allocated. For example, this is what I see when I plug a nominal 8 GB microSD card into my card reader:

$ lsblk

NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 477G 0 disk

├─sda1 8:1 0 500M 0 part /boot/efi

├─sda2 8:2 0 40M 0 part

├─sda3 8:3 0 3G 0 part

├─sda4 8:4 0 457.6G 0 part /

└─sda5 8:5 0 15.8G 0 part [SWAP]

sdb 8:16 1 7.2G 0 disk

└─sdb1 8:17 1 7.2G 0 part /media/chris/101F-5626

In this case, sda is my 512 GB hard drive and sdb is the microSD card. It has a single partition, sdb1, which is mounted as directory /media/chris/101F-5626.

Although the microSD card had 8 GB printed on the outside, it was only 7.2 GB on the inside. In part, this is because of the different units used. The advertised capacity is measured in Gigabytes, 109, but the sizes reported by software are in Gibibytes, 230. Gigabytes  are abbreviated GB, Gibibytes as GiB. The same applies for KB

and KiB, and MB and MiB. In this , I have tried to use the right units. In the case of the SD card, it so happens that 8 Gigabytes is approximately 7.4 Gibibytes. The remaining discrepancy is because flash memory always has to reserve some space for bad block handling. This is a topic that I will return to in  7, Creating a Storage Strategy.

If I use the built-in SD card slot, I see this:

$ lsblk

NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 477G 0 disk

├─sda1 8:1 0 500M 0 part /boot/efi

├─sda2 8:2 0 40M 0 part

├─sda3 8:3 0 3G 0 part

├─sda4 8:4 0 457.6G 0 part /

└─sda5 8:5 0 15.8G 0 part [SWAP]

mmcblk0 179:0 0 7.2G 0 disk

└─mmcblk0p1 179:1 0 7.2G 0 part /media/chris/101F-5626

In this case, the micro SD card appears as mmcblk0 and the partition is mmcblk0p1. Note that the microSD card you use may have been formatted differently to this one and so you may see a different number of partitions with different mount points. When formatting an SD card, it is very important to be sure of its device name. You really don't want to mistake your hard drive for an SD card and format that instead. This has happened to me more than once. So, I have provided a shell script in the 's code archive named MELP/format-sdcard.sh, which has a reasonable number of checks to prevent you (and me) from using the wrong device name. The parameter is the device name of the microSD card, which would be sdb in the first example and mmcblk0 in the second. Here is an example of its use:

$ MELP/format-sdcard.sh mmcblk0

The script creates two partitions: the first is 64 MiB, formatted as FAT32, and will contain the bootloader, and the second is 1 GiB, formatted as ext4, which you will use in  5, Building a Root Filesystem.

After you have formatted the microSD card, remove it from the card reader and then re-insert it so that the partitions are auto mounted. On current versions of Ubuntu, the two partitions should be mounted as /media/[user]/boot and /media/[user]/rootfs. Now you can copy the SPL and U-Boot to it like this:

$ cp MLO u-boot.img /media/chris/boot

Finally, unmount it:

$ sudo umount /media/chris/boot

Now, with no power on the BeagleBone board, insert the micro-SD card into the reader. Plug in the serial cable. A serial port should appear on your PC as /dev/ttyUSB0. Start a suitable terminal program, such as gtkterm, minicom, or picocom, and attach to the port at 115200 bps (bits per second) with no flow control. gtkterm is probably the easiest to setup and use:

$ gtkterm -p /dev/ttyUSB0 -s 115200

Press and hold the Boot Switch button on the Beaglebone Black, power up the board using the external 5V power connector, and release the button after about 5 seconds. You should see a U-Boot prompt on the serial console:

U-Boot#


Using U-Boot


In this section, I will describe some of the common tasks that you can use U-Boot to perform.

Usually, U-Boot offers a command-line interface over a serial port. It gives a Command Prompt which is customized for each board. In the examples, I will use U-Boot#. Typing help prints out all the commands configured in this version of U-Boot; typing help <command> prints out more information about a particular command.

The default command interpreter for the BeagleBone Black is quite simple. You cannot do command-line editing by pressing cursor left or right keys; there is no command completion by pressing the Tab key; and there is no command history by pressing the cursor up key. Pressing any of these keys will disrupt the command you are currently trying to type, and you will have to type Ctrl + C and start over again. The only line editing key you can safely use is the backspace. As an option, you can configure a different command shell called Hush, which has more sophisticated interactive support, including command-line editing.

The default number format is hexadecimal. Consider the following command as an example:

nand read 82000000 400000 200000

This will read 0x200000 bytes from offset 0x400000 from the start of the NAND flash memory into RAM address 0x82000000.


Environment variables


U-Boot uses environment variables extensively to store and pass information between functions and even to create scripts. Environment variables are simple name=value pairs that are stored in an area of memory. The initial population of variables may be coded in the board configuration header file, like this:

#define CONFIG_EXTRA_ENV_SETTINGS

"myvar1=value1"

"myvar2=value2"

[...]

You can create and modify variables from the U-Boot command line using setenv. For example, setenv foo bar creates the variable foo with the value bar. Note that there is no = sign between the variable name and the value. You can delete a variable by setting it to a null string, setenv foo. You can print all the variables to the console using printenv, or a single variable using printenv foo.

If U-Boot has been configured with space to store the environment, you can use the saveenv command to save it. If there is raw NAND or NOR flash, then an erase block can be reserved for this purpose, often with another used for a redundant copy to guard against corruption. If there is eMMC or SD card storage, it can be stored in a reserved array of sectors, or in a file named uboot.env in a partition of the disk. Other options include storing in a serial EEPROM connected via an I2C or SPI interface or non-volatile RAM.


Boot image format


U-Boot doesn't have a filesystem. Instead, it tags blocks of information with a 64-byte header so that it can track the contents. You prepare files for U-Boot using the mkimage command. Here is a brief summary of its usage:

$ mkimage

Usage: mkimage -l image
-l ==> list image header information

mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_fi

-A ==> set architecture to 'arch'

-O ==> set operating system to 'os'

-T ==> set image type to 'type'

-C ==> set compression type 'comp'

-a ==> set load address to 'addr' (hex)

-e ==> set entry point to 'ep' (hex)

-n ==> set image name to 'name'

-d ==> use image data from 'datafile'

-x ==> set XIP (execute in place)

mkimage [-D dtc_options] [-f fit-image.its|-F] fit-image -D => set options for device tree compiler -f => input filename for FIT source

Signing / verified boot not supported (CONFIG_FIT_SIGNATURE undefined)

mkimage -V ==> print version information and exit

For example, to prepare a kernel image for an ARM processor, the command is:

$   mkimage -A arm -O linux -T kernel -C gzip -a 0x80008000 -e 0x80008000 \ -n 'Linux' -d zImage uImage


Loading images


Usually, you will load images from removable storage, such as an SD card or a network. SD cards are handled in U-Boot by the mmc driver. A typical sequence to load an image into memory would be:

U-Boot# mmc rescan

U-Boot# fatload mmc 0:1 82000000 uimage
reading uimage

4605000 bytes read in 254 ms (17.3 MiB/s)

U-Boot# iminfo 82000000

##   Checking Image at 82000000 ...

Legacy image found

Image Name: Linux-3.18.0C

reated: 2014-12-23 21:08:07 UTC

Image Type: ARM Linux Kernel Image (uncompressed)

Data Size: 4604936 Bytes = 4.4 MiB

Load Address: 80008000

Entry Point: 80008000 Verifying Checksum ... OK

The command mmc rescan re-initializes the mmc driver, perhaps to detect that an SD card has recently been inserted. Next, fatload is used to read a file from a FAT-formatted partition on the SD card. The format is:

fatload <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]

If <interface> is mmc, as in our case, <dev:part> is the device number of the mmc interface counting from zero, and the partition number counting from one. Hence, <0:1> is the first partition on the first device. The memory location, 0x82000000, is chosen to be in an area of RAM that is not being used at this moment. If we intend to boot this kernel, we have to make sure that this area of RAM will not be overwritten when the kernel image is decompressed and located at the runtime location, 0x80008000.

To load image files over a network, you use the Trivial File Transfer Protocol (TFTP). This requires you to install a TFTP daemon, tftpd, on your development system and start it running. You also have to configure any firewalls between your PC and the target board to allow the TFTP protocol on UDP port 69 to pass through. The default configuration of TFTP allows access only to the directory /var/lib/tftpboot. The next step is to copy the files you want to transfer to the

target into that directory. Then, assuming that you are using a pair of static IP addresses, which removes the need for further network administration, the sequence of commands to load a set of kernel image files should look like this:

U-Boot# setenv ipaddr 192.168.159.42

U-Boot# setenv serverip 192.168.159.99

U-Boot# tftp 82000000 uImage
link up on port 0, speed 100, full duplex

Using cpsw device

TFTP from server 192.168.159.99; our IP address is 192.168.159.42 Filename 'uImage'.

Load address: 0x82000000

Loading:

#################################################################

#################################################################

#################################################################

######################################################

######################################################

3   MiB/s done
Bytes transferred = 4605000 (464448 hex)

Finally, let's look at how to program images into NAND flash memory and read them back, which is handled by the nand command. This example loads a kernel image via TFTP and programs it into flash:

U-Boot# tftpboot 82000000 uimage

U-Boot# nandecc hw

U-Boot# nand erase 280000 400000

NAND erase: device 0 offset 0x280000, size 0x400000

Erasing at 0x660000 -- 100% complete.

OK

U-Boot# nand write 82000000 280000 400000

NAND write: device 0 offset 0x280000, size 0x400000

4194304 bytes written: OK

Now you can load the kernel from flash memory using the nand read command:

U-Boot# nand read 82000000 280000 400000


Booting Linux


The bootm command starts a kernel image running. The syntax is:

bootm [address of kernel] [address of ramdisk] [address of dtb].

The address of the kernel image is necessary, but the address of ramdisk and dtb can be omitted if the kernel configuration does not need them. If there is dtb but no initramfs, the second address can be replaced with a dash (-). That would look like this:

U-Boot# bootm 82000000 - 83000000


Automating the boot with U-Boot scripts

Plainly, typing a long series of commands to boot your board each time it is turned on is not acceptable. To automate the process, U-Boot stores a sequence of commands in environment variables. If the special variable named bootcmd contains a script, it is run at power-up after a delay of bootdelay seconds. If you watch this on the serial console, you will see the delay counting down to zero. You can press any key during this period to terminate the countdown and enter into an interactive session with U-Boot.

The way that you create scripts is simple, though not easy to read. You simply append commands separated by semicolons, which must be preceded by a backslash escape character. So, for example, to load a kernel image from an offset in flash memory and boot it, you might use the following command:

setenv bootcmd nand read 82000000 400000 200000\;bootm 82000000


Porting U-Boot to a new board


Let's assume that your hardware department has created a new board called Nova that is based on the BeagleBone Black and that you need to port U-Boot to it. You will need to understand the layout of the U-Boot code and how the board configuration mechanism works. In this section, I will show you how to create a variant of an existing board—the BeagleBone Black—which you could go on to use as the basis for further customizations. There are quite a few files that need to be changed. I have put them together into a patch file in the code archive in

MELP/_03/0001-BSP-for-Nova.patch. You can simply apply that patch to a clean

copy of U-Boot version 2017.01 like this:

$ cd u-boot

$ patch -p1 < MELP/_03/0001-BSP-for-Nova.patch

If you want to use a different version of U-Boot, you will have to make some changes to the patch for it to apply cleanly.

The remainder of this section is a description of how the patch was created. If you want to follow along step-by-step, you will need a clean copy of U-Boot 2017.01 without the Nova BSP patch. The main directories we will be dealing with are:

arch: Contains code specific to each supported architecture in directories arm, mips, powerpc, and so on. Within each architecture, there is a subdirectory for each member of the family; for example, in arch/arm/cpu/, there are directories for the architecture variants, including amt926ejs, armv7, and armv8. board: Contains code specific to a board. Where there are several boards from the same vendor, they can be collected together into a subdirectory. Hence, the support for the am335x evm board, on which the BeagleBone is based, is in board/ti/am335x.
common: Contains core functions including the command shells and the commands that can be called from them, each in a file named cmd_[command name].c.
doc: Contains several README files describing various aspects of U-Boot. If you are wondering how to proceed with your U-Boot port, this is a good

place to start.

include: In addition to many shared header files, this contains the very important subdirectory include/configs/ where you will find the majority of the board configuration settings.

The way that Kconfig extracts configuration information from Kconfig files and stores the total system configuration in a file named .config is described in some detail in  4, Configuring and Building the Kernel. Each board has a default configuration stored in configs/[board name]_defconfig. For the Nova board, we can begin by making a copy of the configuration for the BeagleBone Black:

$ cp configs/am335x_boneblack_defconfig configs/nova_defconfig


Now edit configs/nova_defconfig and change line four from CONFIG_TARGET_AM335X_EVM=y to CONFIG_TARGET_NOVA=y:

1 CONFIG_ARM=y

2 CONFIG_AM33XX=y

3 # CONFIG_SPL_NAND_SUPPORT is not set

4   CONFIG_TARGET_NOVA=y [...]

Note that CONFIG_ARM=y causes the contents of arch/arm/Kconfig to be included, and on

line two, CONFIG_AM33XX=y causes arch/arm/mach-omap2/am33xx/Kconfig to be included.


Board-specific files


Each board has a subdirectory named board/[board name] or board/[vendor]/[board

name], which should contain:

Kconfig: Contains configuration options for the board

MAINTAINERS: Contains a record of whether the board is currently maintained and, if so, by whom
Makefile: Used to build the board-specific code

README: Contains any useful information about this port of U-Boot; for example, which hardware variants are covered

In addition, there may be source files for board specific functions.

Our Nova board is based on a BeagleBone which, in turn, is based on a TI am335x

EVM, so, we should take a copy of the am335x board files:

$ mkdir board/ti/nova

$ cp -a board/ti/am335x/* board/ti/nova

Next, edit board/ti/nova/Kconfig and set SYS_BOARD to "nova", so that it will build the files in board/ti/nova, and set SYS_CONFIG_NAME to "nova" also, so that the configuration file used will be include/configs/nova.h:

1 if TARGET_NOVA

2
3 config SPL_ENV_SUPPORT

4 default y

5

6 config SPL_WATCHDOG_SUPPORT

7 default y

8

9 config SPL_YMODEM_SUPPORT

10 default y

11

12 config SYS_BOARD

13 default "nova"
14

15 config SYS_VENDOR

16 default "ti"

17

18 config SYS_SOC

19 default "am33xx"

20

21 config SYS_CONFIG_NAME

22 default "nova"

[...]

There is one other file here that we need to change. The linker script placed at

board/ti/nova/u-boot.lds has a hard-coded reference to board/ti/am335x/built-in.o on

line 39. Change it as shown:

35            {

36                      *(.__image_copy_start)

37                      *(.vectors)

38                      CPUDIR/start.o (.text*)

39                      board/ti/nova/built-in.o (.text*)

40                      *(.text*)
41            }

Now we need to link the Kconfig file for Nova into the chain of Kconfig files. First, edit arch/arm/Kconfig and add a menu option for Nova, and then source its Kconfig file:

[...]

1069 source "board/ti/nova/Kconfig"

[...]

Then, edit arch/arm/mach-omap2/am33xx/Kconfig and add a configuration option for

TARGET_NOVA:

[...]

21 config TARGET_NOVA

22                      bool "Support the Nova! board"

23                      select DM

24                      select DM_SERIAL

25                      select DM_GPIO

26                      select TI_I2C_BOARD_DETECT

27                      help

28                          The Nova target board

[...]


Configuring header files


Each board has a header file in include/configs/ which contains the majority of the configuration information. The file is named by the SYS_CONFIG_NAME identifier in the board's Kconfig. The format of this file is described in detail in the README file at the top level of the U-Boot source tree. For the purposes of our Nova board, simply copy include/configs/am335c_evm.h to include/configs/nova.h and make a

small number of changes, the most significant of which is to set a new

Command Prompt so that we can identify this bootloader at run-time:

[...]

16 #ifndef __CONFIG_NOVA_H

17 #define __CONFIG_NOVA_H

[...]

38 #define CONFIG_SYS_LDSCRIPT                                    "board/ti/nova/u-boot.lds"

[...]

68 #undef CONFIG_SYS_PROMPT

69 #define CONFIG_SYS_PROMPT "nova!> "

[...]

421 #endif         /* ! __CONFIG_NOVA_H */


Building and testing


To build for the Nova board, select the configuration you have just created:

$ make CROSS_COMPILE=arm-cortex_a8-linux-gnueabi- distclean

$   make CROSS_COMPILE=arm-cortex_a8-linux-gnueabi- nova_defconfig

$   make CROSS_COMPILE=arm-cortex_a8-linux-gnueabi-

Copy MLO and u-boot.img to the boot partition of the microSD card you created earlier and boot the board. You should see output like this (note the Command Prompt):

U-Boot SPL 2017.01-dirty (Apr 20 2017 - 16:48:38) Trying to boot from MMC1MMC partition switch failed
*** Warning - MMC partition switch failed, using default environment

reading u-boot.img

reading u-boot.img

U-Boot 2017.01-dirty (Apr 20 2017 - 16:48:38 +0100)

CPU : AM335X-GP rev 2.0

I2C: ready

DRAM: 512 MiB

MMC: OMAP SD/MMC: 0, OMAP SD/MMC: 1

*** Warning - bad CRC, using default environment

<ethaddr> not set. Validating first E-fuse MAC Net: cpsw, usb_ether

Press SPACE to abort autoboot in 2 seconds

nova!>

You can create a patch for all of these changes by checking them into Git and using the git format-patch command:

$ git add .

$   git commit -m "BSP for Nova" [nova-bsp-2 e160f82] BSP for Nova

12 files changed, 2272 insertions(+) create mode 100644 board/ti/nova/Kconfig create mode 100644 board/ti/nova/MAINTAINERS create mode 100644 board/ti/nova/Makefile create mode 100644 board/ti/nova/README create mode 100644 board/ti/nova/board.c create mode 100644 board/ti/nova/board.h create mode 100644 board/ti/nova/mux.c create mode 100644 board/ti/nova/u-boot.lds create mode 100644 configs/nova_defconfig create mode 100644 include/configs/nova.h

$   git format-patch -1

0001-BSP-for-Nova.patch


Falcon mode


We are used to the idea that booting a modern embedded processor involves the CPU boot ROM loading an SPL, which loads u-boot.bin which then loads a Linux kernel. You may be wondering if there is a way to reduce the number of steps, thereby simplifying and speeding up the boot process. The answer is U-Boot Falcon mode. The idea is simple: have the SPL load a kernel image directly, missing out u-boot.bin. There is no user interaction and there are no scripts. It just loads a kernel from a known location in flash or eMMC into memory, passes it a pre-prepared parameter block, and starts it running. The details of configuring Falcon mode are beyond the scope of this . If you would like more information, take a look at doc/README.falcon.

Falcon mode is named after the Peregrine falcon, which is the fastest bird of all, capable of reaching speeds of more than 200 miles per hour in a dive.


Barebox


I will complete this  with a look at another bootloader that has the same roots as U-Boot but takes a new approach to bootloaders. It is derived from U-Boot and was actually called U-Boot v2 in the early days. The barebox developers aimed to combine the best parts of U-Boot and Linux, including a POSIX-like API and mountable filesystems.

The barebox project website is http://barebox.org/ and the developer mailing list is

barebox@lists.infradead.org.


Getting barebox


To get barebox, clone the Git repository and check out the version you want to use:

$   git clone git://git.pengutronix.de/git/barebox.git

$   cd barebox

$   git checkout v2017.02.0

The layout of the code is similar to U-Boot:

arch: Contains code specific to each supported architecture, which includes all the major embedded architectures. SoC support is in arch/[architecture]/mach-[SoC]. Support for individual boards is in
arch/[architecture]/boards.

common: Contains core functions, including the shell.

commands: Contains the commands that can be called from the shell.

Documentation: Contains the templates for documentation files. To build it,

type make docs. The results are put in Documentation/html.

drivers: Contains the code for the device drivers.

include: Contains header files.


Building barebox


Barebox has used Kconfig/Kbuild for a long time. There are default configuration files in arch/[architecture]/configs. As an example, assume that you want to build barebox for the BeagleBoard C4. You need two configurations, one for the SPL, and one for the main binary. Firstly, build MLO:

$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ am335x_mlo_defconfig
$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-

The result is the secondary program loader, images/barebox-am33xx-beaglebone-mlo.img.

Next, build barebox:

$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ am335x_defconfig
$   make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-

Copy MLO and the barebox binary to an SD card:

$ cp images/barebox-am33xx-beaglebone-mlo.img /media/chris/boot/MLO

$ cp images/barebox-am33xx-beaglebone.img /media/chris/boot/barebox.bin

Then, boot up the board and you should see messages like these on the console:

barebox 2017.02.0 #1 Thu Mar 9 20:27:08 GMT 2017

Board: TI AM335x BeagleBone black

detected 'BeagleBone Black'

[...]

running /env/bin/init...

changing USB current limit to 1300 mA... done

Hit m for menu or any other key to stop autoboot: 3

type exit to get to the menu

barebox@TI AM335x BeagleBone black:/


Using barebox


Using barebox at the command line you can see the similarities with Linux. First, you can see that there are filesystem commands such as ls, and there is a /dev directory:

# ls /dev


full mdio0-phy00 mem mmc0 mmc0.1 mmc1 mmc1.0 null


mmc0.0
ram0


zero


The device /dev/mmc0.0 is the first partition on the microSD card, which contains the kernel and initial ramdisk. You can mount it like this:

# mount /dev/mmc0.0 /mnt

Now you can see the files:

# ls /mnt

MLO am335x-boneblack.dtb barebox.bin
u-boot.img uRamdisk zImage

Boot from the root partition:

#   global.bootm.oftree=/mnt/am335x-boneblack.dtb

#   global linux.bootargs.root="root=/dev/mmcblk0p2 rootwait"

#   bootm /mnt/zImage


TLDR;


Every system needs a bootloader to bring the hardware to life and to load a kernel. U-Boot has found favor with many developers because it supports a useful range of hardware and it is fairly easy to port to a new device. Over the last few years, the complexity and ever increasing variety of embedded hardware has led to the introduction of the device tree as a way of describing hardware. The device tree is simply a textual representation of a system that is compiled into a device tree binary (dtb) and which is passed to the kernel when it loads. It is up to the kernel to interpret the device tree and to load and initialize drivers for the devices it finds there.

In use, U-Boot is very flexible, allowing images to be loaded from mass storage, flash memory, or a network, and booted. Likewise, barebox can achieve the same but with a smaller base of hardware support. Despite its cleaner design and POSIX-inspired internal APIs, at the time of writing it does not seem to have been accepted beyond its own small but dedicated community.

Having covered some of the intricacies of booting Linux, in the next  you will see the next stage of the process as the third element of your embedded project, the kernel, comes into play.

Comments

  1. Great source of info to read about bootloaders

    ReplyDelete

Post a Comment