What are Device Tree Include Files Explained in Linux-Embedded Linux Training In Hyderabad

PREV_LEC NEXT_LEC

Device Tree Include Files Explained
How .dtsi files, /include/, and C-style #include let SoC vendors and board designers share and layer device tree source — free embedded linux course
Chapter 3 · Lecture 8
Free Embedded Systems Course
Beginner to Intermediate

Open the device tree source for almost any real Linux board and the first thing you’ll notice is that it’s tiny — often under a hundred lines. That’s not because the hardware is simple; it’s because the board file only contains what’s unique to that board. Everything the board shares with its SoC family — the CPU cores, the memory controller, the peripheral register maps — lives in a separate, shared file that gets pulled in through an include mechanism. Understanding that layering is essential for anyone working through a free linux kernel development course, because it’s how the kernel supports hundreds of boards without duplicating thousands of lines of address maps.

In this lecture we’ll look at both include mechanisms the device tree ecosystem actually uses, how they overlay on top of one another, and how to trace a property back to the file that actually set it.

Keywords covered in this lecture
device tree include files
dtsi
include directive dts
device tree overlay nodes
node layering
free linux development course

What You Will Learn

  • Why device tree source is split into shared and board-specific files
  • The difference between the Open Firmware /include/ directive and the C-style #include
  • How the C preprocessor gets involved in building a device tree source file
  • How later includes overlay and modify nodes defined by earlier ones
  • How to trace which file actually set a given property’s final value

Prerequisites

This lecture assumes you’re comfortable with basic node/property syntax, the reg property, and phandles, covered in the two previous lectures of this free embedded linux course.

Why Split Device Tree Source At All

A family of SoCs — say, four different chip variants built around the same CPU core and bus fabric — will share the vast majority of their memory map. What differs between individual boards using those chips is typically much smaller: which peripherals are actually wired out to a connector, which GPIO drives a reset line, which regulator powers which rail. Device tree include files let vendors write the shared 90% once and let each board file supply only the remaining 10%.

Method 1: The Open Firmware /include/ Directive

The Devicetree Specification itself defines /include/ as the standard mechanism for pulling one device tree source file into another. It’s a plain textual include — the contents of the named file are spliced in at that point, verbatim.

Open Firmware style include
/include/ “epsoc-common.dtsi”

Method 2: C-Style #include

In practice, most modern in-tree device tree files use the C preprocessor’s own #include directive instead — the same syntax you already know from C source files. This has one big advantage over the Open Firmware form: it lets device tree files also pull in #define macro headers, not just other .dtsi node files.

C-style includes in a board file, and a macro header they pull in
#include “epsoc-family.dtsi”
#include “epboard-common.dtsi”
#include <dt-bindings/gpio/epsoc-gpio.h>
#include <dt-bindings/clock/epsoc-clock.h>

A macro header like dt-bindings/gpio/epsoc-gpio.h typically just defines plain C constants:

dt-bindings/gpio/epsoc-gpio.h — ordinary C macros, reused in dts source
#define EP_GPIO_ACTIVE_LOW 1
#define EP_GPIO_ACTIVE_HIGH 0
#define EP_GPIO_OPEN_DRAIN (1 << 2)

Because the build system runs the whole device tree source through the C preprocessor (cpp) before handing it to the actual device tree compiler, these macros get textually substituted just like they would in a .c file — long before dtc ever sees a raw node. That’s the whole trick: device tree source can reuse the same numeric constants as the kernel’s own C code, because it’s literally preprocessed by the same tool.

Build pipeline: source .dts/.dtsi files to compiled .dtb
board.dts + #include chain
│
▼
C preprocessor (cpp)
resolves #include and #define
│
▼
flattened, macro-free .dts text
│
▼
device tree compiler (dtc)
│
▼
board.dtb (binary, loaded by bootloader)

How Included Nodes Overlay

The real power of the include mechanism isn’t just textual pasting — it’s that node labels let a later file reopen and extend a node that an earlier file already defined. If two files both define a node with the same name (or reference it via the same &label), the properties merge, with later assignments overriding earlier ones for the same property name.

epsoc-family.dtsi — generic, applies to every board using this SoC family
i2c1: i2c@50020000 {
compatible = “ep,socx-i2c”;
reg = <0x50020000 0x1000>;
clock-frequency = <100000>;
status = “disabled”;
};
epboard-devkit.dts — this specific board enables and configures it
#include “epsoc-family.dtsi”&i2c1 {
status = “okay”;
clock-frequency = <400000>;
pinctrl-names = “default”;
pinctrl-0 = <&i2c1_pins_devkit>;
};

Notice what happened: status flips from "disabled" in the shared family file to "okay" in the board file, because not every board wires out every peripheral. clock-frequency is overridden entirely (400 kHz instead of the family default of 100 kHz). And pinctrl-0 is added fresh — it didn’t exist in the family file at all, because pin muxing is inherently board-specific.

Layer Typical contents Example
SoC family .dtsi CPU cores, memory controller, all peripheral register maps, all disabled by default epsoc-family.dtsi
Board common .dtsi Peripherals shared across a family of near-identical boards (e.g. dev-kit variants) epboard-common.dtsi
Board .dts Final overrides unique to one physical board — pinctrl, regulators, enabled peripherals epboard-devkit.dts

