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:

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

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.

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.

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.

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

Applications

Key takeaways

Going further