设备树语法:节点、属性、标签、引用、包含文件

各位同学,咱们今天来啃设备树语法这块硬骨头。说实话,我刚入行那会儿,看到设备树文件里一堆花括号、冒号、分号,头都大了。后来在项目里被坑过几次,才真正摸透了它的脾气。

设备树说白了就是一套描述硬件信息的「说明书」。它用树形结构告诉内核:你有哪些外设?挂在哪个总线上?中断怎么接?GPIO怎么配?嗯,咱们今天就把这套语法的核心要素捋清楚。

1. 节点(Node)—— 设备树的基本单元

每个设备或总线都是一个节点。节点用花括号 { } 包裹,里面可以嵌套子节点。我习惯把节点想象成「硬件模块的身份证」。

/ {
    soc {
        uart0: serial@10000000 {
            compatible = "ns16550";
            reg = <0x10000000 0x1000>;
            interrupts = <0 5 4>;
        };
    };
};

看到没?根节点 / 下面挂了个 socsoc 下面又挂了 serial@10000000。这个 @10000000 是节点的地址,用来区分同名设备。我在一个项目里遇到过两个串口都叫 serial,结果编译报错——就是因为忘了加地址后缀。

节点命名规则:

  • 节点名 + 可选的 @地址
  • 节点名不能以数字开头
  • 同一层级下节点名必须唯一

2. 属性(Property)—— 描述硬件特征

属性就是键值对,用来描述节点的各种参数。常见的属性类型有:

属性类型 示例 说明
字符串 compatible = "ns16550"; 驱动匹配的关键
32位整数 reg = <0x10000000 0x1000>; 地址和长度
布尔值 status = "disabled"; 控制设备启用/禁用
数组 interrupts = <0 5 4>; 中断描述

这里有个坑:compatible 属性是驱动和设备树之间的「接头暗号」。内核启动时,会遍历设备树,找到 compatible 匹配的驱动来初始化硬件。我曾经把 "ns16550" 写成了 "ns16550a",结果驱动死活不认——就少了个字母,查了一下午。

3. 标签(Label)—— 给节点起个外号

标签就是节点的别名。你看上面代码里的 uart0:,这就是标签。有了标签,其他地方引用这个节点就方便多了。

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

我个人习惯给所有重要外设都加上标签,比如 i2c0spi1gpio2。这样在板级文件中修改配置时,不用去翻原始 .dtsi 文件里的完整路径。

小技巧:标签名建议用「外设类型+序号」的格式,比如 uart0mmc1。别用 my_serial 这种名字,三个月后你自己都看不懂。

4. 引用(Reference)—— 把节点串起来

引用用 & 符号表示。它的作用是在一个地方定义节点,在另一个地方补充或修改它。这在实际项目中太常用了。

// 在 soc.dtsi 中定义
uart0: serial@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x1000>;
    status = "disabled";
};

// 在 board.dts 中引用并修改
&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

你想想看,如果所有板子都共用同一个 soc.dtsi,但不同板子的串口配置不一样。用引用机制,我们只需要在板级文件里覆盖 statuspinctrl 就行了。嗯,这就是设备树设计的精妙之处。

注意:引用时不能修改节点的 compatiblereg 属性。这些是硬件的「基因」,必须在原始定义中确定。我曾经试图在引用里改 reg 地址,结果内核直接崩溃——因为它找不到对应的硬件。

5. 包含文件(Include)—— 模块化管理

设备树支持 #include 预处理指令,跟 C 语言一样。通常我们会把 SoC 公共部分放在 .dtsi 文件中,板级特有部分放在 .dts 文件中。

// imx6ull.dtsi
#include "imx6ull-pinfunc.h"
/ {
    soc {
        uart1: serial@02020000 {
            compatible = "fsl,imx6ull-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
        };
    };
};

// myboard.dts
#include "imx6ull.dtsi"

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

我在项目中见过有人把所有东西都塞进一个 .dts 文件,结果文件超过 5000 行,维护起来简直噩梦。正确的做法是:SoC 厂商提供的 .dtsi 不要动,板级文件只写差异部分。

文件组织建议:

  • .dtsi:SoC 公共定义、外设控制器、中断控制器
  • .dts:板级配置、GPIO 引脚复用、设备使能
  • .h:宏定义、引脚编号、中断号

知识体系总览

下面这张图帮你理清今天讲的内容。我画图时习惯把节点比作树干,属性是树叶,标签是树上的名牌,引用是树枝之间的连接线。

根节点 / soc uart0: serial@10000000 (标签 uart0) 属性示例 compatible = "ns16550"; reg = <0x10000000 0x1000>; 引用示例 &uart0 { status = "okay"; 包含文件机制 imx6ull.dtsi ← SoC公共定义(节点、标签、属性) myboard.dts ← 板级配置(引用、修改、使能) #include "imx6ull.dtsi" → 编译时合并 节点 带标签节点 属性/引用

这张图把今天讲的五个要素串起来了。根节点下面挂 soc,soc 下面挂 uart,uart 节点有标签 uart0,节点内部有属性描述硬件信息,其他地方可以通过 &uart0 引用它,而整个文件通过 #include 实现模块化管理。

最后说个实战经验:调试设备树时,我最常用的命令是 dtc -I dtb -O dts 反编译,看看实际生效的设备树长什么样。有时候你写的 .dts 和编译后的 .dtb 可能不一样——嗯,预处理、宏展开、覆盖合并,中间环节多了去了。

好了,设备树语法就这些。记住:节点是骨架,属性是血肉,标签是名字,引用是纽带,包含文件是组织方式。把这五样玩熟了,设备树对你来说就是小菜一碟。

课后练习:找一个实际开发板的 .dts 文件,试着找出里面所有的节点、标签、引用和包含文件。然后自己动手写一个简单的设备树片段,描述一个 GPIO 按键和一个 LED 灯。

专注资料整理