2. 设备树基础:设备树(DTS)语法、节点、属性、标签、包含文件

设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来定义CPU、内存、外设、中断控制器、时钟等硬件组件及其连接关系。在嵌入式Linux系统中,设备树源文件(.dts)经过编译生成设备树二进制文件(.dtb),由Bootloader传递给内核,内核解析后完成设备驱动与硬件资源的绑定。

2.1 设备树源文件结构

一个典型的设备树源文件由以下部分组成:

  • 根节点:使用斜杠 / 表示,是整个设备树的起点。
  • 节点:描述一个硬件设备或总线,格式为 node-name@unit-address。
  • 属性:键值对,描述节点的特性,如地址、中断号、时钟频率等。
  • 标签:为节点或属性起别名,便于在其它位置引用。
  • 包含文件:通过 #include 引入公共头文件或其它设备树片段。
关键概念:设备树的核心思想是将硬件描述与内核代码分离,同一份内核镜像可以通过更换设备树文件来支持不同的硬件平台。

2.2 DTS语法详解

设备树源文件使用一种类C语言的语法,但更简洁。以下是一个完整的设备树示例:

/dts-v1/;

#include "soc-common.dtsi"

/ {
    model = "Embedded Development Board v2.0";
    compatible = "vendor,devboard-v2", "vendor,devboard";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* 1GB RAM */
    };

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

        cpu@0 {
            compatible = "arm,cortex-a72";
            reg = <0x0>;
            clock-frequency = <2000000000>;
        };

        cpu@1 {
            compatible = "arm,cortex-a72";
            reg = <0x1>;
            clock-frequency = <2000000000>;
        };
    };

    uart0: serial@1c090000 {
        compatible = "vendor,uart";
        reg = <0x1c090000 0x1000>;
        interrupts = <0 33 4>;
        clocks = <&uart_clk>;
        status = "okay";
    };
};

2.3 节点定义与命名规则

节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线或功能块。节点名称由两部分组成:

组成部分 说明 示例
节点名 描述设备功能的通用名称,应尽量使用标准名称 serial, i2c, gpio
单元地址 设备的基地址或片选号,用于区分同名节点 1c090000, 0
完整格式 节点名@单元地址 serial@1c090000
最佳实践:节点名应使用小写字母,单词间用连字符分隔。单元地址使用十六进制小写,不带 0x 前缀。

2.4 属性类型与编码

属性是节点中描述硬件特性的键值对。设备树规范定义了多种属性类型:

属性类型 语法格式 示例 说明
字符串 "string" compatible = "vendor,uart"; 用于标识设备兼容性
32位整数 <value> reg = <0x1c090000 0x1000>; 地址、大小、中断号等
64位整数 <high low> reg = <0x0 0x80000000 0x0 0x40000000>; 64位地址空间
字节数组 [hex bytes] local-mac-address = [00 11 22 33 44 55]; MAC地址、UUID等
布尔值 仅属性名,无值 dma-coherent; 表示属性为真
字符串列表 "str1", "str2" compatible = "vendor,devboard-v2", "vendor,devboard"; 按优先级排列的兼容列表
phandle &label clocks = <&uart_clk>; 引用其它节点

2.5 标签(Label)与引用

标签为节点提供一个别名,可以在设备树其它位置通过 &label 引用该节点。标签通常用于:

  • 中断连接:引用中断控制器节点
  • 时钟引用:引用时钟源节点
  • GPIO引用:引用GPIO控制器节点
  • 覆盖(Overlay):在设备树覆盖文件中定位目标节点
/* 定义标签 */
uart0: serial@1c090000 {
    compatible = "vendor,uart";
    reg = <0x1c090000 0x1000>;
    interrupts = <0 33 4>;
    interrupt-parent = <&gic>;
    clocks = <&uart_clk>;
};

/* 在其它位置引用标签 */
&uart0 {
    status = "okay";
    clock-frequency = <1843200>;
};
注意:标签名不能与节点名冲突,且在整个设备树中必须唯一。标签通常使用小写字母加下划线命名。

