U-Boot on Raspberry

Though it’s not very common, it is sometimes desirable to replace the original Raspberry’s bootloader with something more customizable like: Das U-Boot 1. Having a handcrafted bootloader on an embedded system has a number of advantages:

  • booting from other sources, like: network or USB,

  • firmware upgrade,

  • getting back to fail-safe state after messing up something,

  • flexible kernel select,

  • virtually anything one would want to do before the OS starts.

I had a hard time looking for some good knowledge source covering this topic. The only webpage that gave me lots of hints was eLinux 2, though it had some outdated information. Eventually, I’ve managed to achieve the desired result, using the above source and going through many trials and errors.

Tools and stuff

Here’s a list of things I’ve beein using during the development and testing:

Raspberry Pi Zero W

The device I’m targeting.

SD card with Raspbian

Something to boot.

Lubuntu

Host machine where U-Boot is built. Any Linux distro will be okay.

USB to UART adapter

Even though U-Boot supports HDMI and USB, it’s more convenient to connect directly to Raspberry’s serial port.

Prepare the host machine

Get the ARM toolchain

The first and foremost step is installation of the ARM toolchain. On distros like Ubuntu it’s really straightforward:

sudo apt install git build-essential crossbuild-essential-armhf

It might happen that the above set of packages won’t be sufficient for the setup. In that case, just install what’s missing, there’s no magic here.

Clone U-Boot

The best way to get the newest U-Boot is of course GitHub:

git clone -b master --depth 1 https://github.com/u-boot/u-boot.git
cd u-boot

The additional arguments to git command saves lots of time, as they instruct git to clone only the tip of the master branch.

Build U-Boot

This step is pretty easy and shouldn’t cause troubles. Just make sure appropriate cross compilation environment variable are present, so U-Boot will be built for ARM.

It’s important to select defconfig appropriate to the target Raspberry. Available defconfigs are grouped inside the configs directory. In case the wrong defconfig will be used, U-Boot will simply fail to run. When everything is prepared, the magic spell to build U-Boot is:

export CROSS_COMPILE=arm-linux-gnueabihf-
make rpi_0_w_defconfig
make -j8 -s

The compilation should be quite fast, it really depends on a CPU of the host. When it’s finished without errors, a u-boot.bin file should be present in the current directory.

Before hitting the red button

Everything required to write U-Boot to the SD card, and set it as the default boot target is ready. But before that can happen, there are a couple of additional steps.

Enable UART on Raspberry

In order to communicate with U-Boot freely, UART on Raspberry must be enabled. This can be easily accomplished by modifying config.txt file living in the boot partition of the SD card. The following line should be added anywhere in the file:

enable_uart=1

Know the Kernel command line

When Raspberry boots the usual way, after the 3rd stage bootloader finishes its tasks, it runs start.elf which parses config.txt as one of its steps, and appends appropriate arguments to kernel’s command line. They are not copied 1:1, that would be too simple. For example, enable_uart=1 results in 8250.nr_uarts=1 being added to the kernel’s command line.

Note

For the sake of clarity: 8250 is a kernel module named “8250”, nr_uarts is the module’s option, and 1 is a new value of this option. This is equivalent of invoking: modprobe 8250 nr_uarts=1 within the shell.

This is important to understand because U-Boot is going to be injected into the boot chain between start.elf and kernel image. Kernel won’t receive these arguments anymore, they all go to U-Boot now, so U-Boot is responsible for supplying them to the kernel’s command line. This sounds tricky, and is even trickier.

The easiest, yet not so flexible method, is to peek the actual command line of the running system, and simply use it in U-Boot as the kernel’s command line. Let’s say the current Raspberry’s configuration is satisfactory, and it’s not supposed to be changed soon. If that’s true, the current command line can be easily displayed with the following command, and saved somewhere for later:

cat /proc/cmdline

Caution

Manipulating the kernel’s command line manually doesn’t mean the config.txt file is no longer needed. Raspberry’s CPU still needs it for example to enable UART or select image to boot.

Hit the red button

U-Boot is ready to be written to the SD card, and to be set as the boot source. In order to do that, u-boot.bin must be copied to the boot partition of the SD card, and an additional entry in config.txt must be added, so the board knows what to boot:

kernel=u-boot.bin

The USB-UART adapter can now be connected to Raspberry with the following options:

Baudrate

115200

Data bits

8

Stop bits

1

Parity

None

Flow control

None

For minicom:

sudo minicom -w -b 115200 -D /dev/tty[something]

As soon as Raspberry is powered on, it should put some U-Boot stuff on the terminal and do nothing more. The task has failed successfully, because there are a couple of additional steps to follow.

Tip

