2. 设备树语法入门:节点、属性、标签、别名、包含文件
好,咱们正式开始啃设备树的语法。说实话,很多工程师第一次看 .dts 文件,会觉得像在看天书——一堆花括号、分号、数字,完全不知道在描述什么。我当年刚接触时也差不多,对着一个 2000 行的设备树文件发懵。
但你别怕。设备树的语法其实很单纯。它本质上就是一个树形结构,用来描述硬件资源。CPU 有几个核?内存地址范围是多少?I2C 控制器挂在哪组寄存器上?统统写在这里。
这一节,我把最核心的五个概念掰开揉碎讲给你:节点、属性、标签、别名、包含文件。你把这五个搞明白,就能读懂 90% 的设备树文件。
2.1 节点——设备树的骨架
设备树里的一切都是节点。每个节点代表一个硬件设备或总线。节点从根(/)开始,一层层往下挂。
看个最简单的例子:
/ {
compatible = "acme,coyote";
cpus {
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
这里 / 是根节点。cpus 是子节点,cpu@0 是孙节点。memory@80000000 也是根的子节点。看到没?节点名后面可以跟 @地址,用来区分同名的设备。比如两个串口,可以叫 serial@10001000 和 serial@10002000。
2.2 属性——描述设备的语言
节点里放的是属性。属性告诉内核:这个设备是什么、用哪个驱动、寄存器在哪、中断号是多少。
常用的属性类型就几种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "arm,cortex-a7"; |
驱动匹配的关键字 |
| 32位整数 | reg = <0x80000000 0x40000000>; |
地址和长度,尖括号包起来 |
| 64位整数 | reg = <0x0 0x80000000 0x0 0x40000000>; |
两个 32 位拼一个 64 位 |
| 布尔值 | status = "disabled"; |
用字符串表示,没有 true/false |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号包起来,十六进制 |
| 字符串列表 | compatible = "ti,am335x-uart", "ns16550"; |
逗号分隔,内核会依次尝试匹配 |
这里我要特别强调 compatible 属性。它是设备树里最重要的属性,没有之一。内核通过它找到对应的驱动。我见过有人把 compatible 写错一个字母,结果驱动死活不 probe,查了两天才发现是拼写问题。嗯,这种坑踩过一次就记住了。
2.3 标签——给节点起个方便引用的名字
设备树里节点可以嵌套很深。比如一个 I2C 控制器挂在第 3 个 APB 总线下,你要引用它,得写一长串路径:/soc/apb@10000000/i2c@2000。这太痛苦了。
标签就是用来解决这个问题的。你在节点前面加个冒号:
i2c0: i2c@2000 {
compatible = "snps,designware-i2c";
reg = <0x2000 0x100>;
interrupts = <5>;
};
这里的 i2c0 就是标签。后面想引用这个节点,直接用 &i2c0 就行。比如在另一个文件里要给它加个子设备:
&i2c0 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
你看,通过 &i2c0,我们不需要知道这个 I2C 控制器在设备树里的完整路径,直接就能找到它。这在大型项目中特别有用。
2.4 别名——更短的引用方式
别名和标签有点像,但用途不同。别名是在根节点的 aliases 子节点里定义的:
/ {
aliases {
serial0 = &uart0;
serial1 = &uart1;
i2c0 = &i2c0;
mmc0 = &mmc0;
};
};
别名主要给 bootloader 和早期启动代码用。比如 U-Boot 想通过 serial0 找到串口设备,它不需要解析整个设备树,直接查 aliases 表就行。
我个人习惯:标签用于设备树内部的交叉引用,别名用于给外部固件提供快捷入口。两者不冲突,可以同时用。
2.5 包含文件——把设备树拆成模块
一个 SoC 可能有几十个外设。如果全部写在一个 .dts 文件里,那文件会膨胀到几千行,维护起来非常痛苦。
设备树支持 #include 指令,跟 C 语言一样:
// am335x-boneblack.dts
#include "am335x.dtsi"
#include "am335x-bone-common.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
// ...
};
这里 .dtsi 文件是 SoC 的通用描述,.dts 文件是具体板级的描述。一个 SoC 可以对应多个板子,每个板子只需要写自己特有的部分,公共部分从 .dtsi 里继承。
这种分层设计,说白了就是复用。我在项目里见过一个团队维护 20 多款产品,每款产品只有 GPIO 和少量外设不同。他们就把公共部分放在一个 .dtsi 里,每款产品只写一个几十行的 .dts 文件。维护成本直线下降。
soc.dtsi— SoC 级描述,包含所有内置外设board-common.dtsi— 板级公共描述,比如 DDR 配置、时钟board-rev1.dts— 具体版本,只写差异部分
2.6 知识体系总览
说了这么多,咱们用一张图把整个知识结构串起来。你对照着看,心里就有谱了。
2.7 实战建议
学到这里,你可能会问:这些语法我都懂了,但实际写的时候从哪下手?
我的建议是三步走:
- 先读后写。找一份你手头开发板的 .dts 文件,对照芯片手册,把每个节点和属性都过一遍。我当年就是这么干的,读了三份不同 SoC 的设备树后,基本就摸清套路了。
- 从简单外设开始。别一上来就改 DDR 控制器或时钟树。先试试加一个 GPIO 按键或 LED,设备树改起来风险小,出问题也容易排查。
- 用好编译器的报错。设备树编译工具 dtc 会检查语法错误。我第一次写设备树时,漏了个分号,dtc 报错信息很明确,照着改就行。别怕报错,报错是帮你纠错的。
dtc -I dtb -O dts 可以把编译好的 .dtb 反编译回 .dts。如果你拿到一个只有 .dtb 的板子,反编译一下就能看到它的完整设备树结构。这招我在逆向分析时用过无数次。
好了,设备树语法的五个核心概念就讲到这里。你把这些搞明白,后面讲中断映射、DMA、时钟绑定时,就不会觉得陌生了。记住:节点搭骨架,属性填血肉,标签和别名让引用更灵活,包含文件让工程更整洁。就这么简单。