Chapter 25: Building mainline Linux for i.MX6ULL

GIC: ARM’s Generic Interrupt Controller, the Cortex-A interrupt router roughly analogous to NVIC on Cortex-M. MCU bridge: Think of the GIC like the Cortex-M NVIC scaled up for Cortex-A: it routes peripheral interrupts to CPU cores and has separate distributor and CPU-interface blocks. ELF: Executable and Linkable Format, the standard Linux object and executable file format. ABI: Application Binary Interface: the calling convention, register use, binary format, and library contract that let separately built code run together. GPIO: General-Purpose Input/Output, a pin controlled as a digital input, output, or interrupt source. MCU bridge: Think of Linux GPIO like the same pin set/reset block you used on STM32, but accessed through a kernel subsystem that owns numbering, direction, interrupts, and user-space exposure. TFTP: Trivial File Transfer Protocol, a simple network protocol U-Boot commonly uses to fetch kernels from the host. 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.

What: how a mainline Linux source tree, configuration and toolchain produce the kernel image, device tree blobs and modules for i.MX6ULL. Chapter 26 explains the bootloader handoff.

Why: later chapters refer to these source and artifact layouts. Understanding their roles does not require completing a build.

Focus: the four build artefacts you actually use (vmlinux, zImage, *.dtb, *.ko) and the four directories you will visit most (arch/arm/, drivers/, include/, Documentation/).

25.1 Why mainline

The Linux kernel ships under several release tracks:

  • Mainline at git.kernel.org/torvalds/linux.git, Linus’s tree. The current development tip. New releases roughly every 9 weeks (the “x.y” releases like 6.6, 6.7).

  • Stable: Greg KH backports bug fixes to each mainline release for about 6 weeks. Tags look like 6.6.1, 6.6.2, and so on.

  • Long-Term Support (LTS): selected mainline releases get fix backports for years. Check kernel.org/category/releases.html before choosing for a product. As of June 2026, active longterm lines include 6.18, 6.12, 6.6, 6.1, 5.15, and 5.10.

  • Vendor BSPs: NXP, ST, TI, and other silicon vendors ship forks pinned to a specific kernel minor with thousands of patches on top. NXP forks mainline into linux-imx. Active branches are pinned to 5.15 and 6.6. Older branches exist for legacy products.

We build from upstream Linux, not from a vendor BSP. For source-reading stability, the chapter examples pin to the v6.6 tag. For a shipping product, start from the current longterm branch that supports your hardware. The i.MX6ULL has had full support in mainline since v4.10 (released 2017). Chapter 30A goes deeper on when each track is appropriate.

25.2 Clone the source

$ cd ~/imx6ull/src
$ git clone --depth=1 --branch v6.6 \
      https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git linux-v6.6
$ cd linux-v6.6
$ git describe --tags --always
v6.6

v6.6 is an exact tag, so every reader sees the same source layout and similar line numbers. If you want the maintained longterm branch instead, clone --branch linux-6.6.y or, better, the newest longterm branch listed on kernel.org. That branch receives fixes, so line numbers drift over time.

The chapter examples assume v6.6 unless otherwise noted. Newer longterm kernels work similarly for our purposes, but command output and source line numbers will differ.

Directory layout (the parts you’ll touch)

