/blog/

2025 0419 U-Boot partition detection

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:

  1. Set the last updated timestamp in a comment at the top of the boot script and as a variable accessible to the boot script1.
  2. In the boot script logic, loop through all known devices to find one containing a /boot.cmd.
  3. Compare the top comment in any found /boot.cmd with the timestamp value in the variable. This determines the boot device and partition.
  4. 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 allow if and else.
  • continue and break 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 -*-

  1. Technically there are two files: A source file usually called boot.cmd, and a “binary script image” called boot.scr compiled from the source file by U-Boot’s mkimage utility. U-Boot only cares about boot.scr and loads it from the root of its boot partition, but I also keep my source boot.cmd right next to it which makes troubleshooting things easier. My logic searches for the boot.cmd plain text source file, not the boot.scr binary script image. ↩︎

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).