Embedded Linux: Boot, Device Tree, Drivers & Build Systems
When a product needs a rich UI, networking, file systems or cameras, it moves from MCUs to embedded Linux on application processors (Cortex-A). Learn the boot chain (ROM → SPL → U-Boot → kernel → rootfs → init), the device tree, kernel vs user space, character drivers and kernel modules, GPIO/I²C/SPI access from user space, build systems (Buildroot, Yocto), real-time options (PREEMPT_RT, co-processors), and when Linux is — or isn't — the right choice.
Routers, smart displays, drones' companion computers, industrial gateways and car infotainment units run Linux on processors like the i.MX 8, STM32MP1, TI Sitara or Raspberry Pi's Broadcom SoCs. These chips have an MMU, external DRAM and eMMC/SD storage, so they can run a full OS with processes, drivers and networking stacks.
Embedded Linux development is different from MCU work: you configure and build a whole system (bootloader, kernel, root file system), describe hardware in a device tree, and write most application code in user space — while the kernel owns the hardware.
An analogy
An MCU is a food truck where one cook does everything. Embedded Linux is a restaurant: the kitchen (kernel) handles equipment and safety rules, chefs (processes) work on their dishes, and the menu board (device tree) tells the kitchen which appliances are installed.
MCU or embedded Linux?
Choose Linux when you need:
- Rich networking (Wi-Fi + Ethernet + VPN), web servers, containers
- Graphics/UI (Qt, Wayland), cameras, video, audio, AI frameworks
- File systems, databases, many third-party libraries and languages
- Stay on an MCU (or add one) for: hard real-time µs control, very low power, instant boot, lowest cost
Boot chain and device tree
The SoC's boot ROM loads a small SPL that initialises DRAM, which then loads U-Boot. U-Boot loads the kernel image and the device-tree blob (from eMMC, SD, network) and passes control. The kernel probes drivers according to the device tree, mounts the rootfs and starts init (systemd, BusyBox init). Device-tree overlays add hardware (HATs, capes) without rebuilding.
Kernel space vs user space
The kernel runs privileged and owns hardware; applications run in isolated processes and access devices through system calls on files in /dev and /sys (read, write, ioctl, mmap). A bug in a user process crashes only that process; a bug in a driver can crash the system. Prefer existing kernel frameworks (IIO for sensors, hwmon, input, LED, PWM, SPIdev/I2C-dev) before writing custom drivers.
Build systems, updates and real time
Buildroot builds small, simple images quickly; Yocto/OpenEmbedded builds reproducible, layered distributions for products with long lifecycles (BSP layers from silicon vendors, custom recipes, SDKs). Field updates use A/B partitions or tools like RAUC, SWUpdate, Mender. For real time, PREEMPT_RT (mainline since 6.12) reduces worst-case latency to tens of µs; truly hard deadlines are often delegated to a companion Cortex-M core (STM32MP1, i.MX 8M) communicating via RPMsg.
Formulas
- Boot chain:
Boot ROM → SPL (init DRAM) → U-Boot → kernel + DTB → rootfs → init/systemd → apps - Boot time:
t_boot = Σ stage times (typical 2–20 s; optimised < 1–2 s) - Latency (typical):
Standard kernel: ms-level worst case; PREEMPT_RT: tens of µs - Memory footprint:
Minimal Buildroot system: ~8–32 MB RAM, ~10 MB storage; full distro: hundreds of MB
Worked example: Boot-time budget
Given: ROM+SPL 0.3 s, U-Boot 1.5 s (2 s autoboot delay removed), kernel 2.2 s, systemd to app 3 s
Find: Total and quick wins.
- Total = 0.3 + 1.5 + 2.2 + 3 = 7 s
- Wins: falcon mode (skip U-Boot), trim kernel config/modules, start the app early
Answer: 7 s → often < 2 s after optimisation
Worked example: Reading the sensor with zero driver code
Given: tmp102 node in DT
Find: Temperature from shell.
- cat /sys/class/hwmon/hwmon0/temp1_input → 25000
Answer: 25.000 °C (millidegrees)
Worked example: Hybrid system
Given: Motor control needs 20 kHz loop with < 5 µs jitter; UI and cloud on Linux
Find: Architecture.
- Linux can't guarantee µs jitter
- Use SoC's Cortex-M4 for control, RPMsg to Linux
Answer: Asymmetric multiprocessing: Cortex-A Linux + Cortex-M RTOS
Device-tree fragment describing an I²C sensor and an LED
&i2c1 {
status = "okay";
clock-frequency = <400000>;
temp_sensor: tmp102@48 {
compatible = "ti,tmp102"; /* kernel binds the tmp102 driver */
reg = <0x48>; /* I2C address */
};
};
/ {
leds {
compatible = "gpio-leds";
status_led {
label = "status";
gpios = <&gpio4 17 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};The kernel reads the DTB at boot and instantiates drivers for each node. The same kernel binary supports many boards; only the device tree changes. The sensor then appears under /sys/class/hwmon with no C code written.
User-space GPIO with libgpiod (C)
#include <gpiod.h>
#include <unistd.h>
int main(void)
{
struct gpiod_chip *chip = gpiod_chip_open_by_name("gpiochip3");
struct gpiod_line *led = gpiod_chip_get_line(chip, 17);
gpiod_line_request_output(led, "blink-demo", 0);
for (int i = 0; i < 10; i++) {
gpiod_line_set_value(led, i & 1); /* toggle */
usleep(250000); /* 250 ms */
}
gpiod_line_release(led);
gpiod_chip_close(chip);
return 0;
}
/* build: gcc blink.c -lgpiod -o blink (libgpiod v1 API) */User-space code never touches registers; it asks the kernel through a character device (/dev/gpiochipN). The old /sys/class/gpio interface is deprecated.
Skeleton of a loadable kernel module
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void)
{
pr_info("hello: module loaded\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal example module");
/* insmod hello.ko ; dmesg | tail ; rmmod hello */Real drivers register with a subsystem (platform, I²C, SPI, IIO, input) and are matched to device-tree nodes through their compatible strings.
Common mistakes
- Wrong: Writing a custom kernel driver for a common sensor. — Right: Check existing drivers/frameworks and just add a DT node.
- Wrong: Using Linux for µs-level hard real-time control. — Right: PREEMPT_RT or offload to an MCU/co-processor.
- Wrong: No field-update strategy for the rootfs. — Right: A/B partitions with RAUC/SWUpdate/Mender and rollback.
- Wrong: Using deprecated /sys/class/gpio. — Right: Use libgpiod and the character-device API.
Applications
- Gateways & routers: OpenWrt, industrial IoT edge devices.
- HMI & infotainment: Qt/Wayland displays, Automotive Grade Linux.
- Vision & AI: Cameras with NPUs running inference.
- Robotics: ROS 2 on companion computers with MCU co-processors.
Key takeaways
- ROM → SPL → U-Boot → kernel + DTB → rootfs → apps.
- Device tree describes hardware; drivers bind by compatible string.
- Kernel owns hardware; apps use /dev, /sys and syscalls.
- Hybrid Linux + MCU for rich features plus hard real time.
Going further
- Yocto layers & BSPs: meta-vendor layers, recipes, bbappends and SDK generation.
- Zephyr + Linux AMP: OpenAMP/RPMsg between Cortex-A and Cortex-M.
- Containers at the edge: Docker/Podman/balena for updatable application stacks.