Raspberry Pi 4 Model B Das U-Boot

Setup a Raspberry Pi 4 Model B to boot using Das U-Boot

Das U-Boot is both a first-stage and second-stage bootloader that is very flexible and suitable for embedded systems. Below is a brief summary of setting up a Raspberry Pi 4 Model B (RPi4) to boot using U-Boot.

First iteration

Goal: boot into U-Boot and allow access to the U-Boot boot prompt on a serial console, but don’t boot a kernel.

UART’s

The RPi4 has primary and secondary UART. By default:

To allow serial console access, the primary UART must be enabled using “enable_uart=1” in config.txt. There is a compromise in doing this in that it
disables variable core clock speed and locks the core clock at a low speed of 250MHz. You can add “force_turbo=1”, but that locks it at the highest speed and results in high power usage. For now, we will live with the lower speed. In future iterations, we’ll talk about alternatives. For more details about the serial port config see this bitscope summary and the official documentation.

Build Boot Files

Below is a script that will build a directory named boot with all of the required files. See comments in the script for explanation:

#!/bin/bash
# exit on first error
set -e

# install pre-requisite packages. Assumes a base install of Debian Bullseye
sudo apt install -y git dosfstools gcc-aarch64-linux-gnu make flex bison bc libssl-dev

# clone a recent release of u-boot
git clone --depth=1 --branch=v2023.01 https://source.denx.de/u-boot/u-boot.git

# cross compile for 64 bit arm architecture using RPi4 config
cd u-boot/
CROSS_COMPILE=aarch64-linux-gnu- make rpi_4_defconfig
CROSS_COMPILE=aarch64-linux-gnu- make -j6

# create our boot directory and copy u-boot.bin into it
cd ..
mkdir boot
cp u-boot/u-boot.bin boot/

# clone a recent release of the Raspberry Pi firmware and copy boot files
git clone --depth=1 --branch=1.20230106 https://github.com/raspberrypi/firmware.git
cp firmware/boot/{bcm2711-rpi-4-b.dtb,fixup4.dat,start4.elf} boot/

# create a simple config.txt file
cat << EOF > boot/config.txt
# Enable mini UART
enable_uart=1
# Run in 64-bit mode
arm_64bit=1
# Use Das U-Boot
kernel=u-boot.bin
EOF

The end result looks like:

