The Stack: From Silicon to Application

When you power on an embedded device, software executes in layers. Most developers jump straight to firmware—but the bootloader runs first, and it’s invisible magic until something breaks.

┌─────────────────────────────┐
│    Application Code         │
│  (Your firmware here)       │
├─────────────────────────────┤
│   Operating System (Linux)  │
│   Or RTOS (FreeRTOS)        │
├─────────────────────────────┤
│ Second-Stage Bootloader     │
│  (U-Boot, or bootloader)    │
├─────────────────────────────┤
│  First-Stage Bootloader     │
│  (SPL, ROM bootloader)      │
├─────────────────────────────┤
│    CPU Silicon              │
│    (ROM, CPU, clocks)       │
└─────────────────────────────┘

Let’s break each layer down.


ROM Bootloader: The Unbreakable Foundation

What it is: Code burned into ROM (Read-Only Memory) at the factory, by ARM, or your CPU vendor.

What it does:

  • Initialize CPU clocks and power domains
  • Test external DRAM (if present)
  • Search for bootloader in flash, SD card, or UART
  • Jump to it, or hang with LED blinks if nothing found

You cannot change it. If ROM bootloader has a bug, you’re stuck. This is why silicon revisions matter.

Examples:

  • ARM Cortex-A: ROM bootloader on i.MX8, STM32MP1, Raspberry Pi (ROM burned on-chip)
  • STM32 MCU: System bootloader in ROM allows UART programming if you brick the flash
  • RP2040: Boots from ROM first; can enter bootloader mode (USB device) if flash is empty

The ROM bootloader’s only job: “Find something to run, or fail loudly.”


First-Stage Bootloader (SPL): The Minimalist

What it is: Tiny program (~64KB), typically called SPL (Secondary Program Loader) or FSBL (First-Stage BootLoader).

What it does:

  • Initialize DDR DRAM (complicated—requires vendor tuning, timing, PHY calibration)
  • Set up DDR training (some SoCs do this in ROM, but SPL usually does it)
  • Load and run second-stage bootloader from flash
  • Fit in 64KB because that’s all that fits in on-chip SRAM

You build and sign it. If SPL is corrupt, your device is brick—the ROM bootloader won’t find anything to execute.

Examples:

  • U-Boot SPL: Standard on SoCs like i.MX6, Allwinner, Rockchip
  • TI OMAP: Uses x-loader (predecessor to SPL concept)
  • Raspberry Pi 4: Has stage1 and stage2 bootloaders before U-Boot

DDR initialization is the bottleneck here: SPL exists because initializing complex DDR memory takes 50+ milliseconds and vendor-specific code.


Second-Stage Bootloader: The Flexible Bridge

What it is: Program you interact with (U-Boot, Barebox, LILO, GRUB). Typically 1–2 MB in flash.

What it does:

  • Print startup messages to serial console
  • Load kernel from flash or network
  • Parse command-line arguments
  • Run scripts (e.g., uEnv.txt, boot.scr)
  • Handle boot modes (MMC, NAND, network boot)
  • Jump to kernel entry point

You control it completely. This is where you can interrupt the boot sequence and drop into a bootloader shell (type stop or hit a key during U-Boot splash screen).

Examples:

  • U-Boot: Used by 90% of embedded Linux systems (Toradex, Beaglebone, etc.)
  • Barebox: Lighter, faster boot, used in industrial systems
  • LILO/GRUB: Legacy x86; rarely used in embedded
  • MCU bootloaders: STM32 bootloader, BlueLoader, custom bootloaders

U-Boot on STM32MP1:

# In serial console during boot, type:
=> printenv           # View boot environment
=> setenv bootargs "root=/dev/mmcblk0p2 console=ttySTM0,115200"
=> boot               # Jump to kernel

Operating System: The Scheduler and Resource Manager

What it is: Kernel that abstracts hardware and manages processes.

Embedded Linux

