2. 设备树基础语法:节点、属性、值的定义与格式,dtsi与dts文件的关系

大家好,欢迎来到设备树实战的第二讲。今天咱们聊聊设备树最基础的东西——节点、属性、值,还有那两个容易搞混的文件:.dtsi 和 .dts。

说实话,我刚接触设备树那会儿,看着满屏的花括号和分号,第一反应是「这玩意儿是C语言吗?」后来才发现,它其实更像一个树形结构的配置文件。嗯,咱们慢慢拆解。

2.1 节点(Node)——设备树的骨架

设备树里,每个硬件设备或总线都用一个「节点」表示。节点从根(/)开始,一层层往下挂,就像文件系统的目录结构。

节点的基本格式长这样:

/dts-v1/;

/ {
    node1 {
        child-node {
            // 子节点内容
        };
    };
    node2 {
        // 节点内容
    };
};

每个节点用花括号 { } 包裹,末尾必须加分号 ;。节点名通常用硬件名称或功能描述,比如 cpu、memory、i2c0。

节点命名规则:

  • 节点名可以包含字母、数字、连字符、下划线
  • 建议用全小写,单词间用连字符分隔(比如 serial-port)
  • 同一层级下节点名不能重复

我在项目中遇到过有人把节点名写成 I2C_0(大写+下划线),虽然语法没错,但跟社区风格不搭。你想想看,内核里成千上万个设备树文件,统一风格多重要。

2.2 属性(Property)与值(Value)——节点的血肉

节点光有名字不行,还得有属性来描述硬件特性。属性就是键值对,格式是:

属性名 = <值>;

属性值有几种常见类型,我列个表方便你对照:

值类型 格式 示例 说明
空值 无等号 interrupt-controller; 表示布尔属性,存在即为真
32位整数 <数字> reg = <0x10000000 0x1000>; 尖括号包裹,多个值用空格隔开
64位整数 <高32位 低32位> reg = <0x0 0x80000000 0x0 0x1000>; 两个32位拼一个64位
字符串 "字符串" compatible = "ti,am335x-gpio"; 双引号包裹
字节数组 [十六进制字节] local-mac-address = [00 11 22 33 44 55]; 方括号包裹,每字节两位十六进制
混合值 组合使用 foo = <0x1234>, "bar", [0xab]; 逗号分隔不同类型

我个人习惯把 compatible 属性放在每个设备节点的第一行,因为它是驱动匹配的关键。内核通过这个字符串找到对应的驱动程序。

避坑指南:我曾经在 reg 属性里少写了一个地址单元,导致驱动读到的内存地址完全错位。调试了整整一下午才发现——嗯,从那以后我每次写 reg 都会数一遍括号里的数字个数。

2.3 一个完整的节点示例

咱们把上面说的串起来,看一个实际的UART节点:

uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
    clock-frequency = <1843200>;
    status = "okay";
};

这里 uart0 是标签(label),方便其他地方引用。节点名 serial@10000000 中的 @10000000 是地址后缀,用于区分同类型设备。

属性说明:

  • compatible:告诉内核用哪个驱动
  • reg:基地址和地址范围
  • interrupts:中断号、触发类型
  • clock-frequency:时钟频率
  • status:设备状态,"okay" 表示启用

2.4 dtsi 与 dts 文件的关系

说到这儿,咱们得聊聊 .dtsi 和 .dts 这对「兄弟文件」。说白了,.dtsi 是头文件(包含文件),.dts 是主文件。

为什么要有两个文件?

你想想看,一个芯片厂商(比如TI、NXP)会出很多款SoC,这些SoC有很多共同点——相同的内核、相同的外设IP、相同的内存映射。如果每款芯片都写一份完整的 .dts,那得累死,还容易出错。

所以就有了 .dtsi:

  • .dtsi 存放公共部分:SoC的通用外设、中断控制器、时钟等
  • .dts 存放板级特有部分:具体型号的DDR大小、外设使能、引脚复用等

用 #include 指令把 .dtsi 包含进来:

// am335x-evm.dts
#include "am335x.dtsi"

/ {
    model = "TI AM335x EVM";
    compatible = "ti,am335x-evm", "ti,am335x";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB DDR
    };
};

&uart0 {
    status = "okay";
};

这里 &uart0 的写法叫「引用」,用来修改 .dtsi 中定义的节点。我在项目中经常用这个技巧——先在 .dtsi 里把所有外设的 status 设为 "disabled",然后在板级 .dts 里逐个打开需要的。

注意:.dtsi 文件可以多层嵌套。比如 am335x.dtsi 可能又 #include 了 skeleton.dtsi(内核提供的基础模板)。但别搞成循环包含,编译器会报错的。

2.5 知识体系结构图

下面这张图帮你理清本章的核心逻辑:

设备树基础语法知识体系 设备树文件 .dtsi(公共包含) .dts(板级特有) 节点(Node) 节点(Node) 属性(Property) 属性(Property) 值(Value) 值(Value) dtsi 提供骨架,dts 填充血肉,共同构成完整的设备树

这张图很直观:.dtsi 和 .dts 都包含节点、属性、值这三要素,但职责不同。一个管公共,一个管定制。

2.6 实战中的小建议

最后分享几个我自己的习惯:

  • 善用 #include:别把公共代码复制粘贴到每个 .dts 里,那是给自己挖坑
  • 标签命名要规范:比如 uart0、i2c1、mmc2,一看就知道是哪个外设
  • 属性值写完整:reg 的地址和长度别漏,interrupts 的触发类型别省略
  • 多用 status:调试时把不用的外设设成 "disabled",能减少很多干扰

好了,这一章的内容就到这儿。设备树的语法其实不复杂,多写几遍就熟了。下一章咱们聊聊如何用设备树描述具体的外设——到时候会用到今天讲的这些基础。

可可精品资料库