Mastering Device Tree: A Complete Guide to Custom Hardware Integration in Linux
This article explains the role of Device Tree in embedded Linux, its history, file formats, compilation methods, syntax details, and provides practical examples—including GPIO, LED, and custom sensor implementations—to help developers create and integrate custom hardware descriptions efficiently.
In embedded development, the Device Tree (DT) acts as a precise hardware description that enables the operating system to recognize and manage hardware resources efficiently.
1. Device Tree Overview
Before Device Tree, developers had to modify kernel code for each board, leading to cumbersome and error‑prone porting, especially on ARM platforms. Device Tree decouples hardware description from kernel code, reducing maintenance cost and simplifying board migration.
Originally introduced on PowerPC, Device Tree is now widely used on ARM, MIPS and other architectures.
1.1 Device Tree in ARM
When porting Linux to new ARM boards, developers previously added many C files under arch/arm/…, inflating the kernel. The Linux community eventually adopted Device Tree, placing all board files in arch/arm/boot/dts (or arch/arm64/boot/dts for 64‑bit).
1.2 What is Device Tree?
Device Tree is a data structure describing board‑level devices such as CPUs, peripherals, and bus addresses.
DTS – Device Tree Source file (human‑readable)
DTB – Device Tree Blob, the compiled binary form of a DTS file
DTC – Device Tree Compiler, converts DTS to DTB and vice‑versa
1.3 Device Tree Compilation
makeCompile the whole kernel. make dtbs Compile all Device Tree files. make <xxx.dtb> Compile a specific Device Tree source.
2. Device Tree Syntax
2.1 Terminology
DT (Device Tree) – hierarchical description of hardware components.
FDT (Flattened Device Tree) – binary representation of DT, loaded by the bootloader.
dts – source file written in a readable text format.
dtsi – include file, similar to a C header, shared among multiple DTS files.
dtb – compiled binary blob used by the kernel at boot.
dtc – compiler that translates between DTS and DTB.
2.2 Source Locations
For 32‑bit kernels, Device Tree sources reside in arch/arm/boot/dts; for 64‑bit kernels, they are in arch/arm64/boot/dts.
2.3 Using DTC
Compile a DTS file to DTB:
dtc -I dts -O dtb -o output_file.dtb input_file.dtsDe‑compile a DTB back to DTS: dtc -I dtb -O dts -o example.dts example.dtb Additional useful options include -q for quiet output and -i to add include paths.
3. Advanced Syntax
3.1 Nodes
Nodes are the basic building blocks, identified as node-name@unit-address. Example:
spi@12340000 {<br/> compatible = "fsl,imx6ul-spi";<br/> reg = <0x12340000 0x1000>;<br/> interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;<br/> #address-cells = <1>;<br/> #size-cells = <0>;<br/><br/> spi_device: spi@0 {<br/> compatible = "my_spi_device";<br/> reg = <0>;<br/> spi-max-frequency = <5000000>;<br/> };<br/>};3.2 Properties
Properties use the name = value syntax. Common types include:
32‑bit unsigned integers: interrupts = <17 0xc>; Strings: compatible = "arm,cortex-a7"; Byte arrays: local-mac-address = [00 00 12 34 56 78]; Composite values:
example = <0xf00f0000 19>, "a strange property format";The compatible property matches devices to drivers, while reg describes address ranges.
3.3 Include Files
Shared descriptions are placed in .dtsi files and included with #include "common.dtsi". Standard C headers can also be included, e.g., #include <dt-bindings/gpio/gpio.h>.
3.4 Node Paths
Each node has a unique path, e.g., /soc/spi@12340000. Kernel code can locate a node with:
#include <linux/of.h><br/>#include <linux/device.h><br/><br/>struct device_node *spi_node;<br/>spi_node = of_find_node_by_path("/soc/spi@12340000");<br/>if (spi_node) {<br/> // use the node<br/> of_node_put(spi_node);<br/>}3.5 Aliases
Aliases provide short names for long paths:
aliases {<br/> spi0 = &spi@12340000;<br/> uart1 = &uart@101f0000;<br/>};3.6 Composite Nodes and References
Complex hardware can be described with composite nodes and label references:
chip@10000000 {<br/> compatible = "my_chip";<br/> reg = <0x10000000 0x10000>;<br/><br/> module1: submodule@1000 {<br/> compatible = "module1_type";<br/> reg = <0x1000 0x100>;<br/> };<br/><br/> module2: submodule@2000 {<br/> compatible = "module2_type";<br/> reg = <0x2000 0x200>;<br/> };<br/>};<br/><br/>other_node {<br/> depends_on = <&module1>;<br/>};4. Practical Examples
4.1 GPIO Example
gpio0: gpio@10000000 {<br/> compatible = "fsl,imx6ul-gpio", "gpio-generic";<br/> reg = <0x10000000 0x1000>;<br/> interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>;<br/> gpio-controller;<br/> #gpio-cells = <2>;<br/>};Device drivers reference this controller with gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;.
4.2 LED Example
leds {<br/> compatible = "gpio-leds";<br/> pinctrl-names = "default";<br/> pinctrl-0 = <&led_pins>;<br/><br/> led_red: red_led {<br/> label = "red_led";<br/> gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;<br/> default-state = "off";<br/> };<br/>};<br/><br/>pinctrl: pinctrl@10000400 {<br/> compatible = "fsl,imx6ul-pinctrl";<br/> reg = <0x10000400 0x1000>;<br/><br/> led_pins: ledgrp {<br/> fsl,pins = <MX6UL_PAD_GPIO1_IO18__GPIO1_IO18 0x10b0>;<br/> };<br/>};4.3 Custom Temperature Sensor and Driver
temperature_sensor: sensor@1234 {<br/> compatible = "mycompany,temperature-sensor";<br/> reg = <0x1234 0x100>;<br/> sensor_type = "temperature";<br/> sampling_rate = <100>;<br/>}; #include <linux/module.h><br/>#include <linux/platform_device.h><br/>#include <linux/of.h><br/><br/>static int my_sensor_probe(struct platform_device *pdev) {<br/> struct device_node *np = pdev->dev.of_node;<br/> u32 reg_val, sampling_rate;<br/> const char *sensor_type;<br/><br/> if (!np) return -EINVAL;<br/><br/> if (of_property_read_u32(np, "reg", ®_val)) return -EINVAL;<br/> if (of_property_read_string(np, "sensor_type", &sensor_type)) return -EINVAL;<br/> if (of_property_read_u32(np, "sampling_rate", &sampling_rate)) return -EINVAL;<br/><br/> dev_info(&pdev->dev, "Sensor initialized: reg=0x%x, type=%s, rate=%d
", reg_val, sensor_type, sampling_rate);<br/> return 0;<br/>}<br/><br/>static int my_sensor_remove(struct platform_device *pdev) { return 0; }<br/><br/>static const struct of_device_id my_sensor_of_match[] = {<br/> { .compatible = "mycompany,temperature-sensor" },<br/> {}<br/>};<br/>MODULE_DEVICE_TABLE(of, my_sensor_of_match);<br/><br/>static struct platform_driver my_sensor_driver = {<br/> .probe = my_sensor_probe,<br/> .remove = my_sensor_remove,<br/> .driver = { .name = "mycompany-temperature-sensor-driver", .of_match_table = my_sensor_of_match }<br/>};<br/><br/>module_platform_driver(my_sensor_driver);<br/>MODULE_LICENSE("GPL");<br/>MODULE_AUTHOR("Your Name");<br/>MODULE_DESCRIPTION("Custom Temperature Sensor Driver");Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
