Chapter 19: U-Boot from source, first boot¶
PMIC: Power Management IC, a chip that sequences and regulates the board’s voltage rails. rootfs: root filesystem, the directory tree mounted at / that contains /bin, /etc, /dev, and libraries. MCU bridge: Think of the rootfs as the firmware image’s file-backed runtime environment. On an MCU you link everything into flash. On Linux, programs and config live in this mounted tree.
What: clone mainline U-Boot, build it for the i.MX6ULL,
ddthe result to an SD card, boot it, get a=>prompt. Then run a few commands and recognize, in U-Boot’s output, every step we did by hand in Chapters 9–17. U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.Why: Part II proved we can boot the chip ourselves. From here on the question is what the professional version looks like. U-Boot is that version. Its source is one of the best embedded-Linux codebases to read.
Focus: recognition. By the end of Part III you should be able to point at any line of U-Boot’s
arch/arm/cpu/armv7/start.Sorarch/arm/mach-imx/spl.cand say “that is Chapter 14 §14.6, rewritten by someone who has done it a thousand times.” That recognition is what Part II bought us.
19.1 Why mainline U-Boot¶
There are two U-Boot trees you will see referenced for the i.MX6ULL:
Mainline U-Boot, hosted at
https://source.denx.de/u-boot/u-boot.git(a.k.a.git.denx.de). The canonical project. Current version as of 2026 is in the v2024.x → v2025.x range.NXP’s vendor fork,
https://github.com/nxp-imx/uboot-imx.git, frozen at tags likeimx_v2016.03_4.1.15_2.0.0_ga. Many Chinese-language guides and existing customer projects use this.
We use mainline. Three reasons:
Mainline has full support for the i.MX6ULL EVK since 2017 and tracks every silicon revision and DT change. Nothing about the i.MX6ULL requires a fork.
The 2016-era fork has missed years of security fixes. Every project that pins to it eventually pays the migration cost (Chapter 122A is the playbook).
Starting on mainline removes a future migration entirely.
We use mainline’s mx6ull_14x14_evk_defconfig (NXP’s reference EVK) and port it to the Point Atom MINI in Chapter 22. The boards are close enough that the EVK config boots on the MINI with only minor DT changes for IOMUX and DDR timings.
MCU bridge: Think of IOMUX like STM32 alternate-function selection, but with separate pad electrical settings and board-level ownership by Device Tree. DDR: external DRAM that must be configured and trained before most software can run from it. IOMUX: the pin multiplexer that decides which peripheral function appears on each package pin.
19.2 Clone and look around¶
$ cd ~/imx6ull/src
$ git clone https://source.denx.de/u-boot/u-boot.git
$ cd u-boot
$ git log --oneline -3
abc123def (HEAD -> master, tag: v2025.01, origin/master) Release v2025.01
fed456abc Merge tag 'efi-2025-01-rc7' of ...
789abc456 board: foo: enable CONFIG_BAR
If you want a stable release rather than the development tip:
$ git checkout v2025.01 # or whichever release is current
On slow connections, add --depth=1 to skip ~50 MB of git history. Disk is cheap. Keep the history.
Directory layout¶
u-boot/
├── arch/ # CPU and SoC support
│ └── arm/
│ ├── cpu/armv7/
│ ├── mach-imx/ # the SoC-family code we care about
│ │ └── mx6/
│ ├── dts/ # device-tree sources for ARM boards
│ └── lib/
├── board/ # board-specific code, one folder per board
│ └── freescale/
│ └── mx6ull_14x14_evk/ # our starting point
├── cmd/ # one .c per U-Boot command
├── common/ # shared core: main loop, env, image handling
├── configs/ # *_defconfig files
├── doc/ # docs (read these!)
├── drivers/ # drivers (DM-style), organized by subsystem
├── env/ # environment storage backends
├── fs/ # filesystem support (FAT, ext4, UBIFS, ...)
├── include/ # public headers and per-board config headers
│ └── configs/
├── lib/ # generic library code
├── net/ # network stack
├── post/ # Power-On Self Test framework
├── scripts/ # build helpers
├── test/ # unit tests (you can run them on the host)
├── tools/ # mkimage, dumpimage, etc.
├── Kconfig # top-level Kconfig
├── Makefile # the build system entry point
└── ...
Read doc/README.imx6 and board/freescale/mx6ull_14x14_evk/README before going further. They are short and answer the most common bring-up questions.
19.3 Build for the EVK¶
We already have CROSS_COMPILE and ARCH exported from Chapter 3’s ~/imx6ull/scripts/env.sh. If you forgot to source it:
$ . ~/imx6ull/scripts/env.sh
Now:
$ make mx6ull_14x14_evk_defconfig
$ make -j$(nproc)
The first build takes 1–2 minutes on a modern host. A few interesting lines scroll past:
HOSTCC scripts/...: building host-side helpers (tools, dtc, mkimage) with the host’s compiler.CC arch/arm/cpu/armv7/start.o: that file is the bare-metal startup. You just compiled the i.MX6ULL equivalent of your Chapter 10startup.S. Open it. Read it.LD spl/u-boot-spl: building the SPL (Chapter 20).LD u-boot: building the main U-Boot ELF.
ELF: Executable and Linkable Format, the standard Linux object and executable file format.
OBJCOPY u-boot.bin: the raw binary (Chapter 6).MKIMAGE u-boot.imx: wrapping with an IVT + BootData (Chapter 7), the same operation ourmkimx.pyperforms.
When make finishes without errors, the build produces these files:
File |
What it is |
|---|---|
|
The main U-Boot ELF (with symbols, useful for |
|
The same, stripped to raw binary |
|
The above wrapped with an IVT, the file to flash for SD-boot when no SPL is needed |
|
The SPL ELF |
|
A U-Boot-formatted image of |
|
U-Boot binary + DT blob, wrapped, current preferred form |
|
Symbolic link / copy of the SPL image used by some SoCs (TI / OMAP heritage) |
|
Compiled device tree for the EVK |
Two of these are what we will actually use:
SPL: the first stage, getsdd’d to the SD card at offset0x400(LBA 2). This is what the i.MX6ULL Boot ROM finds and runs.u-boot-dtb.imx: the second stage, getsdd’d to the SD card at offset69 KiB(the location SPL’s defconfig is built to look for).
For the SD-boot case, SPL carries the IVT. SPL then loads u-boot-dtb.imx (or u-boot.img) from a later offset on the card. There is also a simpler no-SPL flow where u-boot-dtb.imx itself is what the ROM loads, used when U-Boot fits in OCRAM (rarely true these days). We will use the SPL flow throughout this book.
19.4 Flash to SD¶
The mainline mx6ull_14x14_evk SD-boot layout is:
Offset (KiB) |
Content |
|---|---|
0 |
(untouched / partition table) |
1 (= LBA 2) |
SPL, contains the IVT |
69 |
|
8192 |
Reserved for partitions (the rootfs partition begins around here) |
To write both stages:
$ sudo dd if=SPL of=/dev/sdX bs=1k seek=1 conv=fsync # uses your sd-write helper
$ sudo dd if=u-boot-dtb.imx of=/dev/sdX bs=1k seek=69 conv=fsync
$ sync
(Or use the helper from Chapter 3 with two invocations. Or write a small wrapper.)
A nicer alternative: uuu does the whole thing over USB-OTG, no SD card needed. We will use that in Chapter 24. For now, SD is concrete and the steps are the most explicit.
19.5 First boot¶
Power on with the SD card inserted and the boot switch on SD. Within 2 seconds picocom should show:
U-Boot SPL 2025.01 (Jan 12 2026 - 17:42:31 +0700)
Trying to boot from MMC1
U-Boot 2025.01 (Jan 12 2026 - 17:42:31 +0700)
CPU: i.MX6ULL rev1.1 at 396 MHz
Reset cause: POR
Model: Freescale i.MX6 UltraLite 14x14 EVK Board
DRAM: 512 MiB
PMIC: PFUZE3000 DEV_ID=0x30 REV_ID=0x11
MMC: FSL_SDHC: 0, FSL_SDHC: 1
Loading Environment from MMC... *** Warning - bad CRC, using default environment
In: serial
Out: serial
Err: serial
Switch to partitions #0, OK
mmc0 is current device
Net: FEC0
Hit any key to stop autoboot: 3 ...
=>
If you press a key during autoboot, you land at the => prompt.
Read the boot log again, slowly. Notice:
“U-Boot SPL” prints first. That’s a small program, loaded by ROM, that initialized DDR. The exact responsibilities you wrote in Chapter 14.
“Trying to boot from MMC1”, SPL is reading
u-boot-dtb.imxfrom the SD card and loading it into DRAM.“U-Boot 2025.01”, the second stage has taken over, running from DRAM, with full peripheral support.
“CPU: i.MX6ULL rev1.1 at 396 MHz”, the boot-default ARM clock. Notice it didn’t go to 696 MHz. The EVK config is conservative. Chapter 13’s PLL config can apply here too.
PLL: Phase-Locked Loop, a clock block that multiplies a reference clock to create faster clocks.
“DRAM: 512 MiB”, SPL’s DDR setup worked. The production version of your Chapter 14 MMDC bring-up. Mainline’s
arch/arm/mach-imx/mx6/ddr.cis a ~1500-line table-driven driver that wraps the same MMDC sequence, hardened across thousands of boards. We dissect it in Ch 20 §20.7.
MMDC: the i.MX6ULL DDR controller block that owns timing, calibration, and DRAM command sequencing.
“Loading Environment from MMC…”, U-Boot tries to read its persistent env from the SD card. There isn’t one yet. It falls back to defaults.
*** Warning - bad CRCis normal on first boot.“Hit any key to stop autoboot”, without intervention, U-Boot would run
bootcmd(Chapter 23) which on the EVK config tries to find a kernel and chain-boot Linux.
19.6 First commands¶
At the => prompt:
printenv¶
=> printenv
arch=arm
baudrate=115200
board=mx6ull_14x14_evk
board_name=EVK
bootcmd=run findfdt; mmc dev ${mmcdev}; mmc rescan; ...
bootdelay=3
console=ttymxc0
ethact=FEC0
ethaddr=00:04:9f:01:30:ad
...
Every variable here was set by the EVK board’s source code or by the default environment compiled into U-Boot. Most are convenience shortcuts. We will spend Chapter 23 understanding bootcmd and bootargs.
bdinfo¶
=> bdinfo
arch_number = 0x00000000
boot_params = 0x80000100
DRAM bank = 0x00000000
-> start = 0x80000000
-> size = 0x20000000
flashstart = 0x00000000
flashsize = 0x00000000
flashoffset = 0x00000000
baudrate = 115200 bps
relocaddr = 0x9ff37000
reloc off = 0x1f737000
Build = 32-bit
current eth = FEC0
ethaddr = 00:04:9f:01:30:ad
IP addr = <NULL>
fdt_blob = 0x9ed3d2c0
new_fdt = 0x9ed3d2c0
fdt_size = 0x00007b80
Several things to notice:
DRAM start = 0x80000000,size = 0x20000000(512 MiB). Same map we used in Chapter 14.relocaddr = 0x9ff37000. This is the actual address U-Boot is running from right now, near the top of DRAM. U-Boot started executing somewhere lower in DRAM, then relocated itself to high DRAM to free the low addresses for the kernel. We’ll trace that in Chapter 21.reloc off = 0x1f737000: the offset between the linker’s idea of where U-Boot lives and where it actually lives. Pointer fixups everywhere use this.fdt_blob = 0x9ed3d2c0: U-Boot loaded its own copy of the device tree into DRAM. It will pass this address to the kernel viar2when it eventuallybootz’s Linux.
md, memory display¶
The Chapter 14 memtest equivalent:
=> md 0x80000000 4
80000000: 12345678 deadbeef ffffffff ffffffff xV4....
=> mw 0x80000000 0xcafebabe
=> md 0x80000000 1
80000000: cafebabe ....
DRAM works. Same DRAM as Chapter 14, but SPL configured it this time.
mmc info¶
=> mmc info
Device: FSL_SDHC
Manufacturer ID: 27
OEM: 5048
Name: SD32G
Bus Speed: 50000000
Mode: SD High Speed (50MHz)
Rd Block Len: 512
SD version 3.0
High Capacity: Yes
Capacity: 29 GiB
Bus Width: 4-bit
Erase Group Size: 512 Bytes
The SD card U-Boot booted from is also enumerated for runtime use, we’ll read kernel images from it shortly.
mtest¶
=> mtest 0x80000000 0x80100000 0x12345678 1
Testing 80000000 ... 80100000:
Pattern 12345678 Writing... Reading...Tested 1 iteration(s) with 0 errors.
Built-in DRAM memtest. Same principle as our Chapter 14 ddr_selftest, with more patterns. If it reports errors, your SPL’s DDR config is wrong.
help¶
Lab vs production: Do not burn fuses, enroll production keys, or sign release images while following the lab. Use throwaway keys and back up the unsigned image plus the key directory before testing irreversible security flows.
=> help
? - alias for 'help'
askenv - get environment variables from stdin
base - print or set address offset
...
About 80 commands ship with the EVK defconfig. We will use perhaps 15 of them in this book. The rest are board-specific or for use cases we don’t reach (USB host, JTAG, fuse programming, etc.).
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.
19.7 Recognizing Chapter 14 in SPL¶
This is the moment Part II earns its keep.
Open arch/arm/mach-imx/spl.c in your editor. Find spl_dram_init (or arch_cpu_init, depending on the SoC). It calls a board-specific function that, on the EVK, lives in board/freescale/mx6ull_14x14_evk/spl.c. Find spl_dram_init in that file. You will see something like:
static void ccgr_init(void) { /* enable clocks to MMDC, IOMUXC, etc. */ }
static void iomux_setup_uart(void) { /* IOMUX pad config */ }
static struct mx6_ddr_sysinfo sysinfo = { /* timing tables */ };
static struct mx6_mmdc_calibration mx6_mmcd_calib = { /* calibration */ };
static struct mx6_ddr3_cfg mt41k128m16jt_125 = { /* JEDEC params */ };
static void spl_dram_init(void)
{
mx6_ddr3_cfg(&sysinfo, &mx6_mmcd_calib, &mt41k128m16jt_125);
}
Three structs and one function call. Inside mx6_ddr3_cfg, you’ll find ~600 lines doing the same job as your Chapter 14 ddr_init: pad config, MMDC core registers, MR loads, ZQ cal, write-leveling. The difference is that it is table-driven and validated across every Micron, Nanya, and ISSI DDR3 part NXP supports.
Read it. After Chapter 14, none of it should look magical. That is the point.
19.8 Lab¶
Clone, build, flash, boot. Confirm
=>appears.Hit a key to stop autoboot. Explore:
printenv,bdinfo,mmc info,md,mw,help.Run
mtest 0x80000000 0x90000000 0xa5a5a5a5 1: A 256 MB memtest. Should report 0 errors.Open
board/freescale/mx6ull_14x14_evk/spl.cand find the DDR struct definitions. Cross-reference each field against your Chapter 14 register values. Annotate.Open
arch/arm/mach-imx/mx6/ddr.cand find the DDR3 init flow. Match it section-by-section to your Chapter 14 code. Note where it does more (e.g., periodic recalibration) and where it does less (e.g., it does not run the stress tool inline. Values are precomputed).
19.9 Pitfalls¶
Building without
ARCH=arm CROSS_COMPILE=.... You will get a confusing host-build error halfway through. Always export these beforemake.Reusing a build dir between defconfigs.
make foo_defconfig && make bar_defconfigdoes not fully reset state. Alwaysmake distcleanbetween configs.Wrong SD-card offsets. SPL goes to LBA 2 (
bs=1k seek=1). The second stage goes toseek=69. If you swap them or use a different offset, the ROM either finds nothing or loads garbage. The exact offsets are SoC-family-specific and controlled byCONFIG_SPL_PAD_TOand similar. The EVK defaults are what we used above.Build artefacts left over from a previous board.
make cleankeeps the.config.make distcleanresets everything. When in doubt, distclean.Bad CRC env warning: not an error. Means the SD card has no saved env yet.
saveenvonce and the warning disappears on future boots.make -jwith not enough RAM. U-Boot is small enough to build single-threaded if your host is constrained, but-j$(nproc)is fine on anything with ≥4 GB RAM.
19.10 Going deeper¶
U-Boot documentation at
https://docs.u-boot.org/. Specificallyarch/arm/cpu/armv7/Kconfiganddoc/README.imx.doc/board/freescale/in the source tree, the board-specific docs. Often the answer to “why this defconfig?”The U-Boot mailing list at
u-boot@lists.denx.de. Read-only for weeks. Learn how patches flow.DENX’s
Bootloader_with_U-Bootarticle series (free). The clearest single intro.The U-Boot README at the top of the source tree. Skim section by section. Bookmark the parts that surprise you.
Next chapter: Chapter 20: U-Boot SPL: the missing link. We zoom into the SPL specifically, what makes it different from full U-Boot, what fits in 64 KB of OCRAM, and how it loads its successor.