If nothing pops up on the terminal, make sure the communication parameters are correct, and UART is enabled. If that seems to be okay, another cause could be messed up U-Boot build. Check correctness of defconfig, CROSS_COMPILE env, and the whole compilation process.

The extra steps

There are a couple of things to fix in U-Boot so it can boot the kernel properly:

  • Device Tree file is wrong.

  • Kernel’s boot arguments are not set.

  • U-Boot can’t find the image to boot.

Thankfully these are the only inconveniences, and the rest is a pure fun. I promise!

Fix Device Tree file

Raspbian’s kernel requires a Device Tree file to boot properly. This file must be loaded into RAM prior to booting, and the loading address shall be given to kernel. The DT file is read from the SD card, and its name is kept in fdtfile env variable. The current content of the variable can be examined like this:

U-Boot> env print fdtfile
fdtfile=bcm2835-rpi-zero-w.dtb

The problem here is that this file doesn’t exist on the SD card, but there’s another: bcm2708-rpi-zero-w.dtb, so it must be used instead of the missing one:

U-Boot> setenv fdtfile bcm2708-rpi-zero-w.dtb

Fix kernel boot args

For most of the time, kernel needs some additional arguments, let it be location of the root file system, or configuration of the device. These additional parameters are called “command line” or simply “boot arguments (args)”. In U-Boot, there’s a dedicated env variable called bootargs that will be automatically passed to the kernel on boot.

The command line arguments obtained in Know the Kernel command line will be needed here. They must become the new value of the bootargs variable:

U-Boot> setenv bootargs console=ttyS0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Fix kernel image

The last thing to do is to point U-Boot to the valid kernel image that can be actually booted. The kernel image is on the SD card, but it must be inside RAM to be executed. In order to copy anything from an external source like the mentioned SD card, or a USB flash drive, the device must be first selected. Usually it is SD card, so the first step is to select it:

U-Boot> mmc dev 0

The above command selects an MMC device at index 0. Raspberry has only one SD card slot, thus “0” is always a valid option. In contrast, USB can have multiple flash drives connected to it, and each will have a different index; it’s up to the developer to select the correct one. All USB devices can be listed with this command:

U-Boot> usb info

Let’s get back on the track. When the proper MMC device is selected, any file from it can be loaded into RAM with just one command. In this case this will be the kernel image, and the Device Tree file, both of them has to be put somewhere into RAM to be usable. Fortunately, the correct load addresses are already set in U-Boot.

The right command to use is fatload and it does exactly what it means - it loads a file from a FAT partition:

U-Boot> fatload mmc 0:1 ${kernel_addr_r} kernel.img
U-Boot> fatload mmc 0:1 ${fdt_addres} ${fdtfile}

The syntax of the command is:

fatload [device type] [device index]:[partition] [load address] [source file]

The first command copies kernel.img to the address stored in the kernel_addr_r env variable. The image file should be available on the first partition of the MMC 0 device. MMC 0 always points to an SD card slot on a Raspberry board, and the first partition is the boot partition. When the SD card is partitioned differently, the correct numbers has to be figured out beforehand.

The second command works exactly the same and copies the Device Tree file.

Boot!

This time everything is prepared and should work flawlessly. The command to boot the kernel is given below, but don’t do this yet.

U-Boot> bootz ${kernel_addr_r} - ${fdt_addr}

bootz is a command that boots a gzipped kernel image. The additional parameters are:

${kernel_addr_r}

Environment variable with a memory address of where the kernel image was loaded.

-

Boot without initrd.

${fdt_addr}

Environment variable with a memory address of where Device Tree blob file was loaded.

Every (healthy) programmer at this point should start thinking: “Can all of that be automated somehow? This is so much work to do”. Yes! The answer is: environment variables.

U-Boot> setenv rpi_boot 'fatload mmc 0:1 ${kernel_addr_r} kernel.img; fatload mmc 0:1 ${fdt_addr} ${fdtfile}; bootz ${kernel_addr_r} - ${fdt_addr}'
U-Boot> setenv bootcmd run rpi_boot
U-Boot> saveenv

The first command sets a new rpi_boot env variable to the provided string. The string itself is a concatenation of commands given to U-Boot so far. The second command sets a new value for bootcmd variable, and the last command persists environment, meaning that the environment variables will be available after power loss.

Here’s what will happen after the RPi is powered: When U-Boot’s auto-start won’t be interrupted by a key, it will run the default boot command. This command simply executes run bootcmd (run treats arguments like commands), and that eventually executes run rpi_boot with the additional arguments.

Raspberry can now be powered off and on again in order to verify that everything works correctly. U-Boot should appear on the terminal for a short moment, and after a couple of seconds it should proceed with booting the kernel.

Extras

Dynamic kernel command line

