Skip to content

Bootloader compatibility mode

In bootloader compatibility mode the bootloader owns the A/B slot switch and the rollback. The OTA-Pulse agent still downloads, verifies and installs the update. It drives the bootloader instead of switching slots itself.

Native mode is the default. It is what the Yocto integration guide describes. Bootloader compatibility mode is opt-in, and it changes nothing for a native build.

The bootloader keeps three things in its own environment. They are the active slot, an “update pending” flag (upgrade_available) and a boot counter (bootcount). The environment is the U-Boot environment on the storage device, or the GRUB environment on the EFI partition.

  1. The agent writes the new slot and sets upgrade_available=1. Then it reboots.
  2. The bootloader boots the new slot and counts the boot.
  3. If the new slot is healthy, the agent clears upgrade_available. That commits the update.
  4. If the new slot keeps failing, the bootloader counts past its limit and switches back to the old slot on its own.

A rollback needs the board to reset. A kernel that panics and then hangs never resets, so the bootloader never gets another turn. That is why every image in this mode must put a positive panic=<seconds> on the kernel command line. The default is 10 seconds, and the build refuses an image without it. See OTAPULSE_PANIC_TIMEOUT_S.

A userspace that hangs while the kernel stays up does not reset the board. A small guard on the device covers that case. It reboots a trial boot that is still uncommitted 20 minutes after boot. See Boot deadline.

On U-Boot the new slot cannot be read, so the kernel never runs. The bootloader resets, counts past its limit and falls back:

Bad Linux ARM64 Image magic!
resetting ...
...
Warning: Bootlimit (1) exceeded. Using altbootcmd.
Saving Environment to MMC... Writing to MMC(0)... OK

On GRUB the same kind of failure looks like this. The next pass flips back to the previous slot:

