Pinning the U-Boot build environment for the IOT-DIN-IMX8PLUS
imx-boot is not one repository — it is U-Boot, ARM Trusted Firmware, NXP's imx-mkimage and the LPDDR4 training blobs, assembled together. Get one of them at the wrong version and it builds cleanly and fails to train memory. How I pinned all four using the board's own boot log.
With backups done and keys generated, the next milestone is building a bootloader from source — unsigned to begin with, so that any problems are clearly build problems and not signing problems.
This post is about everything that had to be right before that build could start. I spent far more time on version pinning than I expected, and part 7 is the story of what happens when one piece of it is wrong.
imx-boot is not one repository
This is the thing I did not properly appreciate going in. The single imx-boot file the boot
ROM loads is a container, assembled from four independently versioned pieces:
imx-boot (a.k.a. flash.bin)
┌──────────────────────────────────────────────────────────────┐
│ SPL from u-boot-compulab │
│ ATF / bl31 from NXP imx-atf, platform imx8mp, @0x970000 │
│ U-Boot from u-boot-compulab │
│ DDR firmware from NXP firmware package, version 202006 │
└──────────────────────────────────────────────────────────────┘
assembled by imx-mkimage → flash.bin_d2d4 / flash.bin_d1d8
The versions that matter on this board:
| Component | Source | Version |
|---|---|---|
| U-Boot | compulab-yokneam/u-boot-compulab |
branch u-boot-compulab_v2023.04, defconfig iotdin-imx8p_defconfig |
| Kernel | compulab-yokneam/linux-compulab |
linux-compulab_v6.6.23 |
| Device tree | same | compulab/iotdin-imx8p.dtb |
| ATF | NXP imx-atf |
platform imx8mp, loads at 0x970000 |
| DDR firmware | NXP | LPDDR4 training blobs, version 202006 |
| Assembly | imx-mkimage |
target flash_evk → flash.bin, written at 32 KiB offset (IMX_BOOT_SEEK=32) |
Everything is on the iot-din-imx8plus-r2.0 branch, CL_RELEASE 2.0.
Getting one of these wrong does not fail loudly. In the worst case it builds cleanly and produces a bootloader that cannot train this board's memory — which is exactly the failure I eventually hit, and it presents as a hardware fault rather than a version mismatch.
The cross-compiler, briefly
Building ARM64 bootloader code on an x86_64 build machine needs a cross toolchain. On Debian or Ubuntu that is one package:
sudo apt install gcc-aarch64-linux-gnu device-tree-compiler bison flex \
libssl-dev bc build-essential
export CROSS_COMPILE=aarch64-linux-gnu-
export ARCH=arm64
aarch64-linux-gnu-gcc --version
That is the boring part and it is one command. The interesting part is knowing which commits to point it at.
Pin the versions using the board's own boot log
The trick that made this tractable: the factory boot log tells you what the factory build was made from. Capture it on the DBG console before you change anything.
tio /dev/ttyUSB0 # or screen /dev/cu.usbserial-XXXX 115200
# power-cycle the board and let it print
You get version strings like:
U-Boot SPL 2023.04-IOTDIN-IMX8P-1.3-g70a7183523f
That g70a7183523f is an abbreviated git hash. It is not a guess about which branch the
vendor used — it is the commit, stated by the binary itself.
ATF: match the build timestamp to the commit time
ARM Trusted Firmware prints a build timestamp down to the second. That turns out to be enough to identify the exact upstream commit, because you can search the repository's history by commit time rather than by date range:
git clone https://github.com/nxp-imx/imx-atf && cd imx-atf
git log --all --format='%H %cI %s' | grep '2026-01-15T14:23'
Out of the entire history, exactly one commit matched the timestamp from my boot log. That is about as strong a confirmation as you can get without the vendor's own build logs — and it takes a minute, against hours of debugging a subtly wrong ATF later.
Substitute your own board's timestamp; the point is the method, not my value.
imx-mkimage: match the version suffix
There is no timestamp to match for the assembly tool. But CompuLab tag their repositories
with the same version suffix across projects, consistently, release after release. Once
the suffix from the U-Boot tag also existed on the imx-mkimage fork, that was a strong
signal of the right pairing:
git ls-remote --tags https://github.com/nxp-imx/imx-mkimage | grep -i <your-suffix>
Less ironclad than a second-accurate timestamp, but consistent enough across many releases to rely on.
DDR firmware: two independent sources agreeing
The vendor's own build configuration names the exact firmware package version — 202006.
Then, as a cross-check, an entirely unrelated upstream project's documentation named the
same package version and listed the same filenames inside it.
Two unrelated sources landing on the same answer from different angles is the point at which you stop second-guessing a version number and move on. Note that this board reads its DRAM-training data from an on-board EEPROM, which is specific to CompuLab's design and not something a generic i.MX8M Plus reference board does — so reference-board instructions do not transfer cleanly here.
The pins, as a script
The end result is small enough to be a script, which is the right place for it — a pinned commit in a reviewable file beats a pinned commit in somebody's memory:
UBOOT_REPO="https://github.com/compulab-yokneam/u-boot-compulab"
UBOOT_BRANCH="u-boot-compulab_v2023.04-dram"
UBOOT_COMMIT="70a7183523fcdf45325684f39629f717257f9f01"
ATF_REPO="https://github.com/nxp-imx/imx-atf"
ATF_TAG="automotive-15.0.0_1.1.0"
ATF_COMMIT="1b27ee3edbb40ef9432c69ccaa744d1ac5d54c5d"
MKIMAGE_REPO="https://github.com/nxp-imx/imx-mkimage"
MKIMAGE_TAG="automotive-15.0.0_1.1.0"
MKIMAGE_COMMIT="71b8c18af93a5eb972d80fbec290006066cff24f"
FIRMWARE_IMX_URL="https://www.nxp.com/lgfiles/NMG/MAD/YOCTO/firmware-imx-8.10.bin"
Look at the U-Boot commit: 70a7183523f…. That is the same string the factory SPL prints in
its banner — 2023.04-IOTDIN-IMX8P-1.3-g70a7183523f. The binary on the board told me which
commit built it, and the pin is that commit, not a guess about which branch was current.
Fetch each one shallowly and verify you got what you asked for, because git fetch of a
commit that does not exist on that remote fails in ways that are easy to miss in a script:
git init -q "$target"
git -C "$target" remote add origin "$repo"
git -C "$target" fetch --depth 1 origin "$commit"
git -C "$target" checkout -q FETCH_HEAD
actual="$(git -C "$target" rev-parse HEAD)"
[ "$actual" = "$commit" ] || { echo "got $actual, expected $commit — aborting"; exit 1; }
firmware-imx is not a git repository at all — it is a self-extracting NXP installer with a
licence prompt, which needs --auto-accept to run unattended:
chmod +x firmware-imx-8.10.bin
./firmware-imx-8.10.bin --auto-accept
The fetched trees themselves are gitignored. They are large, long-history upstream
repositories — u-boot-compulab alone is over 114,000 commits — that this project only ever
needs at one exact commit each. Track the pins, not the trees.
The dead end worth checking
While hunting for these pins I found a second copy of CompuLab's build system online that looked, at first glance, like the authoritative source for exactly this.
It was pinning a different, newer set of versions than the ones my board's boot log demonstrably came from — a separate track, not this unit's factory build.
Using its pins would have meant building against the wrong baseline entirely, while looking completely reasonable the whole way. The general lesson: when a plausible-looking source disagrees with the artefact in front of you, the artefact wins. The boot log is evidence; a repository someone mirrored is a claim.
Take the vendor's defconfig verbatim
One more thing, learned the hard way later. When you come to configure U-Boot, start from
CompuLab's authoritative configs/iotdin-imx8p_defconfig at the pinned commit and add to
it:
cp configs/iotdin-imx8p_defconfig configs/my-iotdin_defconfig
# then add your changes on top
I first tried hand-curating a config from what looked necessary, and it silently missed
options. make olddefconfig will also quietly drop sub-options whose parent is not enabled —
CONFIG_ENVIMAGE disappeared on me exactly that way. Diff your generated .config against
the vendor's defconfig before you trust it:
make my-iotdin_defconfig && make savedefconfig
diff configs/iotdin-imx8p_defconfig defconfig
Every line in that diff should be something you put there deliberately.
Where this leaves things
Four components, each pinned to an exact commit, all on the build machine. The next step is an actual build — unsigned on purpose, so that problems are unambiguous.
One number to carry forward, because it is the one that caught me out: this SoM is 4 GB, so
the build must be d2d4. Confirm yours with bdinfo at the U-Boot prompt before you build,
not after the board stops booting.
Next: building that first unsigned imx-boot, and the two errors it took to get there.