2.6 包含文件(#include)

设备树支持使用C预处理器指令 #include 来包含其它文件,实现代码复用。常见的包含文件类型:

文件类型 扩展名 用途
设备树包含文件 .dtsi SoC公共定义、外设模板
头文件 .h 宏定义、中断号、时钟ID等常量
设备树覆盖 .dtso 运行时硬件扩展
/* soc-common.dtsi - SoC公共定义 */
/ {
    gic: interrupt-controller@1c010000 {
        compatible = "arm,gic-400";
        reg = <0x1c010000 0x1000>,
              <0x1c020000 0x2000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    uart_clk: clock@1c030000 {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <24000000>;
    };
};

2.7 设备树节点关系图

下图展示了一个典型嵌入式系统的设备树节点层次结构:

设备树节点层次结构示例 / (根节点) chosen memory@80000000 cpus cpu@0 cpu@1 soc@0 (外设总线) uart0: serial@1c090000 i2c0: i2c@1c0a0000 gpio0: gpio@1c0b0000 rtc@68 eeprom@50 图例说明 绿色:系统节点 | 紫色:CPU容器 | 橙色:CPU核心 | 红色:外设总线 | 青色:外设设备

2.8 标准属性详解

设备树规范定义了一系列标准属性,用于描述硬件资源的通用特性:

属性名 适用节点 说明
compatible 所有设备节点 设备驱动匹配字符串,格式为 "厂商,设备型号"
reg 有地址空间的设备 描述设备地址范围,格式为 <基地址 长度>
interrupts 产生中断的设备 中断号、触发类型等,格式由中断控制器定义
interrupt-parent 所有设备节点 指定中断控制器节点的phandle
clocks 需要时钟的设备 引用时钟源节点的phandle列表
status 所有设备节点 设备状态:okay, disabled, reserved
#address-cells 总线节点 子节点地址字段占用的32位字数量
#size-cells 总线节点 子节点大小字段占用的32位字数量
device_type memory、cpu等 设备类型标识,主要用于兼容IEEE 1275
model 根节点 板级型号名称

2.9 地址编码与ranges属性

在总线节点中,#address-cells 和 #size-cells 定义了子节点地址空间的编码方式。ranges 属性用于将子节点的地址空间映射到父节点的地址空间:

/ {
    #address-cells = <2>;   /* 64位地址 */
    #size-cells = <2>;      /* 64位大小 */

    soc@0 {
        compatible = "simple-bus";
        #address-cells = <1>;   /* 32位地址 */
        #size-cells = <1>;      /* 32位大小 */
        ranges = <0x0 0x0 0x1c000000 0x1000000>;  /* 子地址0x0 -> 父地址0x1c000000 */

        uart0: serial@1c090000 {
            reg = <0x1c090000 0x1000>;  /* 子地址空间中的地址 */
        };
    };
};
地址映射规则:ranges = <子地址 父地址 长度>。当 ranges 属性为空时,表示子地址与父地址一一对应;当 ranges 属性不存在时,子节点不能直接使用父地址空间。

2.10 设备树编译与反编译

设备树源文件需要通过设备树编译器(dtc)转换为二进制格式:

操作 命令 说明
编译 dtc -I dts -O dtb -o board.dtb board.dts 将DTS编译为DTB
反编译 dtc -I dtb -O dts -o board.dts board.dtb 将DTB反编译为DTS
检查 dtc -I dtb -O dts board.dtb | head -50 快速查看DTB内容
生成符号 dtc -I dts -O dtb -@ -o board.dtb board.dts 生成符号节点,支持覆盖

2.11 设备树覆盖(Overlay)

设备树覆盖允许在不修改原始设备树的情况下,动态添加或修改硬件描述。覆盖文件使用 .dtso 扩展名,通过 &label 语法定位目标节点:

/* overlay-example.dtso */
/dts-v1/;
/plugin/;

&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
};

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

    temperature-sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};
覆盖应用场景:设备树覆盖常用于FPGA动态加载、扩展板支持、调试时临时修改硬件配置等场景,无需重新编译整个设备树。

2.12 常见错误与调试技巧

  • 语法错误:缺少分号、括号不匹配、属性值格式错误。使用 dtc -I dts -O dtb 编译时检查。
  • 地址冲突:多个设备使用相同的地址范围。检查 reg 属性是否重叠。
  • 中断号错误:中断号与中断控制器不匹配。确认 #interrupt-cells 定义。
  • 标签未定义:引用了不存在的标签。检查标签名拼写和包含文件。
  • 兼容字符串不匹配:驱动中的 of_match_table 与设备树中的 compatible 不一致。
调试建议:在Linux内核启动时添加 earlycon 和 dtdebug 参数,可以打印设备树解析过程中的详细信息,帮助定位硬件描述问题。

2.13 本章小结

设备树是嵌入式Linux系统中硬件描述的核心机制,掌握DTS语法、节点定义、属性编码、标签引用和包含文件管理是进行设备驱动开发的基础。通过本章学习,你应该能够:

  • 编写符合规范的设备树源文件
  • 理解节点层次结构和地址映射关系
  • 使用标签实现节点引用和覆盖
  • 通过包含文件实现代码复用
  • 使用dtc工具进行编译和调试