Devicetree

Revision as of 19:15, 19 February 2025 by Knuxify (talk | contribs) (add devicetree navbox)

The devicetree (device tree, DT) is a data structure which holds information about all components present on a device. This data is structured in nested nodes with key/value property pairs for configuration.

In simpler terms - a devicetree tells the kernel (or another DT-compatible piece of software/firmware like a bootloader) where each component is in register space/on an I2C or similar bus, and what settings to use to set it up. It is the basic mechanism for discovering components on embedded platforms, including ARM.

Devicetrees are written in a plaintext format known as the Device Tree Source (DTS) format. The DTS is later compiled into a Device Tree Blob (DTB); in this form, it can be loaded by software/firmware.

Devicetrees are validated using devicetree schema, which is described by bindings; see /Bindings for more information.

History

The initial version of the devicetree standard was developed as part of the OpenFirmware initiative; from this standard, the Flattened Device Tree (FDT) emerged and was adopted by the Linux kernel for PowerPC platforms. Around 2009, discussions began to include FDT support for ARM[1]. It was eventually added and first device trees began to appear in 2011[2], although the format didn't see wider usage (especially in vendor kernels) until around 2013/2014.

Nowadays, the devicetree standard is managed by devicetree.org; they maintain the latest version of the Devicetree Specification and the related set of core DT schema.

Before the introduction of devicetrees, ARM kernels used board files. These were C files stored in arch/arm/mach-* which served a similar purpose to devicetrees - they contained structures for defining component configuration ("platform data"). Unlike device trees however, they could also define C functions, since they were regular C sources compiled into the kernel. Board files technically still exist (citation needed?), but are no longer in wide use.

Device Tree Source (DTS) basics

Here is a very simple DTS file to explain the basics of what you might see in a device tree source file:

foo.dts
/dts-v1/;

/ {
	#address-cells = <1>;
	#size-cells = <1>;
	
	my_node: node1@1000 {
		compatible = "vendor,foo";
		reg = <0x1000 0x54>;
		vendor,bar-factor = <0x2a>;
	};

	node2@2000 {
		compatible = "vendor,bar";
		reg = <0x2000 0xa4>;
		vendor,baz-companion = <&my_node>;
	};

	i2c-controller@3000 {
		compatible = "vendor,foobar-i2c";
		reg = <0x3000 0x800>;
		
		#address-cells = <1>;
		#size-cells = <0>;
		
		sensor@10 {
			compatible = "vendor,baz-sensor";
			reg = <0x10>;
		};
	};
};

In this example, we define an empty DTS; in its root node (/), we place three nodes: a node named "node1" with a label "my_node" at address 0x1000, a node named "node2" at address 0x2000, and a node named "i2c-controller" at address 0x3000.

A node contains:

  • A compatible value (here "vendor,foo"). This specifies what kind of component it is; Linux uses this information to load the correct driver.
  • A reg value. In this case, the first parameter is the base address in memory (0x1000), and the second is the size it occupies (0x54). The amount of cells for the address and size are specified by the #address-cells and #size-cells properties of the root node, respectively.
    • You might notice that the i2c-controller node defines another set of these #address-cells and #size-cells properties - this is because I2C devices contain their own subdevices with addresses ranging from 0x08 to 0x7f (todo verify), and they do not use a size.
  • A custom vendor-specific property, vendor,bar-factor, which takes a number.

On node 2 there is also a property, vendor,baz-companion, which takes a pointer to another node - here the one we labeled "my_node".

Inclusion

Very often you'll see #include directives; these can either include header files (which usually contain constants defined with #define, similarly to C) or other DTSes. DTS files meant for inclusion are called DTS includes and use the .dtsi file extension.

Usually, an SoC will have a common DTS include with all of the relevant nodes for that SoC. A board's DTS will then include the SoC DTSI.

As an example, here's a very bare-bones DTSI for a fictional "FOO1234" SoC:

foo1234.dtsi
/*
 * Common DTSI for Foobar FOO1234 System-on-a-Chip.
 */
/dts-v1/;

