3. 设备树语法入门

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,它使用树形层次结构将处理器、外设、中断、时钟、GPIO 等信息传递给操作系统内核。对于 UART 调试驱动的移植适配而言,掌握设备树的语法是第一步。本章将系统讲解设备树的四个核心语法要素:节点(node)、属性(property)、标签(label)和包含文件(#include)。

学习提示:设备树语法与 C 语言结构体类似,但更强调层次关系和地址映射。建议读者在阅读本章时,对照实际开发板上的 .dts 文件进行练习,效果更佳。

3.1 节点(node)——设备树的骨架

节点是设备树的基本组成单元,每个节点代表一个硬件设备或总线。节点以花括号 { } 包裹,内部可以包含属性或子节点。节点名称通常采用 名称@地址 的格式,其中地址部分为可选项,用于区分同一名称下的不同实例。

/dts-v1/;

/ {
    model = "Embedded UART Debug Board";
    compatible = "vendor,board-v1";

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

        uart0: serial@10000000 {
            compatible = "vendor,uart-ip";
            reg = <0x10000000 0x1000>;
            interrupts = <33>;
            status = "okay";
        };
    };
};

上述代码中,/ 是根节点,soc 是总线节点,serial@10000000 是 UART 设备节点。节点名称 serial@10000000 中的 10000000 表示该 UART 外设的基地址。

节点命名规范:
  • 节点名称应使用小写字母、数字和连字符。
  • 地址部分使用十六进制,不带 0x 前缀。
  • 同一层级下节点名称必须唯一。
设备树节点层次结构图 根节点 / model / compatible soc 总线节点 #address-cells / #size-cells serial@10000000 (UART) compatible reg = <0x10000000 0x1000> interrupts = <33> status = "okay" 节点层次关系说明 根节点 → soc 总线节点 → UART 设备节点,每层可携带属性和子节点

3.2 属性(property)——设备的特征描述

属性是附着在节点上的键值对,用于描述设备的硬件特征。属性值可以是字符串、整数、字节数组、字符串列表或 <u32> 数组。常见的属性类型如下表所示:

属性类型 语法格式 示例 说明
字符串 "string" compatible = "vendor,uart-ip"; 用于匹配驱动
32 位整数 <value> interrupts = <33>; 中断号、时钟频率等
整数数组 <val1 val2 ...> reg = <0x10000000 0x1000>; 地址和长度对
字节数组 [hex bytes] local-mac-address = [00 11 22 33 44 55]; MAC 地址、UUID 等
字符串列表 "str1", "str2" compatible = "vendor,uart-ip", "simple-uart"; 多个兼容驱动
布尔值 仅属性名,无值 dma-coherent; 表示特性存在
常见错误:属性值中的逗号、分号、尖括号必须严格匹配。例如 reg = <0x10000000 0x1000> 末尾缺少分号会导致编译错误。另外,字符串必须使用双引号,整数数组使用尖括号。

在 UART 调试场景中,最关键的属性是 compatible、reg 和 interrupts。compatible 用于内核匹配驱动;reg 描述 UART 控制器的寄存器地址范围;interrupts 指定 UART 的中断号。

属性与节点关系图 UART 节点 serial@10000000 compatible = "vendor,uart-ip" reg = <0x10000000 0x1000> interrupts = <33> 属性作用说明 ● compatible:驱动匹配标识 ● reg:寄存器地址 + 长度 ● interrupts:中断号 ● status:设备状态 ● clock-frequency:时钟频率

3.3 标签(label)——节点的引用别名

标签是设备树中为节点定义的别名,用于在设备树文件的其他位置引用该节点。标签定义在节点名称之前,使用冒号 : 分隔。标签的主要用途包括:

  • 在设备树内部引用:通过 &label 语法引用节点,添加或覆盖属性。
  • 在 C 语言头文件中引用:通过 #define 宏定义,将标签转换为可用的宏。
  • 在 overlay 文件中使用:动态加载设备树片段时,通过标签定位目标节点。
// 定义标签
uart0: serial@10000000 {
    compatible = "vendor,uart-ip";
    reg = <0x10000000 0x1000>;
    interrupts = <33>;
    status = "okay";
};

// 在其他位置引用标签,添加属性
&uart0 {
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
    clock-frequency = <115200>;
};

