Skip to content

03 · The Boot Flow

Between applying power and seeing login: on the console, an embedded Linux board walks through a fixed relay race: boot ROM → SPL → U-Boot → kernel → init. Each runner owns one job, does it, and hands the baton forward. When a board "doesn't boot," debugging is simply asking which runner dropped the baton — so this mental model is the single most useful diagnostic tool in embedded Linux. This module walks the whole race, then compares it with how a PC (UEFI/BIOS) does the same thing.

Stage 0 — Boot ROM: the code you can't change

The first instructions ever executed live in mask ROM inside the SoC, burned at the factory. The boot ROM is tiny and does exactly one thing: find the next stage and load it.

  • Where does it look? Determined by boot mode pins/fuses — physical straps or one-time-programmable fuses. On i.MX chips these select eMMC, SD card, FlexSPI NOR flash, or serial download mode (USB recovery — the reason a "bricked" i.MX board is almost never truly bricked: NXP's uuu tool can always push a new image over USB).
  • What can it load into? Only on-chip SRAM (a few hundred KB) — because external DDR is not initialized yet. This size limit is the entire reason the next stage exists.
  • On secure-boot products, the ROM also verifies the next stage's signature (i.MX calls this HAB/AHAB — Level 4 territory).

Stage 1 — SPL: waking up the RAM

The SPL (Secondary Program Loader — U-Boot's first stage, small enough to fit in SRAM) has one critical job: initialize the DDR controller. DDR training is chip- and board-specific (timings, drive strengths, the exact RAM part you soldered), which is why SPL comes from your board's BSP, not from generic code. With DDR alive, SPL loads full U-Boot into it and jumps.

On modern i.MX parts the picture is richer: the boot image also carries ARM Trusted Firmware and system-manager firmware for the M33 core, bundled by NXP's imx-mkimage into one signed container the ROM understands. Same relay race, more batons.

Stage 2 — U-Boot: the loader you can talk to

U-Boot is the workhorse bootloader of embedded Linux. Now running from DDR with drivers for storage and network, it:

  1. Loads three things into RAM: the kernel image (Image on ARM64), the device tree blob (.dtb — module 7), and optionally an initramfs.
  2. Sets kernel arguments (bootargs) — e.g. console=ttyAMA0 root=/dev/mmcblk0p2 rootwait.
  3. Jumps to the kernel, passing the DTB address.

Interrupt it over serial and you get a shell — the embedded engineer's best friend:

Hit any key to stop autoboot:  0
u-boot=> printenv bootargs
bootargs=console=ttyAMA0 root=/dev/vda rootwait
u-boot=> ls mmc 0:1          # list files on first eMMC partition
u-boot=> booti ${loadaddr} - ${fdtaddr}   # boot: kernel, (no initrd), dtb

Stage 3 — Kernel: from "Booting Linux" to mounting root

The kernel decompresses, reads the device tree to learn what hardware exists, initializes drivers, then must mount the root filesystem named by root=. Two paths:

  • Direct mountroot=/dev/mmcblk0p2 and the driver is built in. Typical embedded fast path.
  • initramfs first — a small cpio filesystem in RAM runs early userspace (load storage modules, unlock encrypted disks, pick an A/B update slot) and then pivots to the real root. Desktop distros always do this; embedded uses it when updates or encryption demand it.

You can replay the whole kernel stage after boot:

$ dmesg | head -4
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd050]
[    0.000000] Linux version 6.6.x ...
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] earlycon: pl11 at MMIO 0x0000000009000000

Stage 4 — init: PID 1 and the login prompt

The kernel starts exactly one userspace program — init, PID 1 (by convention /sbin/init; override with init= in bootargs). Init mounts /proc and /sys, starts services, and spawns getty on the console — which prints the login: prompt. Everything running on the system descends from PID 1. Module 8 covers the two embedded choices: BusyBox init and systemd.

The full relay, and how x86 does it

Power on
  └─ Boot ROM (in SoC, immutable)  — reads fuses, finds boot media
      └─ SPL (in SRAM)             — initializes DDR
          └─ U-Boot (in DDR)       — loads kernel + DTB (+ initramfs)
              └─ Linux kernel      — drivers, mounts rootfs
                  └─ init (PID 1)  — services, getty → login:
