Bootloader In Embedded Linux
The bootloader is the second element of embedded Linux. It is the part that starts the system and loads the operating system kernel.
The bootloader is the second element of embedded Linux. It is the part that starts the system and loads the operating system kernel. We 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, as this will help you follow the connections described in a device tree and relate it to real hardware.
The bootloader has two main jobs: to initialize the system to a basic level and 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 is not set up, so the main memory is not accessible. Likewise, other interfaces are not configured, so storage that's accessed via NAND flash controllers, MMC controllers, and so on is unavailable. Typically, the only resources that are operational at the beginning are a single CPU core,
some on-chip static memory, and the boot ROM.
The final act of the bootloader is to load the kernel into RAM and create
an execution environment for it.
First, the bootloader has to pass a pointer to a structure containing information about the hardware configuration. 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.
The general stages typically include:
1. Phase 1 – ROM Code
This is the first phase after power-on or reset. The ROM code runs from the static ROM memory on the chip. Its main task is to initialize minimal hardware - usually the SRAM controller - and then load the Secondary Program Loader (SPL) into SRAM from a boot storage device (e.g., NAND flash, eMMC, SD card) using predefined offsets. Once the SPL is loaded into SRAM, the ROM code jumps to the beginning of the SPL code.
2. Phase 2 – Secondary Program Loader (SPL)
The SPL must set up the memory controller and other essential parts of the system to prepare for loading the Tertiary Program Loader (TPL) into DRAM. The SPL is limited by the size of the SRAM. Like the ROM code, it can read a program from a list of storage devices using predefined offsets. If the SPL includes a file system driver, it can read familiar filenames such as u-boot.img from a disk partition. SPL usually does not allow user interaction but may print version info and progress messages. The SPL can be open-source (like TI x-loader and Atmel AT91Bootstrap), but often contains proprietary code provided by the manufacturer as a binary blob. After loading the TPL into DRAM, the SPL can jump to that memory region.
3. Phase 3 – Tertiary Program Loader (TPL)
At this stage, a full bootloader such as U-Boot is running from DRAM. Typically, the TPL provides a simple command-line interface for maintenance tasks, such as loading new boot and kernel images into flash memory, and loading and booting the kernel. It also has the capability to load the kernel automatically without user interaction. By the end of this phase, the kernel is already in memory and ready to be booted. Embedded bootloaders typically disappear from memory once the kernel is running and no longer play a role in the system's operation. Before that happens, the TPL needs to hand over control of the boot process to the kernel.
When the bootloader passes control to the kernel, it has to pass some basic information, which includes the following:
A device tree is a flexible way of defining the hardware components of a computer system. Bear in mind that a device tree is just static data, not executable code. 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 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
Device tree have format such as JSON. Basic Structure of the Device Tree:
- The Device Tree represents a computer system as a collection of interconnected components arranged in a hierarchical structure, similar to a tree.
- The device tree begins with a root node, represented by a forward slash /, which contains child nodes representing the system's hardware components.
- Each node has a name and contains a set of properties in the format: name = "value".
file.dts
/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 */
};
};
- Node names often include the @ character followed by an address, used to distinguish this node from others of the same type. The @ character is mandatory if the node has a reg property.
Compatible property
Both the root node and CPU node include a compatible property. The Linux kernel uses this property to find the appropriate device driver by comparing it to strings exported by each device driver in an of_device_id structure.
The value of the compatible property typically follows a naming convention that includes the manufacturer's name and component name, to avoid confusion between similar devices from different vendors.
A device may list multiple compatible values if several drivers can handle it, with the most suitable driver listed first.
Device_type property
The CPU node and memory node include a device_type property, which describes the type of the device.
The node name is often inferred from the `device_type`.
Reg property
Memory and CPU nodes often include a reg property, which refers to a range of units in a register address space.
The reg property includes two values: the physical address and the size (length) of the range. Both values are expressed as one or more 32-bit integers, called cells.
The number of cells required for the address and size is determined by the parent node’s #address-cells and #size-cells properties.
If these are not specified, the default is 1 for each - but relying on default values is considered bad practice.
For 64-bit devices, two cells may be needed for each address and size.
file.dts
/{
#address-cells = <2>;
#size-cells = <2>;
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0 0x80000000>;
};
};
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:
terminal
dtc simpledts-1.dts -o simpledts-1.dtb
To build more complex examples, you will have to use the Kbuild kernel
Benefits of Device Tree
Device Tree has two main forms:
These binary Device Tree Blobs are passed to the Linux kernel at boot time so it can understand the hardware layout.
terminal
# compile dts to dtb
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
# compile a specific dts file
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6dl-sabrelite.dtb
# compile a file dtb to dts
dtc -I dtb -O dts arch/arm/boot/dts/imx6dl-sabrelite.dtb > path/to/my_devicetree.dts
Comments