4 min read
On this page

Embedded Linux

Embedded Linux vs RTOS

Aspect Embedded Linux RTOS (FreeRTOS, Zephyr)
Minimum RAM ~8 MB (practical: 32+ MB) ~2 KB
Storage ~4 MB (minimal), typically 16+ MB ~32 KB flash
Boot time Seconds Milliseconds
Scheduling Soft real-time (with PREEMPT_RT: firm) Hard real-time
Networking Full TCP/IP, Wi-Fi, BLE, cellular Limited, protocol-specific
File systems ext4, UBIFS, SquashFS, tmpfs FAT (optional), LittleFS
User space Full POSIX, multi-process Single address space
MMU Required Not required
Drivers Thousands available upstream Vendor-specific

Choose embedded Linux when you need networking, file systems, multimedia, or complex application logic. Choose an RTOS when you need deterministic timing, minimal resources, or hard real-time guarantees.

Build Systems

Yocto Project

An industry-standard framework for building custom Linux distributions for embedded devices. It generates everything: bootloader, kernel, root filesystem, SDK.

Key Concepts

  • Recipes (.bb): build instructions for a single package
  • Layers: collections of recipes (meta-oe, meta-raspberrypi, meta-custom)
  • BitBake: the build engine that processes recipes
  • Machine: target hardware definition (beaglebone, raspberrypi4, custom board)
  • Distro: distribution policy (init system, libc, features)
  • Image: the final output defining what packages to include

Yocto Workflow

# Clone Poky (reference distribution)
git clone git://git.yoctoproject.org/poky -b scarthgap
cd poky

# Initialize build environment
source oe-init-build-env build

# Configure target machine in conf/local.conf
# MACHINE = "raspberrypi4-64"

# Build a minimal image
bitbake core-image-minimal

# Output: build/tmp/deploy/images/raspberrypi4-64/
#   - core-image-minimal-raspberrypi4-64.wic.bz2  (SD card image)
#   - Image  (kernel)
#   - modules-*.tgz  (kernel modules)

Custom Yocto Recipe

# recipes-app/myapp/myapp_1.0.bb
SUMMARY = "My embedded application"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=..."

SRC_URI = "git://github.com/user/myapp.git;branch=main;protocol=https"
SRCREV = "abc123..."
S = "${WORKDIR}/git"

inherit cargo  # For Rust applications

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${B}/target/${CARGO_TARGET_SUBDIR}/myapp ${D}${bindir}/
}

Buildroot

A simpler alternative to Yocto for generating embedded Linux systems. Uses Kconfig (like the kernel) for configuration.

git clone https://github.com/buildroot/buildroot.git
cd buildroot

# Configure for a target board
make raspberrypi4_64_defconfig

# Customize packages, kernel, bootloader
make menuconfig

# Build everything
make -j$(nproc)

# Output: output/images/sdcard.img
Feature Yocto Buildroot
Complexity High (steep learning curve) Moderate
Flexibility Very high (layers, classes) Good (Kconfig)
Package management opkg, rpm, deb None (static image)
Incremental builds Yes (sstate cache) Limited
SDK generation Yes Yes
Industry adoption Very high High

Device Tree

The device tree describes hardware to the kernel in a structured, machine-readable format. It replaces hard-coded board files and allows a single kernel binary to support multiple boards.

Device Tree Source (DTS)

/ {
    compatible = "vendor,board-name";
    model = "My Custom Board";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB at 0x80000000 */
    };

    /* I2C bus with a temperature sensor */
    i2c@44e0b000 {
        compatible = "ti,omap4-i2c";
        reg = <0x44e0b000 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;
        clock-frequency = <400000>;
        status = "okay";

        temperature-sensor@48 {
            compatible = "ti,tmp102";
            reg = <0x48>;
        };
    };
};

Device Tree Compilation

# Compile DTS to DTB (binary)
dtc -I dts -O dtb -o board.dtb board.dts

# Decompile DTB back to DTS for inspection
dtc -I dtb -O dts -o board.dts board.dtb

The bootloader (U-Boot) passes the DTB to the kernel at boot. Device tree overlays allow runtime modification for add-on boards (HATs, capes).

U-Boot Bootloader

Das U-Boot is the standard bootloader for embedded Linux. It initializes hardware, loads the kernel and device tree, and passes control to Linux.

Boot Sequence

Power On
   |
ROM Bootloader (SoC built-in)
   |
SPL (Secondary Program Loader) -- initializes DRAM
   |
U-Boot proper -- full bootloader with shell
   |
Load kernel (Image/zImage) + DTB + initramfs from storage
   |
Boot Linux: bootz/booti <kernel_addr> <initrd_addr> <dtb_addr>

U-Boot Environment

