Fetching the kernel using HTTP on the RPI3¶
To make a long story short when working with the Raspberry Pi 3 constantly removing and inserting the SD card is not practical
A good solution is network booting, hosting the root filesystem and kernel on the development host and retrieving them over the network - the SD card will only contain a bootloader. This provides an effective way to switch between different kernels and also work with various user space environments: GNU, busybox/buildroot, alpine/busybox, without touching the SD card.
Another benefit is the improvement of the speed of the ‘build, deploy, test, fix’ cycle.
It’s not the first time that I am configuring such a setup on a RaspberryPi 3, This time, I am documenting it.. :)
This article is more of a development journal - and will be split into multiple parts.
The first part explains how to configure U-Boot to fetch Linux over HTTP, subsequent parts will focus on how to configure the kernel to mount a remote root filesystem. Both the kernel and the rootfs will be hosted on my development host - which is my work laptop running Ubuntu Jammy 22.04
U-Boot to rescue¶
By default, the Raspberry Pi 3 uses a custom bootloader and it is perfectly sufficient for most use cases. However, this use case requires a more ‘feature rich’ bootloader because the kernel needs to be fetched over the network.
U-Boot is a popular bootloader that supports many architectures, it can fetch a kernel image over the network. Currently only HTTP and TFTP protocols are supported, HTTP support being the most recent addition. It was added back in 2022 and there has been many fixes since then.
On the Rpi 3 u-boot
acts as a third-stage bootloader, the first stage, which is provided by the vendor.
It lives in the bootcode.bin
file on the SD Card, the bootloader then runs start.elf
which reads config.txt
and the device tree
and only control is given to U-Boot
.
Setting up the project directory¶
mkdir -p ~/rpi3/{dependencies,etc,volume,out}
cd ~/rpi3/
git init
git submodule add --depth 1 https://github.com/raspberrypi/firmware dependencies/rpi_firmware/src
git submodule add --depth 1 https://github.com/u-boot/u-boot dependencies/u-boot/src
mkdir
creates the file hierarchy for this project.
git
downloads the pre-compiled binaries of the current Raspberry Pi Linux kernel,
kernel modules, compiled device trees, user space libraries, and the bootloader/GPU firmware and also U-Boot.
Submodules can be used as a practical way to manage dependencies
U-boot¶
Nowadays, configuring U-Boot for the Raspberry Pi 3 B plus is easy.
Be sure to install the required build dependencies, by following the official documentation https://docs.u-boot.org/en/latest/build/gcc.html#configuration
Copy-paste the apt-get
commands from the documentation,
they will install the GCC compiler for the aarch64 architecture and the necessary libraries required to compile U-Boot.
The next step is configuring U-Boot
cd rpi3/dependencies/uboot
make rpi_3_b_plus_defconfig
This generates a .config
file based on the default configuration for the Raspberry Pi 3 B+
If you’re using another model, use du -a configs | grep rpi_
to list the available default
configurations for the various models of the Raspberry Pi
Fetching the kernel over HTTP requires the use of the wget
monitor command, it can be enabled
by setting the CMD_WGET
Kconfig option which requires PROT_TCP
which enables the TCP stack
This can be done with menuconfig, a ncurses configuration tool, by typing make menuconfig
and selecting the correct options, the .config
file will be modified accordingly.
Save the generated .config
somewhere out of tree, to avoid deleting it when running make mrproper
cp .config ~/rpi3/etc/uboot_rpi_3_b_plus.config
Now that U-Boot is configured, simply run make
to build it.
CROSS_COMPILE=aarch64-linux-gnu- make all
The compiler generates a uboot.bin
file in the current directory
Preparing the SD-Card¶
Most SD cards are pre-formatted with a single FAT-32 partition, the Raspberry Pi 3
uses a two partition setup, one 512 Mb FAT32 boot partition and a EXT4 partition for the rootfs.
Adjusting the partition layout can be achieved with the fdisk(8)
utility
sudo fdisk -l
This will output the list of available disk drives, ignore the /dev/loopXX
devices of which
there are many on Ubuntu…
USB card readers usually use /dev/sdX
so one can filter the output
sudo fdisk -l | grep sd.:
If your system has SATA disks, locate the SD card by looking at the size.
sudo fdisk /dev/sdd
will start fdisk(8)
in interactive mode - to manipulate the partition table of the disk.
d 1
n 1
p
1
2048
+512M
t
0b
w
It removes the first partition, creates another primary partition of 512 Mb, sets the type to FAT32 and writes the changes to the disk and exists
Note that sfdisk(8)
can be used instead to achieve the above result from a script or build system
Check if the partition table is correct by running fdisk -l /dev/sdd
sometimes it’s necessary
to remove and insert the card.
Next format the partition by running sudo mkfs.fat /dev/sdd1
and mount the drive, with sudo mount /dev/sdd1 volume
Copy the necessary files to the SD card
sudo cp -r dependencies/rpi_firmware/src/boot/{bootcode.bin,start.elf,fixup.dat,overlays,bcm2710-rpi-3-b-plus.dtb} volume
sudo cp -r dependencies/u-boot/src/u-boot.bin volume
Create the config.txt
which is read and parsed by start.elf
secondary stage bootloader
cat > etc/bootloader_config.txt <<heredoc
kernel=u-boot.bin
arm64_bit=1
core_freq=250
device_tree=bcm2710-rpi-3-b-plus.dtb
heredoc
Copy the configuration to the SD Card sudo cp etc/bootloader_config.txt volume/config.txt
I found these notes useful, to understand the startup sequence of the Raspberry Pi and the content of config.txt
https://github.com/mhomran/u-boot-rpi3-b-plus
The next step is to create a script for U-Boot to avoid typing monitor commands on the keyboard on each boot
cat > etc/uboot_script.txt <<heredoc
setenv autoload 0
dhcp
setenv serverip 172.22.22.57
wget \${kernel_addr_r} /kernel/linux5.10_rpi
booti \${kernel_addr_r} - \${fdt_addr}
heredoc
This script interrupts the normal loading sequence, acquires an address for the Ethernet card
using the DHCP client and performs a HTTP GET request to fetch the Linux image and store at the address
specified by kernel_addr_r
. The request URL will look like this: http://172.22.22.57/linux5.10_rpi
Compile the script and copy it to the SD card
./dependencies/u-boot/src/tools/mkimage -T script -d etc/uboot_script boot.scr
sudo cp boot.scr volume/
Finally, unmount the volume sudo umount volume
Plug in the Ethernet cable to the Raspberry Pi and insert the card - it will attempt to fetch the kernel and fail.
Server setup¶
At this stage, we have a working U-boot setup that makes use of the wget
monitor command to fetch
the kernel and then boot it with the booti
command great! But a HTTP server needs to serve the kernel when
the bootloader makes a HTTP GET request.
Servers are supposed to have a static IP address, it would be wise to make a DHCP reservation. Setting a static IP on the Ethernet interface used by the web server is another option, It must be an IP address that is outside of the DHCP range of the router you’re using.
Both solutions have a major disadvantage, the U-Boot script will have to be modified when working in a different location. Because the network configuration will most certainly be different from one location to the next.
A solution that doesn’t require patching U-Boot, is to use a dedicated USB network adapter with a DHCP server listening on it.
On Ubuntu, this requires a deep dive into systemd
and systemd-networkd
and then
choosing the right DHCP server and configuring it…
Another solution could be passing the IP address as a DHCP option, is it possible without patching U-Boot ? - I Need to investigate this by checking the source code/documentation, so it will be the focus of a subsequent article.
For this guide - I will use NGINX, a famous HTTP server that I am familiar with.
It can be installed like so sudo apt-get install nginx
I like to disable the systemd
unit file for NGINX sudo systemctl disable nginx
because I tend to run NGINX on-demand, by hand or from a script. It is also possible to run multiple instances of NGINX
but this requires to specify a custom HTTP port in the configuration
Create the NGINX config file
cat > etc/nginx_rpi.conf <<heredoc
user etag;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/rpi_access.log;
error_log /var/log/nginx/rpi_error.log;
server {
listen 80 default_server;
root /home/etag/rpi3/out/kernel;
server_name _;
location /kernel {
try_files \$uri \$uri/ =404;
}
}
}
heredoc
A good idea is to create the webroot in the project file hierarchy since the kernel is fetched over HTTP
make sure the HTTP server serves the content of the out/kernel
directory.
mkdir -p out/kernel
Start NGINX: sudo nginx -p . -c etc/nginx_config.conf
It will spin up NGINX with the custom configuration - this requires setting the prefix with -p
option.
Reboot the Pi, while running tail -f /var/log/nginx/rpi_access.log
, you should see a GET /kernel/linux5.10_rpi
request being logged.
Conclusion¶
To integrate this into an existing build system, scripts will need to be written but before writting them, it is necessary to make it work on other networks without modifying the U-Boot boot script each time..
The next article will focus on configuring and compiling the Raspberry Pi stable kernel, to make it mount the rootfs over the network - so stay tuned!