linux/
├── arch/                # CPU architecture support
│   └── arm/             # 32-bit ARM, includes i.MX6ULL
│       ├── boot/        # boot wrapper + device tree compiler
│       │   ├── dts/     # device tree source files (*.dts, *.dtsi)
│       │   └── compressed/
│       ├── configs/     # *_defconfig files
│       ├── include/asm/ # ARM-specific kernel headers
│       ├── kernel/      # ARM-specific kernel entry (start.S, head.S)
│       ├── mach-imx/    # i.MX SoC family code
│       └── mm/          # ARM memory management
├── block/               # block device layer
├── crypto/              # cryptography subsystem
├── Documentation/       # Sphinx-rendered kernel docs
├── drivers/             # all device drivers, grouped by subsystem
│   ├── clk/imx/         # i.MX clock drivers
│   ├── gpio/            # GPIO drivers
│   ├── i2c/             # I²C bus + slave drivers
│   ├── input/           # input subsystem
│   ├── irqchip/         # interrupt controllers (incl. GIC)
│   ├── mmc/             # MMC / SD card
│   ├── net/             # network drivers
│   ├── pinctrl/         # pin control (incl. freescale/)
│   ├── rtc/             # real-time clocks
│   ├── spi/             # SPI bus
│   ├── tty/serial/      # UART drivers (incl. imx.c)
│   └── usb/             # USB host + gadget
├── fs/                  # file systems (ext4, fat, tmpfs, ...)
├── include/             # kernel-wide headers
│   ├── linux/           # the main public kernel API
│   ├── uapi/            # user-space ABI headers
│   └── dt-bindings/     # DT binding constants (clocks, gpios, IRQs)
├── init/                # kernel init: start_kernel, kernel_init
├── ipc/                 # System V IPC
├── kernel/              # core kernel: scheduler, signals, sysctl, locking
├── lib/                 # generic kernel utility code
├── mm/                  # memory management (page allocator, slab, ...)
├── net/                 # network stack
├── samples/             # example code
├── scripts/             # build scripts (Kconfig, kbuild, dtc)
├── security/            # LSMs (selinux, apparmor, ...)
├── sound/               # ALSA (SoC audio in sound/soc/)
├── tools/               # user-space companion tools
└── usr/                 # initramfs cpio packager

Every subsystem follows the same layout: top-level is the subsystem, next level is the vendor, and the lowest level is the SoC or board. The i.MX6ULL UART driver lives at drivers/tty/serial/imx.c. Its DT binding is at Documentation/devicetree/bindings/serial/fsl-imx-uart.yaml. Its register definitions are inside the driver file. This pattern repeats for every subsystem.

The four directories you will spend the most time in over the rest of this book:

  • arch/arm/boot/dts/: every chapter from 27 onward

  • drivers/<subsystem>/: every driver chapter in Part VI

  • Documentation/devicetree/bindings/: DT binding schemas (Ch 27A)

  • include/dt-bindings/: constants shared between DT source and driver source

25.3 Defconfig and the kernel’s config system

Like U-Boot, the kernel uses Kconfig + a .config file. The arch/arm/configs/ directory holds default starting points:

U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.

$ ls arch/arm/configs/ | grep imx
imx_v6_v7_defconfig
mxs_defconfig

imx_v6_v7_defconfig (formerly imx_v7_defconfig) is the omnibus i.MX configuration that, on v6.6, covers i.MX31/35/27 (ARMv6), i.MX5 (selected boards), i.MX6 (all variants including ULL), and i.MX7. One config builds for all of them. A single zImage boots on any board that has a matching DT. This is the mainline style.

$ . ~/imx6ull/scripts/env.sh
$ make imx_v6_v7_defconfig
#
# configuration written to .config
#

All make commands in Part IV assume env.sh exported:

export ARCH=arm
export CROSS_COMPILE=arm-none-linux-gnueabihf-

If you did not source that script, write those two variables directly on each make command.

Inspect .config:

$ grep -E '^CONFIG_(ARCH|SOC|MACH|ARM|EABI|VFP)' .config | head -20
CONFIG_ARCH_MULTIPLATFORM=y
CONFIG_ARCH_MXC=y
CONFIG_SOC_IMX6=y
CONFIG_SOC_IMX6UL=y          # ← i.MX6UL family (includes 6ULL)
CONFIG_SOC_IMX6Q=y
...

Every entry in .config is either compiled in (=y) or compiled as a loadable module (=m). The set of options is enormous (~7000 for a v6.6 kernel). For now we trust imx_v6_v7_defconfig’s defaults. Chapter 30 returns to specific knobs.

25.4 Build

Single-shot build of everything we need:

$ make -j$(nproc) zImage modules dtbs

Three artifact groups (make builds them in dependency order from the same object tree):

  • zImage: the compressed kernel image. ~6 MB. This is what U-Boot will bootz.

  • modules: every =m driver, compiled to .ko files. ~hundreds in a default config. Installed separately.

  • dtbs: every device tree blob the architecture defines. Includes imx6ull-14x14-evk.dtb for the NXP EVK and ~20 other i.MX6ULL variants.

On a modern host with -j$(nproc), the first build takes 5–10 minutes. Incremental rebuilds finish in seconds.

What just got produced

$ ls -lh arch/arm/boot/zImage
-rw-r--r-- 1 you you 6.0M Jan 22 14:42 zImage

