Chapter 121: Capstone: custom board port¶
NFS: Network File System, which lets the target mount a host directory over Ethernet during development.
What: a board-port exercise that uses most of what came before. Take a custom PCB (or a rework of the Point Atom MINI into a non-trivial variant) and port the entire stack to it: U-Boot defconfig + DTS, kernel DTS + drivers, at least one new peripheral the original board didn’t have, and a reproducible build script that goes from clean checkout → bootable SD in one command. The deliverable is a working, customized Linux system on hardware you (or a colleague) designed. MCU bridge: Think of U-Boot like a much larger boot stub plus debug monitor: it initializes hardware, loads the next image, and gives you commands before Linux starts. U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.
Why: Each Cookbook chapter covered one piece. A real board port is where those pieces have to work together. You’ll touch: pin-muxing (Ch 5), DDR initialization (Ch 14), U-Boot porting (Ch 22), kernel DT (Ch 27), each peripheral chapter that applies (whatever your board has). At the end you have a binary build script + a custom DT that any teammate can run and reproduce. That deliverable, plus the debugging experience that comes with it, is what gets you to the next level of confidence on this stack. DDR: external DRAM that must be configured and trained before most software can run from it.
Focus: the bring-up sequence is U-Boot first (you need a boot loader), then kernel + DT for each peripheral one at a time, with verification at every step. Don’t try to boot everything at once. Bring up serial, then DDR, then SD, then Ethernet, then your custom peripheral. Probe the serial console after each step. Use Ch 118’s JTAG when serial is too coarse. Keep a known-good fall-back image you can flash to recover from bricks. At the end, ask what surprised you and why. That’s what makes the next port faster. MCU bridge: Think of JTAG like SWD debugging on Cortex-M: halt, read registers, set breakpoints. The Cortex-A path adds MMU state, privilege modes, and more complex reset behavior. JTAG: the hardware debug scan chain used to halt, inspect, and single-step CPUs.
121.1 Scope, what to port¶
A realistic 4-week capstone:
Phase |
What |
Deliverable |
|---|---|---|
Week 1 |
Hardware bring-up |
Custom DT compiles; U-Boot prompt over serial; DDR test passes |
Week 2 |
Storage + Network |
SD/eMMC boot; Ethernet up; NFS root works |
Week 3 |
Custom peripheral |
One chip from Part VII wired + driver bound + user-space test |
Week 4 |
Reproducible build |
One-script flow: clean checkout → bootable image |
Pick the peripheral wisely: easiest first time = an existing-driver chip (e.g., AT24 EEPROM, DS3231 RTC, BME280 sensor). Avoid for first attempt: cameras, complex graphics, anything DMA-heavy.
DMA: Direct Memory Access. Hardware moves data to or from memory without the CPU copying each byte.
121.2 Hardware variants, what counts as “custom”¶
Three options, in order of ambition:
Option A, Pretend-port the Point Atom MINI¶
Even using the standard MINI, you can do a meaningful port:
Different model string in U-Boot (board name in
bdinfo)Different default hostname in Linux
Add a peripheral the stock MINI doesn’t ship (e.g., RTC on I²C, second SPI flash)
Custom default
bootargs
You learn the workflow without designing a PCB.
Option B, Rework the Point Atom MINI¶
Solder/desolder peripherals on the existing board:
Replace the WiFi module (if any) with a different one
Add an external RTC chip + battery
Add an SPI Ethernet (DM9051 from Ch 115)
Mod the LCD interface
Real wiring, partial-board changes. Tests your DT skills.
Option C, Full custom PCB¶
Design your own i.MX6ULL board (KiCad / Altium):
Different RAM (DDR3 256 MB vs the MINI’s 512 MB? Different timing).
Different PMIC.
MCU bridge: Think of a PMIC like a programmable power-tree supervisor: it replaces discrete enables and LDO assumptions with sequenced rails the kernel can model. PMIC: Power Management IC, a chip that sequences and regulates the board’s voltage rails.
Different boot media (eMMC, QSPI flash instead of SD).
Different I/O complement (your specific application, sensors, motors, displays).
Different connectors / form factor.
This is the real-world product workflow. Takes 4–8 weeks for the PCB alone. Not for the time-constrained reader.
For most readers: Option A or B is right. The PCB design is a different book.
121.3 The bring-up checklist¶
□ Power
□ All rails come up in the right sequence (scope each)
□ All rails at the right voltage (within ±5 %)
□ POR_B deasserts after rails stable
□ Boot ROM
□ BOOT_MODE pins strap to expected boot source
□ Boot ROM reads IVT/DCD from boot media
□ DCD configures clocks + DDR (or SPL does)
□ Serial console
□ UART1 (or your chosen) at 115200 8N1
□ U-Boot prints "U-Boot SPL ..." then "U-Boot ..."
□ Enter U-Boot prompt with key press
□ DDR
□ U-Boot's "mtest" passes across full DDR range
□ DDR Stress Tool (NXP) green for 1 hour
□ Storage
□ U-Boot can read SD/eMMC (mmc info; ls mmc 0:1)
□ Kernel mounts root from same
□ Network
□ PHY detected (mdio probes return correct ID)
□ Link up when cable plugged
□ DHCP gets IP; ping gateway works
□ Each custom peripheral
□ I²C bus shows device at expected address (i2cdetect)
□ SPI device responds with expected ID
□ Kernel driver probes successfully (dmesg | grep <chip>)
□ User-space test exercises the device end-to-end
Tick each off in order. Each failure isolates to one layer.
121.4 U-Boot port, the steps¶
121.4.1 Fork the reference defconfig¶
cd u-boot
cp configs/mx6ull_14x14_evk_defconfig configs/myboard_defconfig
make myboard_defconfig menuconfig
# Change CONFIG_DEFAULT_DEVICE_TREE to "imx6ull-myboard"
# Change CONFIG_SYS_BOARD to "myboard"
# Change CONFIG_SYS_CONFIG_NAME to "myboard"
121.4.2 Create board files¶
mkdir -p board/myvendor/myboard
cp -r board/freescale/mx6ull_14x14_evk/* board/myvendor/myboard/
# Edit Kconfig, MAINTAINERS, Makefile to reference myvendor/myboard
121.4.3 Adapt the DTS¶
cp arch/arm/dts/imx6ull-14x14-evk.dts arch/arm/dts/imx6ull-myboard.dts
# Edit:
# model = "MyVendor MyBoard"
# compatible = "myvendor,myboard", "fsl,imx6ull"
# Remove unused peripherals
# Add your additions
121.4.4 DDR config (if you changed RAM chip)¶
Run NXP’s DDR Stress Tool with your DDR chip’s datasheet values. Export the calibration. Update board/myvendor/myboard/mx6ullevk.c (the C-language DDR init in U-Boot SPL) with the new register values:
MCU bridge: Think of SPL like the tiny early startup code that runs from internal SRAM before DDR is usable. SPL: Secondary Program Loader, a tiny first U-Boot stage that fits in OCRAM and initializes DDR.
const struct mx6_mmdc_calibration mx6_mmcd_calib = {
.p0_mpwldectrl0 = 0x00000000,
.p0_mpwldectrl1 = 0x00000000,
.p0_mpdgctrl0 = 0x4140043F, /* from Stress Tool */
.p0_mpdgctrl1 = 0x0124013E,
.p0_mprddlctl = 0x40404546,
.p0_mpwrdlctl = 0x40402E32,
};
This is the riskiest step. If DDR config is wrong, nothing else will work. Use the DDR Stress Tool. Don’t hand-calculate.
121.4.5 Build and flash¶
make CROSS_COMPILE=arm-none-linux-gnueabihf- -j8
# u-boot-dtb.imx is the SD-flashable image
sudo dd if=u-boot-dtb.imx of=/dev/sdX bs=1k seek=1 conv=fsync
sudo eject /dev/sdX
# Insert SD into target; power on; watch serial console
If you see U-Boot banner → 80 % of bring-up complete. If you see only DCD garbage → DDR config wrong, debug with JTAG.
DCD: Device Configuration Data: ROM-executed register writes that prepare clocks and DDR before your code runs.
121.5 Kernel DT port¶
Once U-Boot is up, port the kernel DT.
121.5.1 Fork the EVK DT¶
cd linux
cp arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dts arch/arm/boot/dts/nxp/imx/imx6ull-myboard.dts
# Edit to match your hardware
121.5.2 Add to Makefile¶
# arch/arm/boot/dts/Makefile
dtb-$(CONFIG_SOC_IMX6ULL) += imx6ull-myboard.dtb
121.5.3 Build kernel + DT¶
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- imx_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- zImage imx6ull-myboard.dtb modules
121.5.4 Boot from U-Boot¶
=> tftp 0x80800000 zImage
=> tftp 0x83000000 imx6ull-myboard.dtb
=> setenv bootargs console=ttymxc0,115200 root=/dev/mmcblk0p2 rw
=> bootz 0x80800000 - 0x83000000
If kernel hangs early → use earlycon to see early printks. If kernel hangs after init → check console= arg matches your DT’s UART.
121.6 Custom peripheral, adding an external RTC¶
Pick something simple: DS3231 RTC on I²C2.
121.6.1 DT addition¶
&i2c2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
clock-frequency = <100000>;
status = "okay";
rtc@68 {
compatible = "maxim,ds3231";
reg = <0x68>;
interrupt-parent = <&gpio4>;
interrupts = <23 IRQ_TYPE_EDGE_FALLING>;
wakeup-source;
};
};
121.6.2 Enable kernel driver¶
make ARCH=arm menuconfig
# Device Drivers → Real Time Clock → Dallas/Maxim DS1307/37/38/39/40, DS1685/87/89 → built-in
Rebuild zImage + DT.
121.6.3 Verify¶
dmesg | grep ds
# rtc-ds1307 1-0068: registered as rtc1
# rtc-ds1307 1-0068: setting system clock to 2026-05-31 12:34:56 UTC
hwclock --show
# 2026-05-31 12:34:56.123456+00:00
If this works, you’ve taken a peripheral from “nonexistent on the EVK” to “fully driven by mainline Linux” via DT alone, the canonical pattern.
121.7 The reproducible build script¶
The deliverable: one script, one command, bootable SD.
build.sh:
#!/bin/bash
set -euo pipefail
ROOT=$(pwd)
JOBS=$(nproc)
TARGET_SD=${1:-}
[ -z "$TARGET_SD" ] && { echo "Usage: $0 /dev/sdX"; exit 1; }
# Tools
. ~/imx6ull/scripts/env.sh
# 1. Build U-Boot
cd "$ROOT/u-boot"
[ -d .git ] || git clone https://git.denx.de/u-boot.git . && git checkout v2026.04
make myboard_defconfig
make -j$JOBS
# 2. Build kernel
cd "$ROOT/linux"
[ -d .git ] || git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git . && git checkout v6.6
make imx_v7_defconfig
make -j$JOBS zImage modules dtbs
# 3. Build rootfs (Buildroot)
cd "$ROOT/buildroot"
[ -d .git ] || git clone https://git.buildroot.net/buildroot . && git checkout 2026.02
make myboard_defconfig
make -j$JOBS
# 4. Flash SD
sudo bash -c "
set -euo pipefail
# Partition (1 MB unused | 64 MB FAT for boot | rest EXT4 for root)
sfdisk $TARGET_SD <<EOF
,,1M
,64M,c,*
,,L
EOF
mkfs.vfat ${TARGET_SD}1
mkfs.ext4 -F ${TARGET_SD}2
# Mount + copy
mkdir -p /mnt/boot /mnt/root
mount ${TARGET_SD}1 /mnt/boot
mount ${TARGET_SD}2 /mnt/root
cp $ROOT/linux/arch/arm/boot/zImage /mnt/boot/
cp $ROOT/linux/arch/arm/boot/dts/nxp/imx/imx6ull-myboard.dtb /mnt/boot/
tar -C /mnt/root -xf $ROOT/buildroot/output/images/rootfs.tar
cd $ROOT/linux && make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- INSTALL_MOD_PATH=/mnt/root modules_install
umount /mnt/boot /mnt/root
# Flash U-Boot (with offset 1k)
dd if=$ROOT/u-boot/u-boot-dtb.imx of=$TARGET_SD bs=1k seek=1 conv=fsync
sync
"
echo "Done. Eject SD and boot the target."
Run: ./build.sh /dev/sdb. 30 minutes later (mostly compile time), bootable card.
This script is the deliverable. Hand it to a teammate. They get the same image. Reproducibility is the difference between “I shipped a product” and “I have a Linux running on my desk.”
121.8 Common failures + recovery¶
“U-Boot prints DCD garbage then dies”¶
DDR init failed. Use JTAG to dump MMDC registers. Compare to Stress Tool output.
MMDC: the i.MX6ULL DDR controller block that owns timing, calibration, and DRAM command sequencing.
Verify the SPL DDR init values match your DDR3 chip’s datasheet.
“U-Boot prompt but kernel hangs after ‘Uncompressing Linux…’”¶
DT name mismatch, kernel can’t find the DT. Verify
console=and root= args.earlyconandloglevel=8for more info.
“Kernel boots but no Ethernet”¶
PHY MDIO address wrong.
cat /sys/class/net/eth0/phydev/phy_idto see.mdioU-Boot command to scan addresses.
PHY: physical-layer block or chip that converts digital MAC signals to electrical or radio signals.
PHY power rail not up. Verify with multimeter.
“Driver doesn’t probe”¶
Compatible string mismatch, check kernel’s table vs your DT.
Required clock not declared, check
clocksin DT.Required regulator not declared,
dmesg | grep regulator.
“Worked once, now bricked”¶
Boot back into recovery via USB-OTG SDP (Ch 8).
Re-flash known-good image.
121.9 Lab¶
Set up workspace. Clone u-boot, linux, buildroot at known versions. Verify each compiles.
Buildroot: a configuration-driven build system that produces a complete root filesystem and related images.
Stock build first. Build U-Boot + kernel for the EVK (no changes). Boot the stock Point Atom MINI. Verify everything works.
Customize step by step. Fork the EVK as your board. Change only the model string. Rebuild. Reboot. Verify
bdinfoshows your name.Add a peripheral. Wire DS3231 RTC (Ch 117). Add DT. Enable driver. Verify.
Or replace a peripheral. Remove the stock WM8960 audio. Add an SGTL5000 (Ch 89). Update DT. Verify
aplayworks.Or add a new bus. Wire a second I²C bus to spare pins. Add DT. Verify
i2cdetect -y 1works.Build script. Write
build.shfrom scratch. Test on a clean checkout. Time the full build.CI integration (preview of Ch 121A). Wire
build.shinto GitHub Actions. Verify it runs on every commit.Document it. Write a 1-page README: “what hardware is needed, what software prerequisites, how to build, how to flash, how to verify.” A new engineer should be able to follow it.
(Stretch) Upstream the DT. Format-patch + send your board DT to linux-arm-kernel + linux-imx (per Ch 120A). Even if rejected, the experience is valuable.
121.10 Reflection¶
After 4 weeks of bring-up, write a 500-word retrospective:
What did you expect to be hard that wasn’t?
What did you expect to be easy that wasn’t?
What did you learn that surprised you?
What did you wish you’d known on day 1?
What single tool / technique saved the most time?
What was the most frustrating debug session, and how did you eventually solve it?
These answers are gold for the next board you port. Reread them when you start.
121.11 Pitfalls¶
Trying to bring up everything at once. Bring up one layer at a time. Verify each.
No serial console early. UART1 with PowerView / minicom from minute zero. Other debug paths require more setup.
No JTAG when needed. When the serial output is “boots and hangs at unknown location,” JTAG is the only ground truth. Don’t argue.
Cargo-culting EVK config. Read every line of the defconfig and DTS. Understand why each is there before changing.
DDR config “close enough.” No. Stress Tool every time. A 5 % timing margin difference can mean “works at 25 °C, crashes at 40 °C.”
DT compile errors not fatal-looking.
dtcgives warnings that often hide errors. Read every line.One-script build that depends on your laptop. Test on a fresh checkout in a fresh VM. If it doesn’t work, it’s not reproducible.
Forgetting modules.
make modules_installto the rootfs partition. Without it, your driver isn’t actually present on the target.Wrong rootfs init. Buildroot’s default init is BusyBox. Verify
/sbin/initexists.Wrong console in bootargs.
console=ttyS0won’t work on i.MX6ULL (it’sttymxc0). Verify against your DT.No fallback image. Brick your custom board with no way to recover. Always keep a stock-EVK SD on hand for comparison.
121.12 Going deeper¶
NXP IMX6ULL Reference Manual: your single most important reference.
NXP DDR Stress Tool: for DDR bring-up.
U-Boot README.imx+doc/imx/mkimage/: for IMX-specific boot flow.Documentation/devicetree/bindings/arm/fsl.yaml: the i.MX SoC binding.MAINTAINERS: to find subsystem maintainers when upstreaming.Bootlin training materials: http://bootlin.com/training/embedded-linux/.
i.MX Solutions Catalog: NXP’s reference designs for various i.MX SoCs.
Ch 120A: for upstreaming your DT.
Ch 121A: to wire your
build.shinto CI.Ch 22 + Ch 25: the original U-Boot and kernel port chapters.
Next chapter: Chapter 121A: CI/CD for embedded Linux.