# U-Boot prompt
=> setenv bootcmd 'load mmc 0:1 ${kernel_addr_r} Image; load mmc 0:1 ${fdt_addr_r} board.dtb; booti ${kernel_addr_r} - ${fdt_addr_r}'
=> setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait'
=> saveenv
=> boot

Kernel Configuration

The Linux kernel is configured via Kconfig. For embedded systems, minimize the kernel to reduce boot time, memory usage, and attack surface.

# Start from a defconfig for your SoC
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

# Customize configuration
make ARCH=arm64 menuconfig

# Key embedded options:
# - Disable unused filesystems, network protocols, drivers
# - Enable CONFIG_EMBEDDED for additional size options
# - Enable CONFIG_CC_OPTIMIZE_FOR_SIZE
# - Disable CONFIG_PRINTK for extreme size reduction
# - Enable CONFIG_MODULES=n for monolithic kernel (faster boot)

# Build
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

Linux Kernel Drivers

Driver Types

Type Location Advantages Disadvantages
Built-in (y) Kernel image Available at boot, no loading Increases kernel size
Module (m) Separate .ko file Loaded on demand Slight load overhead
User-space /dev, sysfs, UIO Simpler development, no kernel crash Higher latency

Simple Platform Driver Skeleton (C)

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>

static int my_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    void __iomem *base;

    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    dev_info(dev, "probed successfully\n");
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");

sysfs and procfs

sysfs (/sys)

Exposes kernel objects (devices, drivers, buses) as a filesystem hierarchy:

# Read CPU frequency
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq

# Control GPIO from user space
echo 17 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio17/direction
echo 1 > /sys/class/gpio/gpio17/value

# Read I2C sensor via hwmon
cat /sys/class/hwmon/hwmon0/temp1_input

procfs (/proc)

Provides kernel and process information:

cat /proc/cpuinfo      # CPU details
cat /proc/meminfo      # Memory usage
cat /proc/interrupts   # Interrupt counts per CPU
cat /proc/device-tree/model  # Device tree model string

Real-Time Linux (PREEMPT_RT)

The PREEMPT_RT patch set transforms the Linux kernel into a near-hard-real-time system:

What PREEMPT_RT Changes

  • Converts spinlocks to RT-mutexes (preemptible)
  • Makes interrupt handlers into kernel threads (schedulable)
  • Enables priority inheritance throughout the kernel
  • Reduces worst-case latency from milliseconds to tens of microseconds

Kernel Preemption Models

Config Latency Throughput Use Case
PREEMPT_NONE Worst Best Servers
PREEMPT_VOLUNTARY Better Good Desktop
PREEMPT Good Moderate Low-latency desktop
PREEMPT_RT (full) Best (~20-50 us) Lower Industrial control

Real-Time Application Best Practices

#include <sched.h>
#include <sys/mman.h>

// Lock all memory to prevent page faults
mlockall(MCL_CURRENT | MCL_FUTURE);

// Set SCHED_FIFO with high priority
struct sched_param param = { .sched_priority = 80 };
sched_setscheduler(0, SCHED_FIFO, &param);

// Use clock_nanosleep for precise periodic execution
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
while (1) {
    next.tv_nsec += 1000000; // 1 ms period
    if (next.tv_nsec >= 1000000000) {
        next.tv_sec++;
        next.tv_nsec -= 1000000000;
    }
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
    do_rt_work();
}

Cross-Debugging

GDB Remote Debugging

# On target (or via QEMU):
gdbserver :1234 /usr/bin/myapp

# On host:
aarch64-linux-gnu-gdb myapp
(gdb) target remote 192.168.1.100:1234
(gdb) break main
(gdb) continue

JTAG and SWD

Interface Pins Speed Use
JTAG 4-5 (TDI, TDO, TMS, TCK, nTRST) Up to 50 MHz Full debug, boundary scan
SWD 2 (SWDIO, SWCLK) Up to 50 MHz ARM-specific, fewer pins

Both provide:

  • Hardware breakpoints (no code modification needed)
  • Memory and register inspection while halted
  • Flash programming
  • Real-time trace (with ETM/ITM)

Tools: OpenOCD, probe-rs, Segger J-Link, ULINK.

# OpenOCD for STM32 via ST-Link
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

# Connect GDB
arm-none-eabi-gdb -ex "target remote :3333" firmware.elf

Key Takeaways

  • Embedded Linux suits devices with 32+ MB RAM needing networking, filesystems, and process isolation.
  • Yocto provides maximum flexibility for custom distributions; Buildroot is simpler for straightforward builds.
  • Device trees decouple hardware description from kernel code, enabling one kernel for many boards.
  • PREEMPT_RT brings Linux worst-case latency down to tens of microseconds for soft/firm real-time use.
  • JTAG/SWD provide hardware-level debug access essential for bring-up and hard-to-reproduce bugs.