What it does:

  • Manage processes and threads (scheduling)
  • Provide virtual memory via MMU
  • Handle file systems (ext4, ubi, jffs2)
  • Handle network stack (TCP/IP)
  • Interrupt handling, device drivers

You build and configure it with Buildroot, Yocto, OpenEmbedded, or manually.

Examples: Linux kernel on Raspberry Pi, BeagleBone, i.MX8, OpenWrt routers.

Boot time: Takes 5–30 seconds after U-Boot finishes.

RTOS: Real-Time Operating System

What it is: Lightweight kernel for MCUs. No MMU, no virtual memory.

What it does:

  • Guaranteed task scheduling with priority levels
  • Interrupt handling and timer services
  • Memory pool management
  • Optional networking stack

Examples: FreeRTOS, RIOT, ChibiOS, Zephyr.

Boot time: Milliseconds.


Bare Metal: No Operating System

What it is: Just your firmware, no abstraction layer.

When to use:

  • Simple tasks: blink LED, read sensor, transmit UART
  • Interrupt handlers entirely your responsibility
  • No scheduler—you manage timing

Examples: Arduino sketches, embedded bootloaders themselves, PLC firmware.


Why This Hierarchy Exists

On Microcontrollers (Cortex-M, no MMU):

┌─ Application (main.c)
├─ Optional RTOS (FreeRTOS)
├─ Custom bootloader
└─ System bootloader (ROM)

Why? MCUs don’t have DDR to initialize, so ROM bootloader is simple. You might add a custom bootloader for signature verification or OTA updates.

On Microprocessors (Cortex-A, with MMU):

┌─ Application (Linux userspace)
├─ Linux kernel
├─ U-Boot (second-stage bootloader)
├─ SPL (first-stage bootloader)
└─ ROM bootloader

Why? Complex DDR initialization requires SPL. Complex OS (Linux) requires U-Boot’s flexibility.


Real-World Examples

STM32H7 (MCU, no OS)

// ROM bootloader invoked automatically
// Looks for valid firmware in flash at 0x08000000
// Jumps to main()

int main(void) {
    HAL_Init();
    while(1) {
        // Your firmware
    }
}

Boot time: Microseconds. No bootloader involved if you’re not upgrading firmware.

STM32MP1 (MPU with Cortex-A and Cortex-M)

ROM bootloader → SPL (from NAND at offset 0) → U-Boot → Linux kernel
                                                        → Cortex-M4 firmware

Boot time: ~2 seconds to Linux shell.

Raspberry Pi 4

ROM bootloader → bootcode.bin → start*.elf → u-boot.bin → Linux kernel

Boot time: ~5 seconds (slow because of USB device enumeration).


When You Need to Care

Custom bootloader:

  • Firmware signing verification
  • A/B firmware switching (OTA updates)
  • Boot from custom storage (encrypted NAND)
  • Custom SoC initialization

Bootloader override:

  • Device gets stuck (ROM bootloader fallback via UART)
  • Recover a bricked device
  • Force enter bootloader mode (USB or serial)

Example: STM32 Recovery

# Device is bricked (corrupted flash)
# Connect BOOT0 pin to VDD, hold RESET
# ROM bootloader enters UART mode on UART1
# Use STM32CubeProgrammer to reflash

The Real Picture

  1. ROM bootloader: You never think about it. It exists.
  2. SPL: Only on Cortex-A systems. Usually built by vendor (you rarely touch it).
  3. U-Boot: You interact with this. Can edit environment, boot arguments.
  4. Kernel: Your OS. Massive complexity. But provided by distros (Raspbian, etc.).
  5. Application: Your code. Either runs in Linux userspace or bare metal.

Golden rule: Each layer does one job well. Don’t skip layers. Bootloader bugs cascade to OS instability.

For MCUs? Forget this stack exists. ROM bootloader + your firmware = done.

For Embedded Linux? U-Boot is your friend. Learn to control it.