In this repository, I will demonstrate how to boot a 64-bit kernel using u-boot and busybox. I struggled to find a complete guide to make me able to do that. I did some reverse engineering on a working system to understand what I'm supposed to do to boot my custom kernel. So I will try to show that in detail with some screen shots. My purpose is to make things easy for everyone else to build their own kernel without hassle.
I use a USB drive to boot my custom kernel and not an SD card. A USB drive is much easier for you to remove and plug back in again. Luckily, rpi3b+ can boot from a USB drive without any necessary configuration. All you just need to do is to unplug the SD card if it exists.
- Raspberry pi 3b+
- USB drive
- USB to TTL
We'll use USB to TTL to see the kernel messages. It will be connected to the pins 14 and 15 in the raspberry pi. The USB drive will hold our custom linux OS.
First, the ROM bootloader starts and loads the second bootloader in the L2 Cache. Then, the second bootloader bootcode.bin
starts and enables SDRAM and loads start.elf
. After that, start.elf
starts and makes a Flattened Device Tree (FDT) using the device tree binary .dtb in the boot folder and applies overlays that're written in config.txt
. Then, it passes the FDT to u-boot
and loads u-boot
. The u-boot
starts and loads the kernel into RAM, then it passes the same FDT passed by start.elf
to the kernel and starts the kernel. Finally, the kernel starts, mounts the filesystem, and executes /sbin/init
.
It's important to mention how to apply a device tree overlay when it comes to raspberry pi. u-boot
uses a library called libfdt
to deal with device trees and apply overlays to them. Unfortunately, raspberry pi device trees don't follow the standard that this library uses. So, whenever I tried to apply an overlay, it always fails with FDT_ERR_NOTFOUND
.
To get over this problem, we can reuse the same FDT
offered by start.elf
and make u-boot pass it to the kernel. This will allow us to enable whatever overlays we want by writing their names in config.txt
. The downside of this method is that we always need to provide the device tree in the boot directory. So, it can't be downloaded from an FTB server, for example.
Your USB drive should have two partitions. One should be in FAT
format. Usually, it's called boot
. This filesystem will hold the kernel files, the bootloaders, device trees, and overlays. Another one can be in ext4 format. This one holds the linux file system. It's usually called rootfs
. The file system size should be large enough to hold the content in it.
You can partition your USB drive using either the Ubuntu Disks utility or fdisk
command. There are plenty of sources out there to show you how to partition your drive.
There are vendor-specific files that should exist in the boot
filesystem. These files are:
boocode.bin
: this is the bootloader, which is loaded by the SoC on boot, does some very basic setup, and then loads thestart.elf
file.start.elf
: its job is to combine overlays with an appropriate base device tree, and then to pass a fully resolved Device Tree to u-boot.config.txt
: Contains many configuration parameters for setting up the Raspberry Pi.fixup.dat
: This is a linker file.
Those files (except for config.txt
) can be found in the firmware repository of raspberry pi here. config.txt
is created and populated manually (this will be discussed later). You can leave it empty for now.
u-boot expects a flattened device tree (FDT) from the start.elf bootloader. The start.elf bootloader needs a device tree binary.dtb file to turn it into an FDT and pass it to u-boot. At this moment, we don't have one. So, we need to compile a device tree source first.
-
We need to clone the Linux kernel. We have two options. The first option is to clone the Linux repository and start configuring the kernel without any default configuration. This is a lot of work. The second option, which I chose, is to clone the Linux kernel maintained by the Raspberry Pi because it provides a default configuration to start from.
The kernel repository can be found here. You can just run
git clone https://github.com/raspberrypi/linux --depth=1
--depth=1
makes sure that we don't get the whole linux repository history which is so big and not useful for our case. We just need the latest tag. -
We need to have the cross compiler
aarch64-linux-gnu-
you can install it on ubuntu using this command:sudo apt-get install gcc-aarch64-linux-gnu
-
Configure the linux kernel using this command:
make bcmrpi3_defconfig ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
-
Compile the DTBs using this command:
make -j12 dtbs ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
-
Move the compiled Device Tree Binary file to the boot filesystem. You can find it under
arch/arm64/boot/dts/broadcom/bcm2710-rpi-3-b-plus.dtb
Note: I found that this device tree is the most compatible one with the overlays. I tried others like bcm2837.
Your config.txt
should have these lines:
-
enable_uart=1
is basically enabling the the uart to be used byu-boot.bin
-
kernel=u-boot.bin
gives the kernel name to be loaded which isu-boot.bin
in our case. -
arm_64bit=1
forces the kernel loading system to assume a 64-bit kernel, starts the processors up in 64-bit mode. -
core_freq=250
Frequency of the GPU processor core in MHz. -
device_tree
specifiy the name of the.dtb
file to be loaded.
-
clone
u-boot
repositorygit clone https://github.com/u-boot/u-boot --depth=1
-
set the rpi3b+ default configuration
make rpi_3_b_plus_defconfig ARCH=arm CROSS_COMPILE=aarch64-linux-gnu-
-
Make further configuration
make menuconfig ARCH=arm CROSS_COMPILE=aarch64-linux-gnu-
-
Save fdt address, passed by the previous bootloader
start.elf
, to env vatprevbl_fdt_addr
you can find it here:Boot options->Save fdt address
-
Change the Shell prompt to my username, you can find it here:
Command Line Interafce->Shell Command
-
Increase the autoboot delay to 20 seconds so that we can have enough time to interrupt the autoboot, you can find it here:
Boot options->Autoboot options->delay
-
-
Build
u-boot
-
There's an issue I found when I tried to build
u-boot
which is multiple definitions of the functionsave_boot_params
. I could get over this by deleting the implementation in board/raspberrypi/rpi/lowlevel_init.S. -
Now you can run:
make -j12 ARCH=arm CROSS_COMPILE=aarch64-linux-gnu-
-
Move
u-boot-bin
to the boot filesystem
- Plug the usb drive into the raspberry pi. At this point, the u-boot should boot successfully.
-
In step 3, we just built the device tree binaries. There are two more steps to build our custom kernel:
-
Configure the kernel with this command:
make menuconfig ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
-
build the kernel with this command:
make -j12 Image ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
-
Move the Image to the boot filesystem. You can find it under:
arch/arm64/boot/Image
Note: for 64bit kernel, there's no option for making a compressed image e.g. zImage
Note: you can setup a trivial ftp server and grap the image from the host machine. (to be discussed later)
For u-boot to be able to boot the kernel, it should have the kernel and the device tree binary address. The kernel expects some boot arguments. Boot arguments configure things in the kernel. For u-boot to be able to pass the boot arguments, we need to put them in an environment variable bootargs
using the command setenv
.
Now let's consider booting the kernel step by step:
-
Turn your raspberry pi on and let u-boot start, then interrupt the autoboot by pressing any key.
-
You should load the kernel into the RAM. You can run this command in u-boot:
fatload usb 0 ${kernel_addr_r} Image
-
We have the address of the binary tree binary given to u-boot stored in
prevbl_fdt_addr
. So, we don't need to load any DTB. -
We need to specify the bootargs. You can run:
setenv bootargs "8250.nr_uarts=1 root=/dev/sda2 rootwait console=ttyS0,115200n8"
8250.nr_uarts=1
specifies the number of serial ports supported. In our case, it's one serial portttyS0
.root=/dev/sda2
specify the partition of the root filesystem.rootwait
Wait (indefinitely) for the root device to show up. Useful for devices that are detected asynchronously (e.g. USB and MMC devices).console=ttyS0,115200n8
tells the kernel to start a console for us on the serial portttyS0
with the baudrate of 115200 with no parity and the data size is 8 bytes. -
Now, we're ready to boot our kernel using the command:
booti ${kernel_addr_r} - ${prevbl_addr_r}
If you got something like that, then you booted the kernel successfully. The last message indicates that there's no init file, which should exist in the root filesystem at /sbin/init. In the next section, we'll make it using busybox
.
reference: https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html
We'll use busybox to build the root filesystem.
-
Clone the repository:
git clone https://github.com/mirror/busybox
-
We don't need a default configuration, so we'll skip this step.
-
We can start configuring using this command:
make menuconfig
- Make a static binary (no shared libs). We can find that option under
settings->Build Static Binary
- Determine the cross compiler. You can find it under this option:
settings->Cross compiler prefix
IMPORTANT NOTE: I tried passing CROSS_COMPILE=aarch64-linux-gnu- to
make menuconfig
but it didn't consider it. I just found it after I tried to use the filesystem and it didn't work. Be careful and determine it after runningmake menuconfig
. -
Build the busybox using this command:
make -j12
-
Install the filesystem using this command:
make install
The installed filesystem could be found in
./_install
folder. Copy it to the filesystem partition. -
Make the
rcS
script that will remount the filesystem as read/write and do other stuff.mkdir _install/etc
mkdir _install/etc/init.d
vi _install/etc/init.d/rcS
copy this and paste it in the
rcS
file#!/bin/sh mount -t proc none /proc mount -o remount,rw / mount -t sysfs none /sys echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s
Make it excutable
chmod +x _install/etc/init.d/rcS
-
Create important folders:
mkdir _install/proc
mkdir _install/sys
mkdir _install/dev
-
Copy _install to the root filesystem partition
cp -r _install/* /media/mhomran/rootfs
Plug in the USB drive into the raspberry pi and start the kernel by doing step #6 once more.
My root filesystem !The u-boot
supports a command (tftpboot
) that can help us load the kernel from an FTP server. The Raspberry Pi is connected to the host machine using Ethernet. To be able to load the image from your host machine onto the Raspberry Pi:
-
From the host machine side:
-
Setup a static IP address for your host machine's Ethernet. I chose
192.168.2.3
. -
Install the trivial FTP server using this command (assuming you are using Ubuntu OS):
sudo apt install tftp-hpa
-
Configure
tftpd-hpa
by editing/etc/default/tftpd-hpa
. You need to put your chosen host IP address in the variableTFTP_ADDRESS
.TFTP_USERNAME="tftp" TFTP_DIRECTORY="/var/lib/tftpboot" TFTP_ADDRESS="192.168.2.3:69" TFTP_OPTIONS="--secure
-
Move the kernel
Image
to/var/lib/tftpboot
.
-
-
From the Raspberry Pi side:
-
Start your Raspberry Pi and interrupt the u-boot autoboot.
-
Then, set two environment variables that indicate the host and the target IP addresses. The first one is
serverip
, which sets the server IP address, which is192.168.2.3
in my case. The second one isipaddr
which indicates the Raspberry Pi IP address, which is192.168.2.2
in my case.setenv serverip 192.168.2.3
setenv ipaddr 192.168.2.2
-
Load the kernel using this command:
tftpboot ${kernel_addr_r} Image
-
Note: The reason I used this IP address specifically is that, according to the routing table, my ethernet interface was chosen to process internet traffic when I chose an IP address in the form of 192.168.1.x
. I wanted the internet traffic to be routed to my wireless interface instead.
Note: Sometimes when you reboot your host machine, the tftp server fails, so you need to restart it using:
service tftpd-hpa restart
You can check its status using:
service tftpd-hpa status
Embedded Linux From Scratch [English]
Embedded Linux Youtube Playlist [Arabic]