$ ls arch/arm/boot/dts/nxp/imx/imx6ull*.dtb
imx6ull-14x14-evk.dtb
imx6ull-9x9-evk.dtb
imx6ull-colibri-eval-v3.dtb
imx6ull-colibri-iris.dtb
imx6ull-colibri-wifi-iris.dtb
...

$ find . -name '*.ko' | wc -l
432

$ ls -lh vmlinux
-rwxr-xr-x 1 you you 145M Jan 22 14:42 vmlinux

Four artefacts, four roles:

File

Type

Used for

vmlinux

ELF with full debug info

gdb / addr2line / oops decoding

arch/arm/boot/Image

Raw kernel binary, uncompressed

rarely used on ARM32 (used on AArch64)

arch/arm/boot/zImage

Compressed kernel + decompressor stub

what U-Boot loads

arch/arm/boot/dts/*.dtb

Compiled device tree blobs

one per board variant

**/*.ko

Loadable kernel modules

each =m driver

You do not ship vmlinux to the target, it is 20× the size of zImage. You do keep vmlinux around on the host because it has symbols zImage lacks (Chapter 57 uses it to decode panics).

Module installation to the rootfs

Privilege boundary: $ means normal user. # or sudo means root and can change host or target state. After a privileged command, verify the expected device, service, or file appears before continuing. Roll back by undoing the config change or stopping the service you just enabled.

The .ko files are scattered across the build tree. Before they are useful on the target, they need to be collected into a /lib/modules/<version>/ hierarchy:

$ make INSTALL_MOD_PATH=~/imx6ull/rootfs modules_install
$ ls ~/imx6ull/rootfs/lib/modules/
6.6.0/
$ ls ~/imx6ull/rootfs/lib/modules/6.6.0/kernel/drivers/ | head
acpi
ata
auxdisplay
base
block
bluetooth
...

INSTALL_MOD_PATH=~/imx6ull/rootfs is the path that becomes / on the target, usually your NFS-exported rootfs directory (Chapter 24). The make rule also generates modules.dep, modules.alias, and a few other index files so modprobe works on the target.

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. NFS: Network File System, which lets the target mount a host directory over Ethernet during development. rootfs: root filesystem, the directory tree mounted at / that contains /bin, /etc, /dev, and libraries.

25.5 zImage vs vmlinux vs Image, what compresses what

A short sketch of the wrapping:

              vmlinux (ELF with symbols, 145 MB)
                       │
                strip + objcopy
                       ▼
        arch/arm/boot/Image (raw binary, ~16 MB)
                       │
              gzip compression
                       ▼
              compressed/piggy.gzip (~6 MB)
                       │
   prepend decompressor stub (compressed/head.S + misc.c)
                       ▼
        arch/arm/boot/zImage (~6 MB)

When U-Boot bootz’s a zImage:

  1. U-Boot loads the zImage into DRAM at the address told (typically 0x82000000).

  2. U-Boot transfers control to the first instruction of zImage, which is the decompressor stub in arch/arm/boot/compressed/head.S.

  3. The stub copies itself out of the way, sets up a small workspace, and decompresses the gzipped kernel into the final run-from address.

  4. The stub jumps to the decompressed kernel’s entry point stext (Chapter 28). You see the famous “Uncompressing Linux… Done, booting the kernel.” message.

  5. The decompressor stub is now discarded. The kernel runs from where the stub decompressed it to.

This is why the kernel works regardless of whether you store it compressed or not, the kernel image carries its own decompressor.

25.6 Sanity check the build

$ file arch/arm/boot/zImage
arch/arm/boot/zImage: Linux kernel ARM boot executable zImage (little-endian)

$ file vmlinux
vmlinux: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
         statically linked, BuildID[sha1]=..., with debug_info, not stripped

$ arm-none-linux-gnueabihf-readelf -h vmlinux | head
ELF Header:
  ...
  Entry point address:               0x80008000
  ...

$ ls arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dtb
arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dtb

$ dtc -I dtb -O dts arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dtb | head -30
/dts-v1/;
/ {
    #address-cells = <0x01>;
    #size-cells = <0x01>;
    interrupt-parent = <0x01>;
    compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
    model = "Freescale i.MX6 ULL 14x14 EVK Board";
    ...
};

The dtc reverse-compile is a useful sanity check that the DT compiled correctly. We will walk this DT in detail in Chapter 27.

