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.
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:
systemd-boot: This used to be called gummiboot. It is a simple UEFI-compatible bootloader, licensed under LGPL v2.1. The
website is https:/ /www.freedesktop.org/wiki/Software/systemd/systemd-boot/.
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.
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
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
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 {
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
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
. Therefore, to build
U-Boot using the Crosstool-NG compiler we created in Chapt er 2, Learning About Toolchains, you would type:
$ 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:
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
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.
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.
Great source of info to read about bootloaders
ReplyDelete