设备树语法入门:节点、属性、标签、别名、包含文件
设备树(Device Tree)说白了就是一块板子的「硬件简历」。CPU 是什么?内存多大?外设挂在哪?全写在一个文本文件里。内核启动时读它,就知道该怎么初始化硬件了。
我刚开始接触设备树时,觉得它跟 XML 差不多——都是树形结构。但用久了你会发现,它比 XML 简洁得多。今天咱们就把最核心的五个语法要素讲透:节点、属性、标签、别名、包含文件。
一句话总结:设备树 = 一棵树,节点是树枝,属性是树叶,标签和别名是给树枝贴的标签,包含文件是复用其他树的枝叶。
2.1 节点(Node)—— 树的骨架
节点是设备树的基本单元。每个节点代表一个硬件设备或总线。写法很简单:
/dts-v1/;
/ {
cpu {
compatible = "arm,cortex-a72";
reg = <0x00>;
};
memory {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
看到没?/ 是根节点,里面挂了一个 cpu 和一个 memory。节点可以嵌套,比如:
/ {
soc {
i2c {
eeprom {
compatible = "atmel,24c02";
};
};
};
};
我在项目中遇到过有人把节点名写成 I2C(大写),结果内核死活认不出来。记住:节点名统一小写,用下划线或连字符分隔单词。
命名规范:节点名格式为 name@address,比如 i2c@3f804000。address 部分写该设备在总线上的地址或寄存器基址。
2.2 属性(Property)—— 节点的血肉
属性描述节点的具体特征。常用的属性类型有四种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ti,am335x-uart"; |
驱动匹配的关键,格式为 "厂商,设备名" |
| 32位整数 | reg = <0x48020000 0x1000>; |
地址和长度,尖括号包裹 |
| 64位整数 | reg = <0x00000000 0x80000000>; |
高32位在前,低32位在后 |
| 布尔值 | status = "disabled"; |
存在即 true,不存在即 false |
举个例子,一个完整的 UART 节点长这样:
uart0: serial@48020000 {
compatible = "ti,am335x-uart", "ti,omap3-uart";
reg = <0x48020000 0x2000>;
interrupts = <72>;
status = "disabled";
};
这里 compatible 写了两个字符串,内核会按顺序匹配驱动。第一个匹配不上就试第二个。我曾经因为写反了顺序,导致驱动加载了错误的配置,折腾了大半天。
注意:reg 属性的地址和长度单位是 #address-cells 和 #size-cells 决定的。默认都是 1,即 32 位。如果父节点指定了 2,那就是 64 位。
2.3 标签(Label)—— 给节点起个外号
标签就是节点的别名,方便在其他地方引用。写法是在节点名前加 标签名::
uart0: serial@48020000 {
// ...
};
i2c0: i2c@44e0b000 {
// ...
};
有了标签,你就可以在别处用 &uart0 来引用它。比如在板级文件中覆盖配置:
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
我个人习惯给所有重要的外设都加上标签。这样后期做多板卡适配时,改起来特别方便。你想想看,如果没有标签,你要修改一个深层嵌套的节点,得把整条路径写一遍,多累啊。
2.4 别名(Alias)—— 系统级的快捷方式
别名和标签有点像,但作用域不同。别名定义在根节点的 aliases 子节点里:
/ {
aliases {
serial0 = &uart0;
serial1 = &uart1;
i2c0 = &i2c0;
mmc0 = &mmc0;
};
};
别名主要用于 Bootloader(如 U-Boot)和内核早期启动阶段。比如 U-Boot 通过 serial0 就能找到控制台串口,不用知道它具体叫 uart0 还是 serial@48020000。
| 对比项 | 标签(Label) | 别名(Alias) |
|---|---|---|
| 定义位置 | 节点名前 | 根节点 aliases 下 |
| 引用方式 | &标签名 |
serial0 字符串 |
| 主要用途 | DTS 文件内引用 | Bootloader / 内核早期使用 |
| 作用域 | 整个 DTS 编译单元 | 全局(通过 /aliases 节点) |
我的建议:标签和别名都写上。标签方便 DTS 内部引用,别名方便 Bootloader 和用户空间工具查找设备。两者不冲突,反而互补。
2.5 包含文件(Include)—— 别重复造轮子
设备树支持 #include 预处理指令,跟 C 语言一样。通常用来引入 SoC 公用的 .dtsi 文件:
#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 可以衍生出几十种板卡,每个板卡只需写几十行差异配置。
我曾经维护过一个项目,有 8 块不同板卡。一开始我把所有配置写在一个文件里,用 #ifdef 区分。结果文件膨胀到 3000 多行,改一处要查半天。后来改成 .dtsi + 板级 .dts 的结构,每个文件不到 200 行,清爽多了。
最佳实践:
- SoC 级通用配置放 .dtsi
- 板级差异配置放 .dts
- 多板卡共享的部分再抽一个 common.dtsi
- 避免在 .dts 中重复定义已在 .dtsi 中定义的节点
2.6 知识体系总览
下面这张图把今天讲的五个要素串起来了。你可以看到它们如何协同工作:
2.7 实战小练习
光看不练假把式。给你一个简单的需求:为一块虚拟板卡编写设备树,包含一个 CPU、256MB 内存、一个 UART(地址 0x10000000,中断号 33)。
试试看,能不能写出下面这个效果:
/dts-v1/;
#include "virtual-soc.dtsi"
/ {
model = "Virtual Board v1.0";
compatible = "virtual,board-v1";
aliases {
serial0 = &uart0;
};
};
&uart0 {
status = "okay";
clock-frequency = <48000000>;
};
嗯,这里要注意:virtual-soc.dtsi 里已经定义了 uart0 节点,板级文件只需要用 &uart0 引用并启用它。别重复定义整个节点,否则编译器会报错。
常见错误:新手最容易犯的错是在板级 .dts 里重新写一遍 uart0: serial@10000000 { ... };,而不是用 &uart0 { ... }; 来覆盖。前者会定义一个新节点,后者才是引用已有节点。