Device Tree
Introduction
Device Tree is a data structure and language for describing hardware in embedded systems, particularly in Linux-based platforms. Instead of hardcoding hardware information directly into the kernel or drivers, Device Tree provides a standardized way to describe the hardware components, their relationships, and configuration parameters. This approach makes the kernel more portable and allows the same kernel binary to work with different hardware configurations.
What is Device Tree?
Device Tree serves as a hardware description language that tells the operating system about the hardware components that cannot be automatically discovered (non-discoverable hardware). Think of it as a “map” that describes:
- Hardware components: CPUs, memory, peripherals, buses
- Hardware relationships: How components are connected
- Hardware properties: Addresses, interrupts, clocks, pins
- Configuration parameters: Operating modes, capabilities, constraints
Key Benefits:
- Hardware abstraction: Separates hardware description from kernel code
- Portability: Same kernel works with different hardware configurations
- Maintainability: Hardware changes don’t require kernel recompilation
- Standardization: Common format across different architectures
Device Tree Structure
Device Tree uses a hierarchical tree structure similar to a filesystem, with nodes representing hardware components and properties describing their characteristics.
Basic Syntax
/dts-v1/;
/ {
model = "Example Board";
compatible = "vendor,example-board";
#address-cells = <1>;
#size-cells = <1>;
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x20000000>; // 512MB at 0x40000000
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <0>;
clock-frequency = <800000000>; // 800MHz
};
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@44e09000 {
compatible = "ti,am335x-uart", "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "okay";
};
i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
#address-cells = <1>;
#size-cells = <0>;
eeprom@50 {
compatible = "at,24c256";
reg = <0x50>;
};
};
};
};
Key Concepts
Nodes and Properties
Nodes represent hardware components:
uart0: serial@44e09000 {
// Properties go here
};
Properties describe node characteristics:
compatible = "ti,am335x-uart", "ti,omap3-uart"; // String list
reg = <0x44e09000 0x2000>; // Address/size
interrupts = <72>; // Interrupt number
status = "okay"; // Status string
clock-frequency = <800000000>; // Integer value
Addressing
#address-cells: Number of cells needed for addresses #size-cells: Number of cells needed for sizes reg: Register address and size
soc {
#address-cells = <1>; // Address needs 1 cell (32-bit)
#size-cells = <1>; // Size needs 1 cell (32-bit)
uart0: serial@44e09000 {
reg = <0x44e09000 0x2000>; // Address: 0x44e09000, Size: 0x2000
};
};
References and Phandles
Phandles allow nodes to reference other nodes:
/ {
clocks {
osc: oscillator {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <24000000>;
};
};
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
clocks = <&osc>; // Reference to oscillator
clock-names = "fclk";
};
};
Common Properties
Standard Properties
node-name {
compatible = "vendor,device-model"; // Device identification
reg = <address size>; // Register address/size
interrupts = <irq-number>; // Interrupt configuration
status = "okay" | "disabled"; // Enable/disable status
// Clock properties
clocks = <&clock-phandle>;
clock-names = "functional-clock";
// GPIO properties
gpios = <&gpio-controller pin flags>;
gpio-names = "reset-gpio";
// Power management
power-domains = <&power-controller>;
// DMA properties
dmas = <&dma-controller channel>;
dma-names = "tx", "rx";
};
Bus-Specific Properties
I2C devices:
i2c@address {
#address-cells = <1>;
#size-cells = <0>;
sensor@1a {
compatible = "st,lis3dh-accel";
reg = <0x1a>; // I2C slave address
};
};
SPI devices:
spi@address {
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>; // Chip select
spi-max-frequency = <25000000>;
};
};
Compilation and Tools
Device Tree Compiler (dtc)
Compile .dts to .dtb:
1
2
3
4
5
6
7
8
9
# Using kernel build system
make ARCH=arm dtbs # Build all DTBs
make ARCH=arm my-board.dtb # Build specific DTB
# Using dtc directly
dtc -I dts -O dtb -o output.dtb input.dts
# Decompile .dtb back to .dts
dtc -I dtb -O dts -o output.dts input.dtb
Include and preprocessing:
/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include "am335x.dtsi" // Include base SoC definition
/ {
model = "Custom AM335x Board";
gpio-leds {
compatible = "gpio-leds";
led0 {
label = "status";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
};
Practical Examples
Example 1: GPIO LED Configuration
/ {
gpio-leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&led_pins>;
led-heartbeat {
label = "heartbeat";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
led-status {
label = "status";
gpios = <&gpio1 22 GPIO_ACTIVE_HIGH>;
default-state = "on";
};
};
gpio-keys {
compatible = "gpio-keys";
button-user {
label = "User Button";
gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
linux,code = <KEY_ENTER>;
debounce-interval = <50>;
};
};
};
Example 2: I2C Sensor Configuration
&i2c1 {
status = "okay";
clock-frequency = <400000>; // 400kHz
/* Temperature sensor */
tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio0>;
interrupts = <7 IRQ_TYPE_LEVEL_LOW>;
};
/* Accelerometer */
lis3dh@19 {
compatible = "st,lis3dh-accel";
reg = <0x19>;
interrupt-parent = <&gpio1>;
interrupts = <6 IRQ_TYPE_EDGE_RISING>;
st,drdy-int-pin = <1>;
st,click-single-x;
st,click-single-y;
st,click-single-z;
};
/* EEPROM */
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
pagesize = <64>;
};
};
Example 3: SPI Device Configuration
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <1000000>;
};
flash@1 {
compatible = "jedec,spi-nor";
reg = <1>;
spi-max-frequency = <25000000>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
bootloader@0 {
label = "bootloader";
reg = <0x0 0x40000>;
read-only;
};
kernel@40000 {
label = "kernel";
reg = <0x40000 0x300000>;
};
rootfs@340000 {
label = "rootfs";
reg = <0x340000 0xcc0000>;
};
};
};
};
Device Tree Overlays
Overlays allow runtime modification of the device tree without rebuilding the entire DTB. Commonly used in systems like Raspberry Pi.
Overlay Syntax
/dts-v1/;
/plugin/;
/ {
compatible = "ti,am335x-bone-black";
/* Fragment to enable I2C2 */
fragment@0 {
target = <&i2c2>;
__overlay__ {
status = "okay";
clock-frequency = <100000>;
tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
};
/* Fragment to add GPIO LEDs */
fragment@1 {
target-path = "/";
__overlay__ {
my-leds {
compatible = "gpio-leds";
led@0 {
label = "overlay-led";
gpios = <&gpio1 16 0>;
default-state = "off";
};
};
};
};
};
Apply overlay at runtime:
1
2
3
4
5
# Load overlay
echo "my-overlay.dtbo" > /sys/kernel/config/device-tree/overlays/my-overlay/path
# Unload overlay
rmdir /sys/kernel/config/device-tree/overlays/my-overlay/
Debugging Device Tree
Verification and Analysis Tools
Check compiled device tree:
1
2
3
4
5
6
7
8
9
# View current device tree in kernel
ls -la /sys/firmware/devicetree/base/
cat /sys/firmware/devicetree/base/model
# Compare device trees
scripts/dtc/dtx_diff arch/arm/boot/dts/am335x-boneblack.dts /proc/device-tree
# Validate device tree
dtc -I dts -O dtb -o /dev/null -W no-unit_address_vs_reg my-board.dts
Debug kernel boot messages:
1
2
3
4
5
# Boot with device tree debugging
dmesg | grep -i "device tree\|of:"
# Check for driver binding issues
dmesg | grep -i "bound\|match\|probe"
Common Issues and Solutions
Issue 1: Device not detected
// Problem: Missing or incorrect compatible string
sensor@48 {
compatible = "wrong,sensor-name"; // Wrong!
reg = <0x48>;
};
// Solution: Use correct compatible string
sensor@48 {
compatible = "ti,tmp102"; // Correct!
reg = <0x48>;
};
Issue 2: Address/size cell mismatch
// Problem: Inconsistent addressing
parent {
#address-cells = <2>; // Parent expects 2 cells
#size-cells = <1>;
child@100 {
reg = <0x100 0x10>; // Only 1 address cell! Wrong!
};
};
// Solution: Match parent's addressing scheme
parent {
#address-cells = <2>;
#size-cells = <1>;
child@100 {
reg = <0x0 0x100 0x10>; // 2 address cells, 1 size cell
};
};
Best Practices
Organization and Maintainability
1. Use includes and modularity:
// base-soc.dtsi - SoC-level definitions
// board-common.dtsi - Board family common features
// specific-board.dts - Board-specific configuration
#include "am335x.dtsi" // SoC base
#include "am335x-bone-common.dtsi" // Board family
2. Consistent naming:
// Good naming convention
uart0: serial@44e09000 { }; // Label: uart0, node: serial@address
i2c0: i2c@44e0b000 { }; // Label: i2c0, node: i2c@address
gpio_keys: gpio-keys { }; // Label: gpio_keys, node: gpio-keys
3. Status management:
// Disable by default in SoC file
uart1: serial@44e09100 {
compatible = "ti,am335x-uart";
reg = <0x44e09100 0x2000>;
status = "disabled"; // Default disabled
};
// Enable in board file
&uart1 {
status = "okay"; // Enable for this board
pinctrl-names = "default";
pinctrl-0 = <&uart1_pins>;
};
Integration with Linux
Kernel Driver Integration
Driver matching process:
- Kernel reads device tree at boot
- Creates platform devices from DT nodes
- Matches drivers using compatible strings
- Calls driver probe function with device tree data
Example driver integration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In driver code
static const struct of_device_id my_sensor_dt_ids[] = {
{ .compatible = "vendor,my-sensor-v1" },
{ .compatible = "vendor,my-sensor-v2" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_dt_ids);
static struct platform_driver my_sensor_driver = {
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.driver = {
.name = "my-sensor",
.of_match_table = my_sensor_dt_ids,
},
};
Reading device tree properties in driver:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int my_sensor_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
u32 frequency;
const char *name;
// Read integer property
if (of_property_read_u32(np, "clock-frequency", &frequency))
frequency = 1000000; // Default value
// Read string property
if (of_property_read_string(np, "sensor-name", &name))
name = "default-sensor";
// Check boolean property
bool is_active = of_property_read_bool(np, "active-low");
return 0;
}
Conclusion
Device Tree is a powerful and essential technology for describing hardware in modern embedded Linux systems. Key takeaways:
- Separates hardware description from kernel code, improving portability and maintainability
- Uses hierarchical tree structure with nodes representing hardware components and properties describing their characteristics
- Provides standardized syntax for describing addresses, interrupts, clocks, GPIOs, and other hardware resources
- Supports overlays for runtime hardware configuration changes
- Integrates seamlessly with Linux kernel driver model through compatible string matching
Best practices for success:
- Start with existing examples for similar hardware
- Use modular organization with includes
- Follow consistent naming conventions
- Validate syntax with dtc compiler
- Test thoroughly with actual hardware
Device Tree knowledge is essential for embedded Linux developers working with ARM, RISC-V, and other architectures. Understanding DT enables you to effectively describe hardware, debug hardware-related issues, and create portable, maintainable embedded systems.