/ {
	#address-cells = <1>;
	#size-cells = <1>;

    cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpu0: cpu@0 {
			device_type = "cpu";
			compatible = "arm,cortex-a7";
			reg = <0>;
			clock-frequency = <1000000000>;
		};
    };

    soc {
        /* ... */

        i2c1: i2c@3000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x3000 0x800>;
            status = "disabled";
        };

        i2c2: i2c@4000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x4000 0x800>;
            status = "disabled";
        };

        i2c3: i2c@5000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x5000 0x800>;
            status = "disabled";
        };

        /* ... */
    };
};

Here is a DTS for a fictional device named "Quox Haystack" that uses the FOO1234 SoC:

foo1234-quox-haystack.dts
/*
 * Device tree source for Quox Haystack based on FOO1234 SoC
 */
/dts-v1/;

#include "foo1234.dtsi"

/ {
	compatible = "quox,haystack", "foobar,foo1234";

	memory@80000000 {
		device_type = "memory";
		reg = <0x80000000 0x40000000>;
	};
};

&i2c1 {
    clock-frequency = <400000>;
    status = "okay";

    sensor@10 {
		compatible = "foobar,baz-sensor";
		reg = <0x10>;
	};
};

Some interesting things to look out for:

  • The board DTS includes the SoC DTSI with a #include "foo1234.dtsi" directive.
  • We re-define the root / node, and inside of it we place a memory node. This node, as well as the compatible, will be merged into the root node of the included DTSI.
  • Outside of the root node, there is a node with a name starting with an ampersand (&i2c1); this signifies a pointer to a labeled node (see node labels in the previous example). Like with the root node above, the contents of this node will be merged with the contents of the node with this label in the SoC DTSI.
    • A similar effect could be achieved by overriding the i2c1 node under the root node, where it's located in the SoC DTSI (though the preferred way is to do it with a label - we do it here for demonstration purposes):
    foo1234-quox-haystack.dts
    / {
    	compatible = "quox,haystack", "foobar,foo1234";
    
        /* ... */
    
    	i2c1: i2c@3000 {
            clock-frequency = <400000>;
            status = "okay";
    
            sensor@10 {
    	    	compatible = "foobar,baz-sensor";
    	    	reg = <0x10>;
    	    };
    	};
    };
    

For a more visual example - the merged version of these two files would look something like this:

/dts-v1/;

/ {
	#address-cells = <1>;
	#size-cells = <1>;

    compatible = "quox,haystack", "foobar,foo1234";

	memory@80000000 {
		device_type = "memory";
		reg = <0x80000000 0x40000000>;
	};
	
    cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpu0: cpu@0 {
			device_type = "cpu";
			compatible = "arm,cortex-a7";
			reg = <0>;
			clock-frequency = <1000000000>;
		};
    };

    soc {
        /* ... */

        i2c1: i2c@3000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x3000 0x800>;
            clock-frequency = <400000>;
            status = "okay";

            sensor@10 {
                compatible = "foobar,baz-sensor";
                reg = <0x10>;
            };
        };

        i2c2: i2c@4000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x4000 0x800>;
            status = "disabled";
        };

        i2c3: i2c@5000 {
            compatible = "vendor,foobar-i2c";
            reg = <0x5000 0x800>;
            status = "disabled";
        };

        /* ... */
    };
};

Verifying DTS files

The Linux kernel has tools for making sure that device tree sources (DTS) use the bindings correctly. These are useful when writing device sources; getting the DT checks to pass is also mandatory for upstream inclusion of a DTS.

To verify all DTS files built with the selected defconfig options, run:

$ make dtbs_check

To verify a specific DTS, build the DTB target directly and provide the CHECK_DTBS=y option:

$ make CHECK_DTBS=y qcom/sm8450-hdk.dtb

You can also test a DTS against a specific subset of bindings by providing the DT_SCHEMA_FILES variable as mentioned in the previous section:

$ make CHECK_DTBS=y DT_SCHEMA_FILES=trivial-devices.yaml qcom/sm8450-hdk.dtb

Working with DTC

The dtc tool handles compiling DTS files into DTBs, as well as decompiling DTBs back into DTS. (TODO: add instructions)

See also

References

todo enable refs extension

[1] https://www.mail-archive.com/[email protected]/msg01721.html [2] https://github.com/torvalds/linux/commit/b85a3ef4ac65169b65fd2fe9bec7912bbf475ba4