3、设备树基础语法:节点、属性、值、标签、引用、路径、别名等基本元素

设备树(Device Tree)是一种描述硬件资源的数据结构,它通过节点(node)和属性(property)的层级组合,将板级硬件信息从内核代码中剥离出来。理解设备树的基本语法,是编写、修改和调试设备树源文件(.dts)的前提。本章将围绕节点、属性、值、标签、引用、路径和别名这七个核心元素展开,建立对设备树“数据描述语言”的完整认知。

根节点 "/" cpus 节点 memory 节点 soc 节点 cpu@0 cpu@1 memory@40000000 uart@1a000000 i2c@1a02000 属性示例: compatible = "arm,cortex-a9"; // 字符串 reg = <0x1a000000 0x1000>; // 数值 interrupts = <0 27 4>; // 数组

设备树的核心思想是“用数据描述硬件”,而非“用代码初始化硬件”。在Linux内核中,设备树源文件(.dts)经过设备树编译器(dtc)编译为二进制设备树块(.dtb),内核在启动时解析该二进制文件,从而获知系统中有哪些设备、设备位于何处、如何配置驱动。这种机制使得同一份内核镜像可以支持多种硬件平台,只需更换对应的.dtb文件即可。

3.1 节点:设备树的骨架

节点是设备树的基本组成单元,用于表示一个硬件设备、总线、控制器或简单的逻辑分组。节点采用树形层级结构,从根节点“/”开始,向下延伸出子节点。每个节点可以包含属性(描述自身特征)和子节点(描述其下的从属设备)。

核心原理:节点的命名与层级

节点名称由“节点名”和“单元地址”两部分组成,格式为node-name@unit-address。其中,node-name用于标识节点类型,通常使用通用名称(如uart、i2c、ethernet);unit-address用于区分同一类型的不同实例,通常对应设备在总线上的寄存器地址或片选编号。例如:

/ {
    soc {
        uart@1a000000 {
            compatible = "ns16550a";
            reg = <0x1a000000 0x1000>;
        };
        uart@1a001000 {
            compatible = "ns16550a";
            reg = <0x1a001000 0x1000>;
        };
    };
};

上述代码中,uart@1a000000和uart@1a001000是两个不同的串口节点,通过单元地址区分。节点名的选择应遵循“设备类型优先”原则,避免使用具体芯片型号作为节点名(如uart@...优于ns16550@...),因为节点名是给内核和调试工具识别的逻辑标识,而具体芯片型号由compatible属性描述。

判断方法:节点是否合法

判断一个节点是否合法,可以从以下三个维度检查:

  • 命名规范:节点名只能包含小写字母、数字、逗号和连字符,且必须以字母开头;单元地址为十六进制数字,可省略。
  • 层级关系:每个节点必须有且仅有一个父节点(根节点除外);子节点之间不能重名(包括单元地址)。
  • 标签唯一性:如果节点定义了标签(label),标签名在整个设备树中必须唯一。

案例拆解:CPU节点的组织方式

以双核ARM处理器为例,设备树中通常将CPU核组织在cpus节点下:

/ {
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };
};

这里#address-cells和#size-cells是总线节点的重要属性,它们决定了子节点中reg属性的地址和长度字段的位数(详见3.3节)。device_type属性是CPU节点的特殊标识,用于兼容旧版内核的启动代码。

经验提醒:在编写设备树时,建议先画出硬件拓扑图,再按“总线→控制器→外设”的层级关系组织节点。不要将功能无关的设备强行挂载到同一父节点下,否则会导致驱动匹配逻辑混乱。

3.2 属性与值:描述设备特征

属性是节点的“键值对”数据,用于描述设备的硬件特征。属性名是字符串,属性值可以是空、字符串、数值、数组或布尔值。设备树规范(ePAPR)定义了一系列标准属性,同时也允许自定义属性。

核心原理:属性值的编码规则

属性值在设备树源文件中的表示方式如下:

值类型 语法示例 说明
字符串 compatible = "arm,cortex-a9"; 用双引号包裹,可包含多个字符串(以空格分隔)
32位整数 reg = <0x1a000000 0x1000>; 用尖括号包裹,多个数值以空格分隔
64位整数 reg = <0x0 0x40000000 0x0 0x1000>; 由两个32位整数组合表示
字节数组 local-mac-address = [00 11 22 33 44 55]; 用方括号包裹,每字节两位十六进制数
布尔值 status = "disabled"; 通常用字符串"okay"或"disabled"表示

其中,compatible属性是最关键的匹配属性。内核通过它查找设备对应的驱动,其值是一个字符串列表,按优先级从高到低排列。例如:

compatible = "fsl,imx6ull-uart", "ns16550a";

内核首先尝试匹配fsl,imx6ull-uart,如果失败则尝试ns16550a。这种机制允许同一驱动兼容多个硬件版本。

操作流程:如何定义自定义属性

自定义属性用于传递板级特有信息(如GPIO编号、时钟频率、电源域标识)。定义时需注意:

  1. 属性名建议使用“厂商前缀,属性名”格式(如fsl,pinmux),避免与标准属性冲突。
  2. 属性值类型需与驱动解析代码保持一致,否则会导致数据错位。
  3. 在驱动中通过of_property_read_u32()、of_property_read_string()等API读取。

常见误区:属性值类型混淆

初学者常将字符串和整数混淆。例如,将clock-frequency = <24000000>;写成clock-frequency = "24000000";,这会导致驱动读取到的是字符串指针而非整数,引发时钟配置错误。另一个常见错误是忘记#address-cells和#size-cells的设置,导致reg属性解析异常。

