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, ¶m);
// 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.