25.7 Make this fit your workspace

A practical workspace recipe:

$ mkdir -p ~/imx6ull/{src,build,rootfs}
$ cd ~/imx6ull/src
$ git clone --depth=1 --branch v6.6 \
      https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git linux-v6.6
$ cd linux-v6.6

# Build out-of-tree to keep the source clean (optional but recommended)
$ make O=~/imx6ull/build/kernel imx_v6_v7_defconfig
$ make O=~/imx6ull/build/kernel -j$(nproc) zImage modules dtbs

# Symlink artefacts into TFTP and rootfs
$ ln -sf ~/imx6ull/build/kernel/arch/arm/boot/zImage /srv/tftp/zImage
$ ln -sf ~/imx6ull/build/kernel/arch/arm/boot/dts/nxp/imx/imx6ull-14x14-evk.dtb /srv/tftp/imx6ull.dtb

# Install modules into NFS-exported rootfs
$ make O=~/imx6ull/build/kernel INSTALL_MOD_PATH=~/imx6ull/rootfs modules_install

O=... puts every generated file in a sibling directory. The source tree stays bit-identical to what git checked out. Cleanups are rm -rf ~/imx6ull/build/kernel.

25.8 Key Notes

  • A kernel build is defined by its source revision, resolved .config and toolchain. An output directory separates generated state from source, but does not make different configurations interchangeable.

  • vmlinux carries symbols for host-side diagnosis, while zImage packages the ARM32 kernel and its decompressor for loading. Their different sizes reflect different roles, not different kernel features.

  • One multiplatform kernel can support several boards through separate DTBs. An EVK or Colibri DTB is a hardware description for that board, not evidence of a matching MINI description.

  • Built-in features are present before rootfs access. Loadable .ko files require a matching kernel and a reachable module tree, with indexes used by modprobe.

  • INSTALL_MOD_PATH selects the target rootfs staging tree. Omitting it crosses into the host’s module installation path rather than preparing the target.

  • Successful artifact generation establishes build output, not correct board wiring, DDR behavior or a successful Linux boot.

25.9 Pitfalls

Driver choice: Use the in-tree, maintained driver first. Use out-of-tree, spidev, or custom-driver paths only after you accept the kernel-version maintenance cost and document who owns updates.

  • Forgetting the Chapter 3 environment. If you do not run . ~/imx6ull/scripts/env.sh, make will build for the host x86-64 machine and fail somewhere deep in arch code. Source the script before invoking make.

  • Building from the source tree without O=. Works, but git status becomes useless because every make populates the source tree with .o files. Out-of-tree builds keep the source pristine.

  • Wrong defconfig. make imx_v6_v7_defconfig not make x86_64_defconfig. The latter happens when you forget to export ARCH=arm, Linux helpfully picks the host default.

  • Old gcc-toolchain miscompile. Mainline kernels usually require a fairly recent gcc (≥ 5.1 for v6.x. ≥ 4.9 for older). The Arm GNU Toolchain from Chapter 3 is fine. Custom-built ancient toolchains sometimes miscompile RCU or AAPCS-sensitive code paths.

  • make modules_install to a system location. By default make modules_install writes to /lib/modules/$(uname -r)/. Always pass INSTALL_MOD_PATH=... when cross-building or you will overwrite your host’s modules.

  • Mismatch between zImage and modules. Modules built against kernel version X will refuse to load on a running kernel built from version Y (they check the version’s “vermagic” string). If you rebuild the kernel, rebuild + reinstall modules.

25.10 Going deeper

  • Documentation/admin-guide/README.rst in the kernel tree, the upstream-maintained README. Read it once.

  • Documentation/process/: how the community works (coding-style.rst, submitting-patches.rst, 4.Coding.rst).

  • Documentation/kbuild/: the kernel build system. kconfig.rst and makefiles.rst are the most useful.

  • Documentation/arch/arm/: ARM-specific docs, including the .dts → .dtb flow.

  • kernelnewbies.org: the friendliest entry point for new kernel hackers.

  • The kernel mailing list archive at lore.kernel.org, search [PATCH] imx6ull to read every recent i.MX6ULL change discussion.

Next chapter: Chapter 26: Booting the kernel from U-Boot. The boot case study follows a zImage through the firmware handoff and interprets the kernel’s early output.