Verifying the Final, Flattened Result

Because so many files can contribute to one final node, the most reliable way to answer “what value does this property actually end up with?” is to decompile the compiled .dtb and read the flattened output, rather than trying to trace it through several source files by eye.

Decompiling a built .dtb to see the final merged node
$ dtc -I dtb -O dts epboard-devkit.dtb | grep -A 6 “i2c@50020000”
i2c1: i2c@50020000 {
compatible = “ep,socx-i2c”;
reg = <0x50020000 0x1000>;
clock-frequency = <400000>;
status = “okay”;
pinctrl-names = “default”;
pinctrl-0 = <0x0f>;
};

This single command is one of the most useful debugging habits you can build in this free embedded linux course — it shows you the ground truth the kernel actually receives, independent of how many .dtsi layers contributed to it.

A Minimal Original Include Example You Can Build

You don’t need real hardware to experiment with this. Here’s a small, original three-file example you can compile locally with dtc to see the overlay behavior for yourself:

ep-base.dtsi
/ {
led0: led-controller@0 {
compatible = “ep,demo-led”;
status = “disabled”;
brightness-levels = <4>;
};
};
ep-demo-board.dts
/dts-v1/;
#include “ep-base.dtsi”&led0 {
status = “okay”;
brightness-levels = <8>;
};
Compile and check the merged output
$ dtc ep-demo-board.dts -o ep-demo-board.dtb
$ dtc -I dtb -O dts ep-demo-board.dtb | grep -A 4 led-controller
led0: led-controller@0 {
compatible = “ep,demo-led”;
status = “okay”;
brightness-levels = <8>;
};

status and brightness-levels both took the board file’s values, exactly as the overlay rules predict.

Common Mistakes and Troubleshooting

  • Mixing include styles inconsistently. Both /include/ and #include work, but mixing them arbitrarily across a codebase makes it harder to predict whether macro headers are available at a given point.
  • Assuming node order matters for merging. It’s the label/name match that determines merging, not textual order beyond “later overrides earlier” for the same property.
  • Forgetting a peripheral is disabled by default upstream. Many SoC family files ship every peripheral as status = "disabled"; if your board file never flips it to "okay", the driver never binds — silently.
  • Debugging from source instead of the compiled output. With three or four layers of includes, eyeballing source is error-prone — always confirm with dtc -I dtb -O dts.

Best Practices

  • Ship every peripheral in the SoC family .dtsi as status = "disabled" by default, and let board files opt in explicitly.
  • Keep board-specific values (pinctrl, regulators, board-specific clock rates) out of the shared family file entirely.
  • Reuse dt-bindings/ macro headers for constants shared with kernel C code instead of hardcoding raw numbers in both places.
  • Always sanity-check the final merged tree with dtc -I dtb -O dts before debugging a driver bind failure.

Summary and Key Takeaways

  • Device tree source is split into shared .dtsi files and board-specific .dts files to avoid duplication across a chip family.
  • Two include mechanisms exist: the Open Firmware /include/ directive, and the more common C-style #include.
  • The whole source tree is run through the C preprocessor before dtc, so #define macro headers work inside device tree source.
  • Later includes overlay earlier ones property-by-property on matching node labels; later wins.
  • dtc -I dtb -O dts on the compiled output is the most reliable way to see the final merged tree.

Conclusion

The include mechanism is what makes device trees scale to the enormous diversity of embedded boards Linux supports today — one SoC family file, reused by dozens of boards, each contributing only what’s genuinely unique to it. Once you can read a board’s .dts and mentally reconstruct which layer set which property, debugging device tree issues on any unfamiliar board becomes dramatically faster. Next in this free linux kernel development course, we’ll close out the device tree chapter by actually compiling a tree with dtc and understanding its command-line options in depth.

Frequently Asked Questions

What is a .dtsi file in the Linux device tree?

A device tree include file — conventionally holding hardware definitions shared across multiple boards, such as an SoC family’s peripheral register maps.

What’s the difference between /include/ and #include in device tree source?

/include/ is the Open Firmware standard directive for pulling in another device tree file; #include is the C preprocessor directive, which additionally allows pulling in macro headers with #define constants.

Why does device tree source get run through the C preprocessor?

So that #include and #define statements resolve into plain text before the device tree compiler runs, letting device tree source reuse the same constant definitions as kernel C code.

What happens when two included files define the same node?

Their properties are merged onto the same node; when both files set the same property, the value from the later-included or overriding file wins.

Why do SoC family .dtsi files often set status = disabled?

So that a peripheral present in silicon but not wired out on a particular board doesn’t get bound to a driver unless the board file explicitly enables it.

How do I see the final merged device tree after includes are resolved?

Compile it to a .dtb and decompile it back with dtc -I dtb -O dts, which shows the fully flattened, merged tree exactly as the kernel receives it.

Can a board .dts file override a property set in a shared .dtsi?

Yes, by reopening the same node via its label and assigning a new value to that property; the override takes precedence over the shared definition.

 

Continue the Free Embedded Linux Bootloader Course

Next up: compiling device trees with dtc, from source to the binary blob the bootloader hands the kernel.

PREV_LEC NEXT_LEC

Leave a Reply

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