I wrote a U-Boot script that searches for itself on all attached storage in order to find the booted partition. Here’s how it works:
- Set the last updated timestamp in a comment at the top of the boot script and as a variable accessible to the boot script1.
- In the boot script logic,
loop through all known devices to find one containing a
/boot.cmd
. - Compare the top comment in any found
/boot.cmd
with the timestamp value in the variable. This determines the boot device and partition. - Once the boot device is known,
look in other partitions for the A and B operating system partitions.
We place a file with the name of the boot label in the root of the A and B filesystems
(
/psyopsOS-A
on the A side and/psyopsOS-B
on the B side). While U-Boot cannot search partitions by their label, it can try to load a file of any name from a partition and know if it succeeded or failed.
Limitations and workarounds
This was a fun project because there were a lot of constraints to wrangle. U-Boot lacks:
else if
; conditionals only allowif
andelse
.continue
andbreak
from loops- The ability to search for a partition by label
- The ability to compare strings in memory to strings in variables
Rationale
psyopsOS uses separate A and B operating system partitions, such that updates can by applied to the non-booted side for safety. On typical UEFI x86_64 PC hardware, it boots GRUB, and GRUB finds the correct A or B partition based on the partition label. However, I recently added psyopsOS support for the Raspberry Pi 4B booted with U-Boot, and U-Boot scripts cannot easily find a partition by its label. I had to find another solution.
Complete psyopsOS U-Boot script
This is an installed boot script.
Note that the top section —
everything above the Welcome to psyopsOS
message —
is created from a template and may change from install to install.
The last updated timestamp at the top and in the $last_updated
variable
# 20250419-133632
# IMPORTANT: LEAVE THE LINE ABOVE THIS COMMENT ALONE.
# This script doesn't know what partition contains it,
# but it can ask U-Boot to find all partitions,
# load boot.cmd if it exists,
# and compare the contents of the first few bytes.
# psyopsOS boot.cmd
# Variables populated by neuralupgrade
#### The next line is used by neuralupgrade to show information about the current configuration.
# neuralupgrade-info: last_updated=20250419-133632 default_boot_label=psyopsOS-A
####
# The date this was last updated
# IMPORTANT: DO NOT CHANGE THE NAME OF THIS VARIABLE.
# The length of the variable **name** must be accounted for in the memory comparison below.
setenv last_updated 20250419-133632
# The partition labels for the A and B filesystems.
setenv psyopsOS_A_label psyopsOS-A
setenv psyopsOS_B_label psyopsOS-B
# The default boot label (must be one of A or B)
setenv default_boot_label psyopsOS-A
# Starting addresses for kernel and initramfs.
# These are JUST used during boot and do not affect OS runtime;
# Linux will move itself around in memory as needed,
# and initrd is mounted to a tmpfs instance at boot,
# so the original memory buffer from U-Boot is freed.
setenv kernel_addr_r 0x80200000
setenv ramdisk_addr_r 0x82200000
setenv dtb_addr_r 0x8a200000
setenv dtbo_1_addr_r 0x8a100000
# Kernel commandline parameters to each A/B side
setenv kernel_params "earlyprintk=dbgp console=tty0 console=ttyAMA0,115200 loglevel=7"
echo "Welcome to psyopsOS. U-Boot configuration last updated: ${last_updated}"
echo ""
# Probe for psyopsOS A and B partitions by their partition label.
# Currently only look on mmc and usb devices because my only U-Boot platform is the Raspberry Pi.
# setenv probe_devtypes "mmc usb sata scsi"
setenv probe_devtypes "mmc usb"
echo "Probing for psyopsOS boot.cmd on ${probe_devtypes} devices..."
setenv bootdev_type ""
setenv bootdev_num ""
for devtype in ${probe_devtypes}; do
if test "${bootdev_type}" = ""; then
if test ${devtype} = "usb"; then
# This takes a few seconds; only bother if we didn't find anything on mmc
echo "Initializing USB..."
usb start
echo "USB initialized"
fi
for devnum in 0 1 2; do
if test "${bootdev_num}" = ""; then
echo "Probing ${devtype} ${devnum} for boot.cmd..."
if ${devtype} dev ${devnum}; then
# Assume that the boot partition will only ever be the first one on a device
if load ${devtype} ${devnum}:1 ${kernel_addr_r} /boot.cmd; then
echo "Found boot.cmd on ${devtype} ${devnum}:1"
# We know we found **SOME** boot.cmd, but we don't know if it's the one that's currently running.
# Because the load worked, ${kernel_addr_r} has a value of something like '# 20250415-003744...'
# Skip two characters to skip the '# ':
setexpr header_timestamp_r ${kernel_addr_r} + 0x2
# Now build a string in RAM that contains **this** boot.cmd's last_updated timestamp.
env export -t ${ramdisk_addr_r} last_updated
# Now ${ramdisk_addr_r} has a value of something like 'last_updated=20250415-003744'
# Skip 13 characters to skip the 'last_updated=':
setexpr this_timestamp_r ${ramdisk_addr_r} + 0xd
# Compare the two timestamps, which are 15 bytes each
if cmp.b ${header_timestamp_r} ${this_timestamp_r} 0xf; then
echo "Found our own boot.cmd file on ${devtype} ${devnum}:0"
setenv bootdev_type ${devtype}
setenv bootdev_num ${devnum}
else
echo "Found a DIFFERENT boot.cmd file on ${devtype} ${devnum}:0, not using it"
fi
else
echo "No boot.cmd found on ${devtype} ${devnum}:0"
fi
else
echo "No ${devtype} ${devnum} device found"
fi
fi
done
fi
done
if test "${bootdev_type}" = ""; then
echo "ERROR: No bootable device found! Dropping into U-Boot shell..."
exit
fi
echo "psyopsOS: Found bootable device: ${bootdev_type} ${bootdev_num}"
echo "psyopsOS: Probing for psyopsOS A and B partitions..."
setenv psyopsOS_A_partnum ""
setenv psyopsOS_B_partnum ""
setenv psyopsOS_A_msg "NOT FOUND"
setenv psyopsOS_B_msg "NOT FOUND"
for partnum in 2 3 4 5 6 7 8; do
if test "${psyopsOS_A_partnum}" = ""; then
if load ${bootdev_type} ${bootdev_num}:${partnum} ${kernel_addr_r} ${psyopsOS_A_label}; then
echo "Found ${A_label} on ${bootdev_type} ${bootdev_num}:${partnum}"
setenv psyopsOS_A_partnum ${partnum}
setenv psyopsOS_A_msg "on ${bootdev_type} ${bootdev_num}:${partnum}"
fi
fi
if test "${psyopsOS_B_partnum}" = "" && test "${psyopsOS_A_partnum}" != "${partnum}"; then
if load ${bootdev_type} ${bootdev_num}:${partnum} ${kernel_addr_r} ${psyopsOS_B_label}; then
echo "Found ${B_label} on ${bootdev_type} ${bootdev_num}:${partnum}"
setenv psyopsOS_B_partnum ${partnum}
setenv psyopsOS_B_msg "on ${bootdev_type} ${bootdev_num}:${partnum}"
fi
fi
done
# Exit with an error if it can't find anything to boot
if test "${psyopsOS_A_partnum}" = "" && test "${psyopsOS_B_partnum}" = ""; then
echo "ERROR: No psyopsOS partitions found! Dropping into U-Boot shell..."
exit
fi
echo "psyopsOS: Finished probing for psyopsOS A and B partitions"
# Location for the dtb and dtbo files for the kernel, relative to the A/B partitions.
# (Remember that U-Boot has its own dtb and dtbo files in the boot partition,
# which may be different versions.)
dtb_file_path="/dtbs/bcm2711-rpi-4-b.dtb"
dtbo_file_path="/dtbs/overlays/disable-bt.dtbo"
# Boot logic for psyopsOS_A
setenv boota "
echo Booting psyopsOS-A...;
setenv bootargs "psyopsos=psyopsOS-A ${kernel_params}" &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_A_partnum} ${kernel_addr_r} /kernel &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_A_partnum} ${ramdisk_addr_r} /initramfs &&
setenv initrdsize \${filesize} &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_A_partnum} ${dtb_addr_r} ${dtb_file_path} &&
fdt addr ${dtb_addr_r} &&
fdt resize &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_A_partnum} ${dtbo_1_addr_r} ${dtbo_file_path} &&
fdt apply ${dtbo_1_addr_r} &&
fdt print /soc/serial@7e201000 &&
fdt print /aliases &&
echo "Booting kernel..." &&
booti ${kernel_addr_r} ${ramdisk_addr_r}:\${initrdsize} ${dtb_addr_r}
"
# Boot logic for psyopsOS_B
setenv bootb "
echo Booting psyopsOS-B...;
setenv bootargs "psyopsos=psyopsOS-B ${kernel_params}" &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_B_partnum} ${kernel_addr_r} /kernel &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_B_partnum} ${ramdisk_addr_r} /initramfs &&
setenv initrdsize \${filesize} &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_B_partnum} ${dtb_addr_r} ${dtb_file_path} &&
fdt addr ${dtb_addr_r} &&
fdt resize &&
ext4load ${bootdev_type} ${bootdev_num}:${psyopsOS_B_partnum} ${dtbo_1_addr_r} ${dtbo_file_path} &&
fdt apply ${dtbo_1_addr_r} &&
fdt print /soc/serial@7e201000 &&
fdt print /aliases &&
echo "Booting kernel..." &&
booti ${kernel_addr_r} ${ramdisk_addr_r}:\${initrdsize} ${dtb_addr_r}
"
# Make due with this message in lieu of bootmenu support,
# which Alpine Linux U-Boot doesn't include.
#
# A note about exiting to the U-Boot shell:
# "exit" stops execution of this script,
# and immediately tries to boot via some fallback method that I can't seem to disable
# that involves BOOTP and TFTP addresses like this from a Raspberry Pi 4 B:
# pxelinux.cfg/01-dc-a6-32-ab-ca-fc This is the MAC address of the device
# pxelinux.cfg/C0A8011A This is the DHCP IP address of the device as an integer
# pxelinux.cfg/C0A8011 Removing one digit at a time from the IP address
# pxelinux.cfg/C0A801 ... etc down to just "C"
# pxelinux.cfg/default-arm-bcm283x-rpi
# pxelinux.cfg/default-arm-bcm283x
# pxelinux.cfg/default-arm
# pxelinux.cfg/default
# pxelinux.cfg/C0A8011A.img
# That means that ctrl-c here will go through all those,
# and you'll have to ctrl-c through each one to get to the U-Boot shell.
# I can't seem to disable this behavior. I've tried:
# setenv preboot
# setenv autoload no
# setenv netretry 0
# setenv bootcmd ''
# setenv boot_targets "mmc usb"
# I've also tried
# - setenv bootcmd "exit", and other values
# - using 'sh' instead of 'exit', which returns to the parent scope immediately when run noninteractively
#
echo "psyopsOS: Ready to boot"
echo ""
echo "Press ctrl-c to cancel and drop into U-Boot shell"
echo "(U-Boot will then try to netboot a bunch of targets; ctrl-c repeatedly to cancel that too)"
echo "Once in the shell:"
echo " run boota: Boot psyopsOS-A (${psyopsOS_A_msg})"
echo " run bootb: Boot psyopsOS-B (${psyopsOS_B_msg})"
echo " bdinfo: Show board information"
echo " reset: Reboot the board"
echo ""
echo "Will boot into ${default_boot_label} in 5 seconds..."
# "setenv bootdelay 5" doesn't work here
# Not sure if that's because Alpine U-Boot is built with CONFIG_BOOTDELAY=2 and its only read once?
sleep 5
if test $? = 1; then
# ctrl-c in sleep returns 1.
echo "Dropping into U-Boot shell..."
exit
fi
# Boot commands for each label
if test "${psyopsOS_A_partnum}" != ""; then
if test "${default_boot_label}" = "${psyopsOS_A_label}"; then
run boota
fi
fi
if test "${psyopsOS_B_partnum}" != ""; then
if test "${default_boot_label}" = "${psyopsOS_B_label}"; then
run bootb
fi
fi
echo "ERROR: Default boot label ${default_boot_label} not found!"
exit
# -*- mode: sh -*-
-
Technically there are two files: A source file usually called
boot.cmd
, and a “binary script image” calledboot.scr
compiled from the source file by U-Boot’smkimage
utility. U-Boot only cares aboutboot.scr
and loads it from the root of its boot partition, but I also keep my sourceboot.cmd
right next to it which makes troubleshooting things easier. My logic searches for theboot.cmd
plain text source file, not theboot.scr
binary script image. ↩︎