How does Porting Linux With A Device Tree work in Linux-Free Embedded Linux Course

Porting Linux With A Device Tree
Bringing up a brand-new ARM board using an existing SoC’s kernel support
Free Embedded Systems Course
Chapter 4 · Porting & Configuring the Kernel
Original board bring-up walkthrough
porting linux to a new board
device tree
free linux kernel development course
free embedded linux course
arch/arm mach directories

Porting Linux to a new board sounds intimidating the first time you’re asked to do
it, but for the common case — a new board built around an SoC the kernel already supports — most
of the “port” is describing your hardware in a device tree, not writing new C code. This lecture,
part of our free linux kernel development course, walks through exactly that scenario:
taking a kernel that already boots a similar reference board and adapting it to boot a new,
custom board of your own design.

What You Will Learn

  • Why the scope of a board port depends entirely on how similar your hardware is to an existing
    supported board
  • How Linux organises architecture-specific code differently across x86, PowerPC, and ARM
  • How to create a new board’s device tree by adapting an existing reference board’s .dts
  • How to build a standalone .dtb and how to wire it into the default build via the arch
    Makefile
  • How to boot-test your new device tree and confirm the kernel recognises your board correctly

Prerequisites

  • A working cross-toolchain and a kernel source tree you can already build for your target
    architecture (covered earlier in this course)
  • Basic device tree syntax — nodes, properties, compatible strings
  • A reference board with an existing, working device tree that is electrically close to your new
    design

How Much “Porting” Do You Actually Need?

The honest answer is: it depends entirely on how different your board is from something the
kernel already supports. If your board is a variant of an existing reference design — same SoC,
same basic memory and peripheral layout, maybe a different sensor on one I2C bus — you are looking
at a device tree edit, not new driver work. If you’re bringing up a genuinely novel SoC or a
wildly different hardware layout, the effort is much larger and out of scope for a single
lecture. This lecture covers the common, practical case: a new board around an already-supported
SoC.

Where Architecture-Specific Code Lives

Before touching anything, it helps to know how differently Linux organises per-board code across
architectures — the layout is not the same everywhere:

Per-Architecture Board Code Layout
arch/x86/
-> hardware detected mostly at runtime,
very little board-specific source per boardarch/powerpc/
-> platforms/
SoC- and board-specific files grouped
under a “platforms” subdirectoryarch/arm/
-> mach-<soc-family>/
one directory approximately per SoC family
(largest and most fragmented layout, because
there are simply the most ARM SoCs and boards)
-> plat-<family>/
code shared across several SoC versions
within one family

ARM has by far the most board- and SoC-specific files of any architecture, precisely because
there are so many ARM SoC vendors and board variants in the field. Each SoC family typically has
its own mach-* directory under arch/arm — and the name doesn’t always tell you the full
story. A single mach-* directory frequently supports several silicon generations of the same
SoC family, not just one chip.

Step 1 — Pick Your Closest Reference Board

For this walkthrough, imagine we’re bringing up a new board we’ll call ep-devboard,
built around the same SoC family as an existing, well-supported reference board. Rather than
writing a device tree from a blank file, we start from the reference board’s .dts and adapt
it — this is standard practice, and it’s exactly how real vendor board support usually starts.

Step 2 — Create the New Device Tree

Copy the reference board’s device tree source to a new file for your board, then change the
identifying fields:

$ cp arch/arm/boot/dts/soc-refboard.dts arch/arm/boot/dts/soc-ep-devboard.dts

Edit the new file’s identity so it no longer claims to be the reference board:

/dts-v1/;
#include "soc-family.dtsi"
#include "soc-refboard-common.dtsi"

/ {
    model = "EP DevBoard";
    compatible = "vendor,ep-devboard", "vendor,soc-refboard", "vendor,soc-family";
};

Keeping the reference board’s compatible string as a fallback entry (after your own) is
deliberate — it lets any driver that only matches the reference board’s compatible string still
bind correctly on your new board, until you give it its own more specific match.

Step 3 — Build the Device Tree Blob Explicitly

You can build just your new .dtb directly, which is the fastest way to iterate while you’re
still editing it:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- soc-ep-devboard.dtb

Step 4 — Wire It Into the Default Build

Once the board is stable, add it to the architecture’s DTS Makefile so it gets built
automatically every time, alongside every other board in that SoC family:

# arch/arm/boot/dts/Makefile
dtb-$(CONFIG_SOC_FAMILY_XX) += \
    soc-refboard.dtb \
    soc-ep-devboard.dtb \
    [...]

Step 5 — Boot Test

Reuse the exact same kernel image you already have working for the reference board — with a
device-tree-based port, the kernel image itself usually doesn’t need to change at all. Only the
.dtb you load changes:

$ qemu-system-arm -M virt -kernel zImage -dtb soc-ep-devboard.dtb \
    -append "console=ttyAMA0 root=/dev/vda rootwait rw" -nographic

Confirm the kernel actually recognised your new board’s identity in its boot log:

Starting kernel ...
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 6.9.0 (builder@ep) #1 SMP PREEMPT
[    0.000000] CPU: ARMv7 Processor [413fc082] revision 2 (ARMv7)
[    0.000000] Machine model: EP DevBoard
...

That Machine model: EP DevBoard line is your confirmation: the kernel parsed your device
tree, recognised the model string you set, and is now running with your board’s identity —
not the reference board’s.

Custom Kernel Configuration for the New Board

Once boot is confirmed, it’s common to trim a full multi_v7_defconfig-style configuration
down to just what your board actually needs — dropping drivers for peripherals you don’t have
shrinks image size and reduces boot time:

$ cp arch/arm/configs/multi_v7_defconfig arch/arm/configs/ep_devboard_defconfig
$ make ARCH=arm ep_devboard_defconfig menuconfig
# disable unused drivers, save, then build normally
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

Real-World Use Cases

  • Custom carrier boards — a vendor SoM (system-on-module) on a bespoke carrier PCB is
    almost always a device-tree-only port, since the SoM itself is already supported
  • Product variants — a company shipping three SKUs of the same product (different
    sensors, different display) typically ships three device trees against one shared kernel image
  • Overlay-based development boards — rapid prototyping boards that let you swap device
    tree overlays for different peripheral add-on boards, without touching the base kernel at all

Common Mistakes and Troubleshooting

Symptom Likely Cause Fix
Kernel boots but shows the reference board’s model name, not yours Bootloader is still loading the old .dtb file, not your new one Confirm the exact dtb filename passed by U-Boot/QEMU matches your new file, and check /proc/device-tree/model after boot
A peripheral that worked on the reference board is missing on your board You changed a pin mux or removed a node while adapting the .dts, breaking a dependency another node relied on Diff your new .dts against the reference board’s line by line, don’t just delete nodes blindly
Build succeeds but the new .dtb is never produced by a plain make dtbs Forgot to add the new filename to the arch Makefile’s dtb-$(CONFIG_...) line Add it explicitly, or continue building it by its explicit target name during development

Best Practices

  • Start from the closest electrically-similar reference board’s device tree, never from a blank
    file — you inherit a huge amount of correct, tested configuration for free
  • Keep the reference board’s compatible string as a fallback in your new compatible
    property list during early bring-up, then remove it once your board has its own dedicated
    driver matches where needed
  • Boot test after every meaningful device tree change, not just at the end — device tree bugs
    are far easier to isolate one node at a time
  • Only trim the defconfig down to a board-specific config after boot is fully stable, not before

Summary and Key Takeaways

  • For a board built around an already-supported SoC, “porting Linux” is mostly a device tree
    exercise, not new kernel C code
  • ARM’s per-SoC-family mach-* layout under arch/arm is the most fragmented of any
    architecture, because of how many ARM SoCs and boards exist
  • Copy-adapt the closest reference board’s .dts, change model and compatible, build,
    and boot test — that loop is the entire device-tree porting workflow
  • The Machine model: line in the boot log is your ground-truth confirmation that the
    correct device tree was loaded

Conclusion

Device-tree-based board porting is one of the friendliest on-ramps into real embedded Linux
bring-up work, precisely because it lets you reuse almost everything the kernel community has
already tested on your reference board. Once you’re comfortable copying, adapting, and boot
testing a device tree, you’re equipped to bring up real custom hardware — a core, practical skill
in this free embedded systems course. In the next lecture we’ll cover the older,
harder path: porting a board that has no device tree support at all.

Frequently Asked Questions

Do I need to modify the kernel image itself when porting via device tree?

Usually not, if your SoC family is already supported. The same zImage/Image that boots
the reference board typically boots your new board unchanged — only the .dtb you load
differs. You only touch kernel C code if your board has genuinely new hardware the kernel has no
driver for at all.

What’s the difference between mach-* and plat-* directories on ARM?

mach-* holds code specific to one SoC family (often covering several chip generations within
that family). plat-* holds code shared across multiple SoC families that share a common IP
block or platform design, rather than being tied to just one mach-* directory.

Should I keep the reference board’s compatible string in my device tree permanently?

It’s fine as a fallback during early bring-up so existing drivers keep matching, but longer term
it’s cleaner to give your board its own specific compatible entries once you understand which
drivers actually need board-specific behaviour, so you don’t silently inherit reference-board
quirks that don’t apply to your hardware.

Why does my board show the wrong model name after I updated the device tree?

Almost always because the bootloader is still loading a stale or wrong .dtb file from
storage. Rebuilding the kernel doesn’t help if the actual file the bootloader loads at boot time
wasn’t updated — check the exact path/filename your bootloader is pointed at.

Can one kernel image support multiple different boards via different device trees?

Yes — this is one of the main benefits of the device tree model. A single generic kernel build
like multi_v7_defconfig can boot many different boards, with the specific hardware
description supplied entirely by whichever .dtb is loaded at boot time.

 

Want to keep going?

Continue this free embedded Linux course with the next lecture on porting Linux without a device tree.

 

Leave a Reply

Your email address will not be published. Required fields are marked *