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
- ROM bootloader: You never think about it. It exists.
- SPL: Only on Cortex-A systems. Usually built by vendor (you rarely touch it).
- U-Boot: You interact with this. Can edit environment, boot arguments.
- Kernel: Your OS. Massive complexity. But provided by distros (Raspbian, etc.).
- 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.