Stage Embedded ARM (i.MX95) PC (x86-64)
First code SoC boot ROM, reads boot fuses/pins UEFI/BIOS flash on the motherboard
RAM init SPL (from your BSP) UEFI firmware (board vendor's)
Loader U-Boot GRUB / systemd-boot (via UEFI boot manager)
Hardware description Device tree blob passed to kernel ACPI tables provided by firmware
Kernel image Image + .dtb bzImage
Early userspace Optional initramfs Practically always initramfs
Recovery story Serial download mode (uuu over USB) Boot from USB stick

The deep difference is the middle rows: on x86 the firmware owns RAM init and self-describes the hardware via ACPI, so one generic Ubuntu ISO boots on any PC. On embedded ARM, you (via the BSP) own RAM init and must hand the kernel an explicit device tree — the price of custom silicon, and the reason BSPs exist.

Cheat sheet

Term Meaning
Boot ROM Immutable first-stage loader inside the SoC
Boot fuses / mode pins Select boot source (eMMC / SD / NOR / serial download) on i.MX
Serial download mode i.MX USB recovery path; uuu tool pushes images — unbrickable
SPL Tiny U-Boot stage in SRAM; initializes DDR
U-Boot Full bootloader: shell, env, loads kernel + DTB, booti
bootargs Kernel command line set by U-Boot (console=, root=, init=)
DTB Compiled device tree — hardware description handed to the kernel
initramfs Optional RAM-based early userspace before the real rootfs
init / PID 1 First process; parents everything; prints login: via getty
UEFI + ACPI The x86 equivalents of (SPL+U-Boot) + device tree

How It Actually Works

What actually happens inside each handoff is a set of concrete memory-and-register events, not an abstract "loads and jumps":

  • ROM → SPL: the ROM computes a checksum (or, on secure-boot parts, verifies a signature) over the image it finds at the fixed offset its fuses point to, copies it byte-for-byte into on-chip SRAM at a fixed address, and does a bare branch to that address — there is no return. If the checksum fails, the ROM falls through its list of boot devices in fuse-defined order, which is why swapping an SD card for eMMC can silently "fix" a boot: it changes which media the ROM tries next.
  • SPL's DDR init isn't generic — it writes dozens of registers in the DDR PHY and controller (drive strength, ODT, CAS latency, refresh timing) using values captured by running the vendor's DDR training tool against your exact RAM part and PCB trace lengths. Get one timing wrong and you don't get a crash, you get intermittent bit-flips under thermal load months into production — which is why this table is board-BSP-owned and never hand-edited.
  • On ARMv8 boards there's a hidden actor between SPL and U-Boot: ARM Trusted Firmware (ATF/TF-A) drops the CPU from EL3 (secure monitor) to EL2, installs the PSCI (Power State Coordination Interface) handler that the kernel later calls to bring up secondary cores and handle suspend/reset, then hands off to U-Boot at a lower exception level. This is why a kernel panic sometimes still lets you power-cycle cleanly — PSCI's reset path lives below Linux entirely.
  • U-Boot → kernel is a documented ABI, not a jump into the unknown: U-Boot places the DTB address in register x0 (AArch64) and the kernel's own first instructions relocate themselves, decompress if needed, and only then set up the MMU and jump to start_kernel() in C.

Exercise

From a running Linux system (your PC, or the QEMU guest you'll boot in module 4), reconstruct its boot story: run cat /proc/cmdline and identify what set each argument (bootloader) and what consumes it (kernel or init); run dmesg | grep -i "Machine model\|command line\|Freeing unused" to find the device-tree model name and the moment the kernel finishes; and run ps -p 1 -o comm= to name PID 1. Then, on paper: your i.MX95 board boots from eMMC but the kernel panics with VFS: Unable to mount root fs on unknown-block(0,0). Which relay stage dropped the baton, which stages provably succeeded, and which single bootargs variable would you inspect first in the U-Boot shell?