标签的命名规则与 C 语言标识符类似:以字母或下划线开头,后跟字母、数字或下划线。建议使用有意义的名称,如 uart0、i2c1、spi2,便于维护。

标签 vs 节点名称:
  • 节点名称用于在设备树中定位节点路径,如 /soc/serial@10000000。
  • 标签用于在设备树源文件中快速引用节点,避免书写完整路径。
  • 一个节点可以有多个标签,用空格分隔,如 uart0: serial0: serial@10000000。
标签引用机制流程图 定义标签 uart0: serial@10000000 { compatible = "..."; }; 引用标签 &uart0 { pinctrl-0 = <&uart0_pins>; }; 编译后等效节点 serial@10000000 { compatible; reg; interrupts; pinctrl-0; pinctrl-names; }

3.4 包含文件(#include)——模块化与复用

设备树支持使用 C 语言预处理器指令 #include 来包含其他文件,实现代码复用和模块化设计。常见的包含文件类型包括:

文件类型 扩展名 用途 示例
设备树头文件 .dtsi SoC 公共定义、外设模板 #include "soc-common.dtsi"
C 语言头文件 .h 宏定义、GPIO/中断号常量 #include "uart-defines.h"
其他设备树 .dts 板级覆盖、overlay 片段 #include "base-board.dts"
// uart-defines.h
#define UART0_BASE      0x10000000
#define UART0_SIZE      0x1000
#define UART0_IRQ       33

// board.dts
#include "soc-common.dtsi"
#include "uart-defines.h"

&uart0 {
    reg = <UART0_BASE UART0_SIZE>;
    interrupts = <UART0_IRQ>;
    status = "okay";
};

使用 #include 时,设备树编译器(DTC)会先调用 C 预处理器展开所有宏和包含文件,然后再进行设备树语法解析。这意味着你可以在 .h 文件中定义 #define 宏,在 .dts 文件中直接使用这些宏作为属性值。

最佳实践:
  • 将 SoC 级公共定义放在 .dtsi 文件中,板级差异放在 .dts 文件中。
  • 使用 .h 头文件统一管理地址、中断号等常量,避免硬编码。
  • 包含文件路径使用双引号 "" 表示相对路径,使用尖括号 <> 表示系统搜索路径。
包含文件与设备树编译流程 board.dts 主设备树文件 soc-common.dtsi SoC 公共定义 uart-defines.h 宏定义头文件 C 预处理器 展开 #include 替换 #define 宏 DTC 编译器 生成 .dtb 二进制文件 #include 在预处理阶段展开,宏在编译前完成替换

3.5 综合示例:UART 调试节点的完整写法

将上述四个语法要素综合起来,一个完整的 UART 调试设备树节点通常包含以下内容:

// uart-debug.dtsi
#include "uart-defines.h"

/ {
    model = "UART Debug Board";
    compatible = "vendor,debug-board-v1";

    chosen {
        stdout-path = &uart0;
    };

    uart0: serial@10000000 {
        compatible = "vendor,uart-ip", "ns16550a";
        reg = <UART0_BASE UART0_SIZE>;
        interrupts = <UART0_IRQ>;
        interrupt-parent = <&gic>;
        clock-frequency = <1843200>;
        current-speed = <115200>;
        reg-shift = <2>;
        reg-io-width = <4>;
        fifo-size = <16>;
        dmas = <&dma0 0 &dma0 1>;
        dma-names = "tx", "rx";
        pinctrl-0 = <&uart0_default_pins>;
        pinctrl-names = "default";
        status = "okay";
    };
};
UART 调试节点关键属性说明:
属性 值示例 作用
compatible "vendor,uart-ip", "ns16550a" 驱动匹配,优先使用第一个
reg <0x10000000 0x1000> 寄存器基地址和映射长度
interrupts <33> UART 中断号
clock-frequency <1843200> UART 输入时钟频率
current-speed <115200> 默认波特率
reg-shift <2> 寄存器地址偏移量(32 位对齐)
fifo-size <16> 硬件 FIFO 深度

通过本章的学习,读者应掌握设备树中节点、属性、标签和包含文件的基本写法。这些语法要素是后续进行 UART 调试驱动移植适配的基础。在实际项目中,建议从简单的 .dts 文件开始,逐步添加节点和属性,并使用 dtc -I dts -O dtb 命令验证语法正确性。