风险提示:修改设备树属性时,务必同时检查对应驱动代码的解析逻辑。设备树只是“数据契约”,驱动才是“数据消费者”。两者不一致时,系统可能静默失败或产生不可预测行为。

3.3 标签、引用与路径:定位节点的三种方式

在设备树中,经常需要从一个节点引用另一个节点(例如中断控制器、时钟源、GPIO控制器)。设备树提供了三种定位节点的方式:标签(label)、路径(path)和别名(alias)。

核心原理:三种定位机制对比

定位方式 语法 特点 典型用途
标签引用 &label_name 编译期解析,效率高;标签名需唯一 在节点中引用中断控制器、时钟等
路径引用 &/path/to/node 直接使用路径,无需预先定义标签 在头文件(.dtsi)中覆盖或修改节点
别名引用 &alias_name 通过/aliases节点定义,语义化强 在bootargs或驱动中引用固定设备

标签的定义方式是在节点名前加上“标签名:”前缀:

/ {
    soc {
        uart0: uart@1a000000 {
            compatible = "ns16550a";
            reg = <0x1a000000 0x1000>;
        };
    };
};

之后在其它节点中可以通过&uart0引用该节点。引用操作通常用于添加或修改属性:

&uart0 {
    status = "okay";
    clock-frequency = <24000000>;
};

这段代码在编译时会被展开为对uart@1a000000节点的属性追加。

操作流程:使用路径引用修改节点

当设备树源文件被拆分为多个.dtsi头文件时,路径引用非常有用。例如,在imx6ull.dtsi中定义了uart1节点,在板级.dts文件中可以这样覆盖:

&/soc/uart@1a000000 {
    pinctrl-0 = &pinctrl_uart1;
};

路径引用要求开发者准确掌握节点在树中的位置,一旦父节点改名或层级调整,路径引用就会失效。因此,在大型项目中优先使用标签引用。

案例拆解:别名在串口调试中的应用

在Linux内核启动参数中,经常需要指定控制台串口,例如console=ttymxc0。这里的ttymxc0对应设备树中的/aliases节点:

/ {
    aliases {
        serial0 = &uart1;
        serial1 = &uart2;
    };
};

内核通过serial0别名找到uart1节点,再根据其compatible属性匹配驱动。使用别名的好处是:即使硬件上更换了串口引脚,只需修改aliases节点,无需改动内核命令行参数。

实战经验:在编写设备树时,建议为每个关键外设(串口、网口、显示接口)定义别名。这不仅能简化内核参数配置,还能让用户空间的udev规则更稳定地识别设备。

3.4 综合案例:从零构建一个最小设备树

为了将上述语法元素串联起来,我们构建一个包含内存、串口和GPIO控制器的简化设备树。假设硬件平台为:单核ARM处理器,256MB内存起始地址0x40000000,一个NS16550串口位于0x1a000000,一个GPIO控制器位于0x1b000000。

/dts-v1/;

/ {
    model = "Example Board";
    compatible = "example,board-v1";

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

    aliases {
        serial0 = &uart0;
    };

    memory@40000000 {
        device_type = "memory";
        reg = <0x40000000 0x10000000>;  // 256MB
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;  // 子节点地址与父节点地址空间相同

        uart0: uart@1a000000 {
            compatible = "ns16550a";
            reg = <0x1a000000 0x1000>;
            interrupts = <0 27 4>;  // SPI中断,编号27,高电平触发
            clock-frequency = <24000000>;
            status = "okay";
        };

        gpio0: gpio@1b000000 {
            compatible = "example,gpio";
            reg = <0x1b000000 0x1000>;
            #gpio-cells = <2>;
            gpio-controller;
        };
    };
};

该设备树展示了以下关键点:

  • 根节点:包含model和compatible属性,用于识别板卡型号。
  • 地址单元:根节点和soc节点均声明#address-cells = <1>和#size-cells = <1>,表示子节点reg属性中地址和长度各占一个32位整数。
  • ranges属性:空ranges表示子节点地址与父节点地址空间直接映射,无需地址转换。
  • 中断描述:interrupts = <0 27 4>中的第一个字段0表示SPI中断类型,27为中断号,4为触发方式(高电平触发)。
  • GPIO控制器:gpio-controller属性标记该节点为GPIO控制器,#gpio-cells = <2>表示引用该控制器时需要两个参数(GPIO编号和标志)。

编译与验证流程

使用设备树编译器检查语法正确性:

dtc -I dts -O dtb -o example.dtb example.dts
dtc -I dtb -O dts -o example_verify.dts example.dtb  # 反编译验证

反编译后的example_verify.dts应与源文件内容一致(除注释和格式外)。如果编译报错,通常是因为括号不匹配、属性值类型错误或标签重复。

常见误区与复盘

  • 误区1:忘记声明#address-cells和#size-cells。这会导致reg属性被错误解析,驱动读取到的地址和长度完全错误。
  • 误区2:在节点中重复定义同名属性。设备树规范不允许同一节点内出现两个同名属性,编译器会报错。
  • 误区3:引用未定义的标签。使用&uart0前必须确保uart0标签已定义,否则编译失败。
  • 误区4:忽略status属性。默认情况下,节点状态为"okay",但如果硬件上未焊接某个外设,应显式设置为"disabled",避免驱动加载失败。
总结复盘:设备树语法的核心是“节点组织硬件拓扑,属性描述硬件特征,标签/路径/别名提供定位手段”。在编写时,建议遵循以下顺序:先画硬件框图,再定义节点层级,然后填充属性,最后通过编译和反编译验证。熟练掌握这些基本元素后,才能进一步理解设备树覆盖(overlay)、引脚复用(pinctrl)和时钟框架等高级主题。