Booting new update...
error: disk `hd0,msdos2' not found.

When the kernel starts but has no working init, the panic timeout does the reset:

Kernel panic - not syncing: No working init found. ...
Rebooting in 7 seconds..

These lines come from our qualification runs. The 7 is the value those images were built with.

Use bootloader compatibility mode when all of this is true:

  • Your board boots through U-Boot with an environment on MMC or SD, or through GRUB on UEFI.
  • You build with Yocto, or you have a Debian or Raspberry Pi OS image to convert.
  • You want the bootloader to own the rollback.

Use native mode instead when your board is in the verified list of the Yocto integration guide. That mode has hardware proof. NVIDIA Jetson boards stay native.

We say plainly what has been proven and what has not.

TargetBootloaderStatus
QEMU arm64 (virt), SD card with an MBR partition tableU-BootQualified on QEMU. Two complete passes.
QEMU x86-64, UEFI disk with a GPT partition tableGRUB (through OVMF)Qualified on QEMU. Two complete passes.
Raspberry Pi 4 (64-bit)U-BootHardware-qualified on one bench board. Not production-qualified: power loss during install is untested. See below.
NXP i.MX8MP FRDMU-BootNot attempted in this mode. It is proven in native mode.
Debian 12 arm64 images, with otapulse-convertGRUB (UEFI)The conversion works and the image boots under QEMU. An OTA install is not qualified.
Any other boardU-Boot or GRUBNot tested. Follow the qualification guide.

What “qualified” means here. Each QEMU target passed our qualification pipeline twice in a row. One pass runs about 1.5 to 2.5 hours on emulated hardware. It covers registration, monitoring, a real OTA install with commit, interruption and rollback tests, negative tests, and five bad-update cases:

  • a new root file system the bootloader cannot read,
  • a root file system with no usable init,
  • a wedged userspace that never commits,
  • an agent that cannot start because the data seed fails,
  • a standalone install that waits for its own reboot, which must not be rebooted early.

Every bad update was reverted by the bootloader. The server reported the failed deployment, and a healthy install committed afterwards.

What “qualified” does not mean. QEMU runs without hardware acceleration here. It proves the OTA-Pulse logic and the bootloader environment flow. It does not prove your board’s flash timing, its watchdog, its storage, or its power-loss behavior. No staging server run was made. All runs were on a LAN server.

Raspberry Pi 4. The image builds and passes the image checks (100 of 101; the one failure is identical on the native image). It runs on one real bench board, a Raspberry Pi 4 on a LAN server. There the full qualification pipeline passed three times, including the five bad-update cases on real hardware. The security, monitoring, Remote SSH and process-crash suites also passed on it, with the same results as the image in native mode.

What is not proven on this board:

  • Power loss during an install is untested on the Raspberry Pi 4 in bootloader mode. The design writes only the inactive slot and flips the bootloader environment after the write is verified, but no test cut the power in the middle of an install.
  • Serial-console evidence of the kernel panic reset is still pending. The panic setting is present on the running kernel, and the board returned to its original slot after a panic, but the panic message itself was not captured.
  • Several suites were not run on it: the soak test, the large artifact tests and the kernel panic crash case.
  • It is one board, on one SD card, without staging.

Until these are closed, treat the Raspberry Pi 4 in bootloader mode as hardware-qualified on a bench board, not as production-qualified.

You need the OTA-Pulse source tree, which holds the meta-otapulse layer and the board support layers next to it.

  1. Add the layers

    Bootloader mode needs the board support layers in conf/bblayers.conf, before meta-otapulse:

    BBLAYERS ?= " \
    /path/to/poky/meta \
    /path/to/poky/meta-poky \
    /path/to/poky/meta-yocto-bsp \
    /path/to/meta-openembedded/meta-oe \
    /path/to/meta-openembedded/meta-python \
    /path/to/meta-openembedded/meta-networking \
    /path/to/meta-openembedded/meta-multimedia \
    /path/to/ota-pulse/meta-otapulse-bsp/core \
    /path/to/ota-pulse/meta-otapulse-bsp/qemu \
    /path/to/ota-pulse/meta-otapulse \
    "

    Use meta-otapulse-bsp/qemu for QEMU machines. For real boards use the layer under meta-otapulse-bsp/boards/<vendor> that matches your board. A native build must not list these layers.

  2. Set the mode in conf/local.conf

    This is the whole OTA-Pulse part for a U-Boot board.

    INHERIT += "otapulse-full"
    OTAPULSE_BOOT_MODE = "bootloader"
    OTAPULSE_BOOTLOADER = "uboot"
    OTAPULSE_IMAGE_TYPE = "sd"
    OTAPULSE_STORAGE_DEVICE = "/dev/mmcblk0"
    OTAPULSE_STORAGE_TOTAL_SIZE_MB = "2048"
    OTA_SERVER_URL = "https://<your-server>"
    OTAPULSE_PROVISIONING_TOKEN = "spt_..."

    For a GRUB and UEFI board, change three lines:

    OTAPULSE_BOOTLOADER = "grub"
    OTAPULSE_IMAGE_TYPE = "uefi"
    OTAPULSE_STORAGE_DEVICE = "/dev/vda"

    INHERIT must be in local.conf. An inherit line inside a recipe is too late, and the build stops with a message that says so. Every variable is described in the variable reference.

  3. Install the agent in your image

    Your image recipe stays an ordinary image recipe. It must install the agent:

    IMAGE_INSTALL:append = " soc-ota-agent"

    The build checks the finished image for the agent, the boot mode marker and the update scripts. It stops if one is missing.

  4. Make sure the agent has its Go vendor directory

    The agent is built offline from a vendor/ directory that is not in the repository. Create it once, from the agent source directory of the OTA-Pulse tree:

    Terminal window
    cd /path/to/ota-pulse/soc-ota-agent
    go mod vendor
  5. Check the configuration, then build

    Many of the build checks run when BitBake parses the recipes. You can run just that part first:

    Terminal window
    bitbake -p

    Then build your image:

    Terminal window
    bitbake <your-image-recipe>

The build writes the disk image and, unless you set OTAPULSE_GENERATE_ARTIFACT = "0", a signed .otapulse artifact of the root file system. OTAPULSE_IMAGE_TYPE = "sd" produces a .sdimg file and "uefi" produces a .uefiimg file.

The root file system partitions get what is left after the boot partition, the data partition and a small overhead. Set OTAPULSE_STORAGE_TOTAL_SIZE_MB to the size of the medium you flash to. A full-featured image needs room. Our reference image needs about 540 MiB per root file system slot. Its x86-64 disk is 2048 MB, and 1024 MB would leave slots that are too small.

The layers handle the bootloader environment and the boot scripts. A board still needs its own machine configuration, such as the U-Boot defconfig, the device tree name and storage drivers in the kernel. That is board support work and it is yours to supply. The QEMU virt machine needs MMC support added to its U-Boot and kernel, for example. The qualification guide lists what to check.

A new device must register with your server. There are two ways to give it the fleet provisioning token. Both end with the same first-boot registration, which creates a unique per-device API key.

Path 1: bake the token into the image (default)

Section titled “Path 1: bake the token into the image (default)”

Set OTAPULSE_PROVISIONING_TOKEN in local.conf, and leave OTAPULSE_PROVISIONING_MODE at its default baked. The build writes the token into the image as /etc/otapulse/provision.env, mode 0600, one variable. It never goes into a package, the package feed or the package-level shared state cache.

The device registers itself on first boot.

Set OTAPULSE_PROVISIONING_MODE = "manual". The image then carries no provision file. Place the file on each device, then reboot:

Terminal window
umask 077
printf 'OTAPULSE_PROVISIONING_TOKEN=%s\n' 'spt_...' > /data/otapulse/provision.env
reboot

The file must be mode 0600. The data partition survives A/B updates, so the file stays. In our test the device did not register before the file was there. After the file was placed and the device rebooted, it registered once.

This path was booted live on the QEMU arm64 image only. The x86-64 image uses the same code, but we did not boot it in manual mode.

OTAPULSE_PROVISIONING_MODE controls the agent’s token file only. Whether socmond gets a token is decided by your image recipe today. If your recipe writes the token into the socmond configuration, a “manual” image still carries it. Leave it out of the recipe and give the device its token on the device instead: put it in /etc/socmond/provisioning_token (mode 0600). In our reference image the “manual” mode removes that recipe step.

On a converted image the agent and socmond register as two separate server records. See otapulse-convert.

A misconfiguration fails the build, with a message that names the OTAPULSE_* variable to fix. The checks cover the image type against the bootloader, the U-Boot environment layout, the boot counter, the kernel panic timeout, the GRUB debug options and the finished root file system. See Build checks and their messages.

Names from the open-source board support can show up in build errors, build logs and on the device. They are listed under Names you may see in build logs and on devices. You never need a tool with one of those names to build or update. To inspect the GRUB environment by hand, the tool names are listed there too.

  • Mostly QEMU. Apart from the one Raspberry Pi 4 bench board, all qualification ran on emulated machines without KVM. Your hardware is untested until you qualify it.
  • Bench accommodation on arm64. The emulated SD card sometimes stalls the whole virtual CPU for about a minute during the agent’s large read-back after an install. The guest’s software watchdog then rebooted it. Our arm64 test image therefore carries softdog.soft_margin=600 on its kernel command line. That is a bench setting for emulated hardware. Do not copy it to a hardware image.
  • GRUB console. On the x86-64 image the serial console shows kernel messages, not systemd unit status. Some of our evidence for the x86-64 row is therefore indirect.
  • Secure boot. EFI secure boot is not tested. The generated GRUB configuration is not signed.
  • Firmware. In bootloader mode only the root file system slots (and the kernel inside them) are updated. The boot partition, the bootloader and the board firmware are written by the image only.
  • Remote SSH. The QEMU bootloader-mode images were not qualified with Remote SSH (the Raspberry Pi 4 bench board passed the Remote SSH suites). On the arm64 image the first-boot service defers the remote shell tunnel credential and logs that it did. The x86-64 image completed the tunnel provisioning.
  • Boot deadline limits. See Boot deadline.