boot/
|-- bcm2711-rpi-4-b.dtb
|-- config.txt
|-- fixup4.dat
|-- start4.elf
`-- u-boot.bin

Copy to and microSD Card

Here is a helper script (mkboot.sh) for creating the microSD card with our boot files:

#!/bin/bash
# exit on any error
set -e
if [ $# -ne 1 ]; then
  echo "no disk device specified"
  exit 1
fi
# make a 100MiB partition
echo 'start=2048, size=204800, type=b'|sfdisk "$1"
# format the partition
part="${1}1"  # normal /dev/sda1 format
if [[ $1 == /dev/mmcblk* ]]; then
  part="${1}p1"
fi
mkfs.vfat -F 32 -n BOOT "$part"
# mount the partion, copy boot files, then unmount
mount "$part" /mnt/
cp -r boot/* /mnt/
umount /mnt/

Run the mkboot.sh script as root with the microSD card device as an argument to overwrite the microSD card with a single partition containing the contents of ./boot/. Example with /dev/sda:

 sudo ./mkboot.sh /dev/sda

WARNING: make sure you specify the correct device as this will overwrite every thing on the disk.

Then insert the microSD card in the Raspberry Pi.

Serial Connection

Use a 3.3V FTDI USB to serial adapter such as this.

Attach the RX from the adapter to pin 8 on the Raspberry Pi header and attach the TX to pin 10 (i.e., attach RX of one to TX of the other and vice versa). Also attach ground from the adapter to a ground pin (either pin 6 or 14 will work).

When you plug in the USB adapter you should see a new device /dev/ttyUSB0. To connect to the port, use minicom:

sudo apt install -y minicom
minicom -D /dev/ttyUSB0 -b 115200

To exit minicom, pres ctrl-a then z then x.

Power Up

Plug a USB-C power cord into the Raspberry Pi to power it on and boot. You should see something like this in minicom:

Welcome to minicom 2.8

OPTIONS: I18n 
Port /dev/ttyUSB0, 20:26:14

Press CTRL-A Z for help on special keys



U-Boot 2023.01 (Jan 26 2023 - 02:24:13 +0000)

DRAM:  948 MiB (effective 7.9 GiB)
RPI 4 Model B (0xd03114)
Core:  209 devices, 16 uclasses, devicetree: board
MMC:   mmcnr@7e300000: 1, mmc@7e340000: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1... 
In:    serial
Out:   vidconsole
Err:   vidconsole
Net:   eth0: ethernet@7d580000
PCIe BRCM: link up, 5.0 Gbps x1 (SSC)
starting USB...
Bus xhci_pci: Register 5000420 NbrPorts 5
Starting the controller
USB XHCI 1.00
scanning bus xhci_pci for devices... 2 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot:  0 
U-Boot> 

Second Iteration

Ok, we were able to boot into U-Boot, but now lets actually boot a kernel and a root filesystem using an initramfs. We won’t actually use an init system, but just boot into a shell using “rdinit=/bin/sh” boot argument.

This time we will disable the bluetooth module so we can use the PL011 UART for the serial console and don’t have to lock the core clock speed. We do this by adding “dtoverlay=disable-bt” to the config.txt and copying the disable-bt.dtbo file into an overlays directory in our boot directory.

#!/bin/bash
# exit on first error
set -e

# install pre-requisite packages. Assumes a base install of Debian Bullseye
sudo apt install -y git dosfstools gcc-aarch64-linux-gnu make flex bison bc libssl-dev fakeroot wget cpio

if ! [ -d u-boot ]; then
  # clone a recent release of u-boot
  git clone --depth=1 --branch=v2023.01 https://source.denx.de/u-boot/u-boot.git
  cd u-boot/
  # cross compile for 64 bit arm architecture using RPi4 config
  CROSS_COMPILE=aarch64-linux-gnu- make rpi_4_defconfig
  CROSS_COMPILE=aarch64-linux-gnu- make -j6
  cd ..
fi


if ! [ -d firmware ]; then
  # clone a recent release of the Raspberry Pi firmware
  git clone --depth=1 --branch=1.20230106 https://github.com/raspberrypi/firmware.git
fi

if ! [ -d rootfs ]; then
  # download and extract an alpine linux root filesystem
  wget https://dl-cdn.alpinelinux.org/alpine/v3.17/releases/aarch64/alpine-minirootfs-3.17.1-aarch64.tar.gz
  mkdir rootfs
  tar xf alpine-minirootfs-*-aarch64.tar.gz -C rootfs
fi

# create initramfs
cd rootfs && find . | fakeroot  cpio -ov -H newc | gzip > ../initramfs.igz && cd ..

# create a u-boot boot script. Put EOF in double quotes so shell variables are not expanded.
# note: fdt_addr is the address of the device tree passed by start4.elf.
cat << "EOF" > boot.txt
fatload mmc 0:1 ${kernel_addr_r} Image
fatload mmc 0:1 ${ramdisk_addr_r} uRamdisk
booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr}
EOF

# create our boot directory and copy files into it
rm -rf boot
mkdir -p boot/overlays
cp u-boot/u-boot.bin boot/
cp firmware/boot/{bcm2711-rpi-4-b.dtb,fixup4.dat,start4.elf} boot/
cp firmware/boot/overlays/disable-bt.dtbo boot/overlays/

# uncompress kernel (U-Boot can boot compressed but takes special handling)
zcat -S .img firmware/boot/kernel8.img > boot/Image

# use the mkimage tool to prepare the initramfs and boot script for u-boot
./u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot.txt boot/boot.scr
./u-boot/tools/mkimage -A arm64 -O linux -T ramdisk -d initramfs.igz boot/uRamdisk

# create a simple config.txt file
cat << EOF > boot/config.txt
# Disable bluetooth so we can user serial console
dtoverlay=disable-bt
# Run in 64-bit mode
arm_64bit=1
# Use Das U-Boot
kernel=u-boot.bin
EOF

# Kernel cmdline. start4.elf combines this with bootargs from the device
# tree file:
#    chosen {
#      bootargs = "coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_compat_alsa=0 snd_bcm2835.enable_hdmi=1";
#     };
# The combined args are used to boot the kernel. If "setenv bootargs ..."
# is used in the u-boot boot script, it will override the device tree bootargs
# and the bootargs in cmdline.txt.
# ref: https://stackoverflow.com/questions/48801998/passing-bootargs-via-chosen-node-in-device-tree-not-working-for-beaglebone-black 
echo "console=tty1 console=serial0,115200 rdinit=/bin/sh" > boot/cmdline.txt

Third Iteration

This iteration is similar to the last one, but we will use the newer Flat Image Tree (FIT) U-Boot image format. It is a newer format that lets you bundle multiple resources into one image (e.g., kernel, fdt, and initramfs). It also has other features such as integrity checking, etc. Ref: docs.

A couple things we have to do differently to support this:

Here is the script:

#!/bin/bash
# exit on first error
set -e

# install pre-requisite packages. Assumes a base install of Debian Bullseye
sudo apt install -y git dosfstools gcc-aarch64-linux-gnu make flex bison bc libssl-dev fakeroot wget cpio device-tree-compiler

if ! [ -d u-boot ]; then
  # clone a recent release of u-boot
  git clone --depth=1 --branch=v2023.01 https://source.denx.de/u-boot/u-boot.git
  cd u-boot/
  # cross compile for 64 bit arm architecture using RPi4 config
  echo "CONFIG_FIT=y" >> configs/rpi_4_defconfig
  CROSS_COMPILE=aarch64-linux-gnu- make rpi_4_defconfig
  CROSS_COMPILE=aarch64-linux-gnu- make -j6
  cd ..
fi


if ! [ -d firmware ]; then
  # clone a recent release of the Raspberry Pi firmware
  git clone --depth=1 --branch=1.20230106 https://github.com/raspberrypi/firmware.git
fi

if ! [ -d rootfs ]; then
  # download and extract an alpine linux root filesystem
  wget https://dl-cdn.alpinelinux.org/alpine/v3.17/releases/aarch64/alpine-minirootfs-3.17.1-aarch64.tar.gz
  mkdir rootfs
  tar xf alpine-minirootfs-*-aarch64.tar.gz -C rootfs
fi

# create initramfs
cd rootfs && find . | fakeroot  cpio -ov -H newc | gzip > ../initramfs.cpio.gz && cd ..

# create a u-boot boot script. Put EOF in double quotes so shell variables are not expanded.
# note: fdt_addr is the address of the device tree passed by start4.elf.
cat << "EOF" > boot.txt
setenv bootargs coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_compat_alsa=0 snd_bcm2835.enable_hdmi=1 console=ttyAMA0,115200 rdinit=/bin/sh
fatload mmc 0:1 2800000 image.itb
bootm 2800000
EOF


cat << "EOF" > image.its
/dts-v1/;
  
/ {
    description = "U-Boot fitImage for Raspberry Pi 4 Model B kernel";
    #address-cells = <1>;
  
    images {
        kernel {
            description = "Linux Kernel";
            data = /incbin/("./firmware/boot/kernel8.img");
            type = "kernel";
            arch = "arm64";
            os = "linux";
            compression = "gzip";
            load = <0x80000>;
            entry = <0x80000>;
            hash {
                algo = "sha1";
            };
        };
        fdt-1 {
            description = "Flattened Device Tree blob";
            data = /incbin/("./firmware/boot/bcm2711-rpi-4-b.dtb");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            load = <0x02600000>;
            hash {
                algo = "sha1";
            };
        };
        fdt-2 {
            description = "Flattened Device Tree blob";
            data = /incbin/("./firmware/boot/overlays/disable-bt.dtbo");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            load = <0x02680000>;
            hash {
                algo = "sha1";
            };
        };
        ramdisk {
            description = "ramdisk";
            data = /incbin/("./initramfs.cpio.gz");
            type = "ramdisk";
            arch = "arm64";
            os = "linux";
            hash {
                algo = "sha1";
            };
        };
    };
    configurations {
        default = "conf";
        conf {
            description = "Boot Linux kernel with FDT blob + ramdisk";
            kernel = "kernel";
            fdt = "fdt-1", "fdt-2";
            ramdisk = "ramdisk";
            hash {
                algo = "sha1";
            };
        };
    };
};
EOF

# create our boot directory and copy files into it
rm -rf boot
mkdir -p boot/overlays
cp u-boot/u-boot.bin boot/
cp firmware/boot/overlays/disable-bt.dtbo boot/overlays/
cp firmware/boot/{bcm2711-rpi-4-b.dtb,fixup4.dat,start4.elf} boot/

# use the mkimage tool to prepare the boot script for u-boot
./u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot.txt boot/boot.scr
./u-boot/tools/mkimage -f image.its boot/image.itb

# create a simple config.txt file
cat << EOF > boot/config.txt
# Disable bluetooth so we can user serial console
dtoverlay=disable-bt
# Run in 64-bit mode
arm_64bit=1
# Use Das U-Boot
kernel=u-boot.bin
EOF

Conclusion

This post documented the process of using U-Boot to boot a Linux kernel on a Raspberry Pi 4 Model B. We also explored options for using the serial console and the compromises between those options (locked clock speed vs no bluetooth).