Embedded Linux From Scratch
A complete Linux system built by hand for a BeagleY-AI board: cross-compiled
mainline kernel, custom device tree, hand-assembled BusyBox rootfs, a custom
/init as PID 1 and a real kernel driver bound through the
device tree.
No vendor BSP. Every layer above the TI bootloaders was built by hand.
1- Why this project
Most embedded work uses a Linux image someone else built. This project goes the other way: it builds the system itself, layer by layer, to understand how an embedded Linux platform actually comes to life — from the boot ROM handoff to a shell running as PID 1, and down into kernel space with a custom driver.
2-Boot stack
The TI AM67A has a multi-stage, vendor-specific boot chain. The three TI binaries are reused as-is; everything in green was built in this project.
3- Cross-compiling the mainline kernel
Compiled on x86 (WSL2) for the ARM64 target, with board support verified in the config before building.
# verify board support (all must be =y) grep CONFIG_ARCH_K3 .config # TI K3 SoC grep CONFIG_SERIAL_8250_OMAP .config # serial console grep CONFIG_MMC_SDHCI_AM654 .config # SD card # build kernel + device trees + modules make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image dtbs modules
4- Root-cause debugging: the WiFi that couldn't work
At boot, the WiFi service crash-looped. A funnel investigation
(systemctl → ip link → dmesg → .config → device tree) peeled back
three layers:
The driver existed as a module (CONFIG_WL18XX=m) but
/lib/modules/7.1.5/ was missing. Deploying the modules loaded
the driver but still no wlan0.
The mainline device tree never declares the chip (wlcore@2
missing under mmc@fa20000). A driver with no matching DT node
stays inert.
The board's actual WiFi chip is a TI CC33xx (recent). Its driver exists only in the vendor kernel (6.x-ti) and is absent from mainline 7.1.5 entirely. Mainline only ships the older wl12xx/wl18xx family.
Image (built-in drivers) + .dtb (hardware description)
+ /lib/modules/ (module drivers). Forget the modules and the system
boots but everything built as a module silently disappears.
5-A rootfs from scratch → PID 1
A minimal root filesystem assembled by hand: a static BusyBox binary (~200 Unix
commands in one file) plus a hand-written /init, the very first
program the kernel launches.
#!/bin/sh — PID 1, first program launched by the kernel mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev export PATH=/bin:/sbin:/usr/bin:/usr/sbin exec setsid cttyhack /bin/sh # shell attached to serial console
~ # ps PID USER TIME COMMAND 1 0 0:00 /bin/sh ← our shell is PID 1 ~ # free Mem: 3872428 total 51444 used 3813520 free ← 51 MB for the whole system
6- Into kernel space: a real driver
Step 5 controlled hardware from userspace (a C app writing to
/sys/class/leds/). This step crosses into kernel space: a platform driver bound through the device tree, the core pattern behind every real device. It registers a virtual LED in /sys/class/leds/ (no GPIO yet): the goal is to prove the binding mechanism, not to drive the physical LED, which uses the kernel's standard leds-gpio driver.
/* the driver declares what it handles */ static const struct of_device_id myled_of_match[] = { { .compatible = "steve,myled", }, // must match the device tree { }, }; static int myled_probe(struct platform_device *pdev) { const char *label; of_property_read_string(pdev->dev.of_node, "label", &label); // read DT property led_classdev_register(&pdev->dev, &myled_cdev); // create /sys interface return 0; }
The kernel calls probe() only when a device-tree node's
compatible matches the driver's of_match_table, the same mechanism that activates the standard LED driver and leaves the unsupported WiFi inert.
myled-dt: probe() called - matched by device tree!
myled-dt: label from DT = 'hello-from-devicetree'
myled-dt: registered /sys/class/leds/myled-dt/
7- Bug log : the real content
Every bug taught something. A selection of the thirteen documented in the repo:
| Symptom | Root cause | Fix |
|---|---|---|
| Services FAILED at boot | =m modules never deployed | modules_install + deploy |
| wl18xx loaded, no wlan0 | chip is a CC33xx, driver not in mainline | investigated → documented |
| Wrong Ramdisk Image Format | booti expects size for raw cpio.gz | add :${filesize} |
| RD image overlaps OS image | initramfs inside the 51 MB kernel region | relocate load address |
| Silent shell, ls does nothing | PATH not set in /init | export PATH |
| probe() never called | stale .dtb — make skipped rebuild | force DT rebuild |
8-Key lessons
- Deploying a kernel = Image + device tree + modules. Forgetting modules is the #1 trap.
- The device tree decides which drivers activate, a loaded driver with no matching DT node stays inert.
- Funnel debugging: descend from symptom to root cause, verify each hypothesis before concluding.
- Boot-time memory layout matters, kernel, DT and initramfs must not overlap in RAM.
- A platform driver is bound by the device tree, not by loading,
insmodalone does nothing. - Vendor vs mainline is a real trade-off; knowing when not to forward-port is an engineering decision.