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.