2. 设备树语法

好,咱们今天聊聊设备树的语法。说实话,我刚接触设备树那会儿,看着满屏的.dts文件,心里直犯嘀咕——这玩意儿不就是个配置文件吗?后来踩了几个坑才明白,它的语法设计其实很讲究,每个关键字都有它的道理。

节点(node):设备树的骨架

节点是设备树最基本的组成单元。你可以把它想象成一颗树上的分叉点,每个节点代表一个硬件设备或总线。

节点长这样:

/ {
    cpu {
        compatible = "arm,cortex-a7";
        reg = <0x00>;
    };
    
    memory {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };
};

根节点用/表示,所有设备都挂在这个根下面。每个节点用花括号{}包裹,最后用分号结束。

我的经验:节点命名最好用全小写,单词之间用短横线连接。比如i2c-controller而不是I2C_Controller。Linux内核的维护者对这个很敏感,命名不规范会被打回来重改。

属性(property):描述节点的特征

属性就是节点的“身份证信息”。它告诉内核:这个设备是什么、怎么访问、需要什么资源。

常见的属性类型有:

属性类型 示例 说明
字符串 compatible = "ti,am335x-uart"; 设备兼容性标识
32位整数 reg = <0x48020000 0x1000>; 地址和大小
布尔值 status = "disabled"; 设备是否启用
字符串列表 interrupt-names = "rx", "tx"; 中断名称列表

这里有个坑——reg属性里的地址和大小,顺序不能搞反。我曾经在调试一个SPI控制器时,把基地址和长度写反了,结果内核访问内存直接崩溃。排查了整整两天才找到问题。

注意:compatible属性是设备树里最重要的属性之一。它的格式是"厂商,设备型号"。比如"ti,am335x-uart",前面的ti是厂商名,后面的am335x-uart是具体型号。内核驱动就是靠这个字符串来匹配设备的。

标签(label):给节点起个“外号”

标签就是节点的别名。为什么要用标签?你想想看,如果一个节点嵌套得很深,比如/soc/bus@48000000/i2c@48020000,每次引用它都要写这么长一串,多麻烦。

标签的用法:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

这里&i2c0就是引用了一个标签。标签的定义通常在别的地方:

i2c0: i2c@48020000 {
    // 节点内容
};

i2c0:就是标签,后面跟着节点名。引用时用&i2c0。

避坑指南:我曾经在一个项目里,给两个不同的节点定义了相同的标签。结果编译设备树时,编译器只认最后一个定义,前面的全被覆盖了。调试时怎么都找不到那个节点,最后用dtc -I dtb -O dts反编译才发现标签冲突了。所以,标签命名一定要全局唯一。

包含机制(include):模块化你的设备树

设备树文件可以像C语言一样使用#include。这个机制让设备树变得可维护、可复用。

最常见的用法:

#include "am335x.dtsi"
#include "am335x-bone-common.dtsi"

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
    
    &uart0 {
        status = "okay";
    };
};

.dtsi文件是设备树的“头文件”,通常存放SoC的通用定义。不同的板子可以包含同一个.dtsi,然后覆盖或添加自己的配置。

这样做的好处很明显:

  • SoC厂商只需要维护一份.dtsi文件
  • 板卡厂商只需要关注自己板子特有的部分
  • 代码复用率高,减少重复劳动

我的习惯:我会把设备树拆成三层:soc.dtsi(SoC通用)、board-common.dtsi(板级通用)、board-specific.dts(具体配置)。这样当产品线有多个变种时,只需要修改最上层的.dts文件就行。

知识体系结构图

下面这张图展示了设备树语法的核心逻辑:

根节点 / 节点 cpu 节点 memory 节点 i2c compatible = "arm,cortex-a7" device_type = "memory" reg = <0x48020000 0x1000> 标签: cpu0: 标签: mem: 标签: i2c0: 包含机制 #include am335x.dtsi (SoC通用) board-common.dtsi (板级通用) 最终 .dts 文件

这张图把设备树的四个核心要素串起来了。从根节点出发,每个节点携带属性和标签,最后通过包含机制组合成完整的设备树文件。说白了,这就是一个搭积木的过程——SoC厂商提供基础模块,板卡厂商在上面做定制。

总结一下:节点是骨架,属性是血肉,标签是快捷方式,包含机制是模块化工具。这四个东西配合好了,设备树就能写得既清晰又高效。我见过有些工程师把所有东西都塞在一个.dts文件里,几千行代码看着就头疼。学会用包含机制拆分文件,你的同事会感谢你的。