The most annoying and fishy step in this tutorial is deliberately hardcoding the kernel command line; this shouldn’t work like that. The command line is generated automatically when Raspberry starts, so it also should be automatically given to the kernel, but U-Boot prevents that from happening. Moreover, it is so stubborn, that I couldn’t find any official way of forcing it to pass the command line down to the kernel image.

I’m stubborn too…

Caution

This is experimental! I can’t guarantee it will work for everyone.

A funny thing will happen when the below command is executed in U-Boot shell:

U-Boot> md.w 0x710 128

This command tells U-Boot to print 128 words starting from address 0x710. The ASCII column of the output should ring a bell:

00000710: 6f63 6568 6572 746e 705f 6f6f 3d6c 4d31    coherent_pool=1M
00000720: 3820 3532 2e30 726e 755f 7261 7374 313d     8250.nr_uarts=1
00000730: 7320 646e 625f 6d63 3832 3533 652e 616e     snd_bcm2835.ena
00000740: 6c62 5f65 6f63 706d 7461 615f 736c 3d61    ble_compat_alsa=
00000750: 2030 6e73 5f64 6362 326d 3338 2e35 6e65    0 snd_bcm2835.en
00000760: 6261 656c 685f 6d64 3d69 2031 6362 326d    able_hdmi=1 bcm2
00000770: 3037 5f38 6266 662e 7762 6469 6874 313d    708_fb.fbwidth=1
00000780: 3832 2030 6362 326d 3037 5f38 6266 662e    280 bcm2708_fb.f
00000790: 6862 6965 6867 3d74 3237 2030 6362 326d    bheight=720 bcm2
000007a0: 3037 5f38 6266 662e 7362 6177 3d70 2031    708_fb.fbswap=1
000007b0: 6d73 6373 3539 7878 6d2e 6361 6461 7264    smsc95xx.macaddr
000007c0: 423d 3a38 3732 453a 3a42 3636 453a 3a44    =B8:27:EB:66:ED:
000007d0: 4337 7620 5f63 656d 2e6d 656d 5f6d 6162    7C vc_mem.mem_ba
000007e0: 6573 303d 3178 6365 3030 3030 2030 6376    se=0x1ec00000 vc
000007f0: 6d5f 6d65 6d2e 6d65 735f 7a69 3d65 7830    _mem.mem_size=0x
00000800: 3032 3030 3030 3030 2020 6f63 736e 6c6f    20000000  consol
00000810: 3d65 7474 5379 2c30 3131 3235 3030 6320    e=ttyS0,115200 c
00000820: 6e6f 6f73 656c 743d 7974 2031 6f72 746f    onsole=tty1 root
00000830: 503d 5241 5554 4955 3d44 3432 6434 3032    =PARTUUID=244d20
00000840: 6337 302d 2032 6f72 746f 7366 7974 6570    7c-02 rootfstype
00000850: 653d 7478 2034 6c65 7665 7461 726f 643d    =ext4 elevator=d
00000860: 6165 6c64 6e69 2065 7366 6b63 722e 7065    eadline fsck.rep
00000870: 6961 3d72 6579 2073 6f72 746f 6177 7469    air=yes rootwait
00000880: 0000 e803 0000 0100 6f62 746f 6f6c 6461    ........bootload

Apparently this is the command line generated for the kernel, but never reaching it, because of U-Boot. It’d be nice to have it as an env variable, so it could be used for booting:

U-Boot> setexpr.s bootargs *0x710

This command sets the env variable (bootargs) to the result of the expression, and the expression itself shall be treated as a string (setexpr**.s**). The expression is a memory pointer treated like a string, so it will contain all characters starting from address 0x710 all the way down until the 0x00 byte. Let’s see the result:

U-Boot> env print bootargs
bootargs=coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_compat_alsa=0 snd_bcm2835.enable_hdmi=1 bcm2708_fb.fbwidth=1280 bcm2708_fb.fbheight=720 bcm2708_fb.fbswap=1 smsc95xx.macaddr=B8:27:EB:66:ED:7C vc_mem.mem_base=0x1ec00000 vc_mem.mem_size=0x20000000  console=ttyS0,115200 console=tty1 root=PARTUUID=244d207c-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

And now integrate it with the boot_rpi command:

U-Boot> setenv boot_rpi 'setexpr.s bootargs *0x710; mmc dev 0; fatload mmc 0:1 ${kernel_addr_r} kernel.img; fatload mmc 0:1 ${fdt_addr} ${fdtfile}; bootz ${kernel_addr_r} - ${fdt_addr}'
U-Boot> saveenv

Thanks to this, the kernel will always boot with proper command line options.

Note

This won’t work for the dtoverlay options in config.txt. Device Tree overlays are applied differently. This is of course possible in U-Boot, but requires extra work.


1

https://www.denx.de/wiki/U-Boot

2

https://elinux.org/RPi_U-Boot