2、设备树基础语法(上):节点与属性的定义,根节点、别名节点、标签的使用,&引用语法
好,咱们正式开始啃设备树的语法。说实话,我刚接触设备树那会儿,看着一堆花括号和分号,第一反应是「这玩意儿到底算代码还是配置文件?」后来做多了才明白,它其实是一棵描述硬件资源的树,每个树枝叫节点,每片叶子叫属性。今天咱们就把这棵树的结构彻底捋清楚。
2.1 节点(node)—— 树的骨架
设备树里的一切都是节点。一个节点代表一个硬件设备或总线。比如一个串口、一个GPIO控制器、一个I2C总线,都是节点。
节点的基本语法长这样:
node_name {
property1 = <value>;
property2 = "string";
child_node {
...
};
};
注意那个分号,每个属性后面都要加分号,花括号后面也要加分号。我见过不少新手在这上面栽跟头——少个分号,编译直接报错,找半天找不到原因。
节点名可以带地址后缀,比如 uart@fe010000,@后面的数字通常是寄存器基地址。这主要是为了区分同类型设备,比如两个串口:
uart@fe010000 {
compatible = "ns16550";
reg = <0xfe010000 0x1000>;
};
uart@fe020000 {
compatible = "ns16550";
reg = <0xfe020000 0x1000>;
};
你看,名字不同,地址不同,但都是串口。内核通过节点名里的地址来区分它们。
2.2 属性(property)—— 节点的血肉
属性就是描述节点特征的数据。常见的属性类型有几种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "my-device"; |
用于匹配驱动 |
| 32位整数 | reg = <0x1000 0x200>; |
地址和长度 |
| 64位整数 | reg = <0x00000000 0x10000000>; |
高32位在前 |
| 字符串列表 | interrupt-names = "rx", "tx"; |
用逗号分隔 |
| 二进制数据 | mac-address = [00 11 22 33 44 55]; |
用方括号括起来 |
我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核匹配驱动时第一个就看它。你把它藏到后面,别人读代码时还得翻半天。
2.3 根节点 —— 树的起点
每个设备树文件只有一个根节点,用斜杠 / 表示。它就像文件系统的根目录,所有其他节点都挂在它下面。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
cpus {
cpu@0 {
compatible = "arm,cortex-a8";
};
};
};
根节点里通常放的是整个板级的信息:CPU类型、内存布局、选定的控制台等。注意那个 chosen 节点,它不是描述硬件,而是告诉内核「你该用哪个串口打印日志」。嗯,这个节点挺特殊的,后面讲内核启动流程时还会提到。
2.4 别名节点(aliases)—— 给节点起个短名
设备树里节点路径往往很长,比如 /soc/serial@fe010000。每次引用都写全路径,手都酸了。别名节点就是用来解决这个问题的。
/ {
aliases {
serial0 = &uart0;
serial1 = &uart1;
i2c0 = &i2c0;
mmc0 = &mmc0;
};
};
有了别名,你在其他地方就可以直接用 serial0 代替那串长路径。内核启动时也会根据别名来分配设备编号。比如 serial0 对应 /dev/ttyS0,serial1 对应 /dev/ttyS1。
2.5 标签(label)与 & 引用语法
标签是设备树里最常用的「指针」机制。你可以在节点前面加个标签,然后在其他地方用 &标签名 来引用它。
uart0: serial@fe010000 {
compatible = "ns16550";
reg = <0xfe010000 0x1000>;
interrupts = <32>;
};
这里 uart0: 就是标签。注意标签和节点名之间用冒号隔开。有了标签,你就可以在其他地方扩展这个节点:
&uart0 {
clock-frequency = <24000000>;
status = "okay";
};
这段代码的意思是:找到标签为 uart0 的节点,给它添加两个新属性。这种写法在设备树里叫「追加节点」。它特别适合在板级文件里修改 SoC 级文件中的节点。
举个例子,SoC 厂商提供的 am335x.dtsi 里定义了串口的基本信息:
uart0: serial@fe010000 {
compatible = "ns16550";
reg = <0xfe010000 0x1000>;
interrupts = <32>;
status = "disabled";
};
注意状态是 disabled,因为不是所有板子都用这个串口。然后在你的板级文件 myboard.dts 里:
#include "am335x.dtsi"
&uart0 {
clock-frequency = <24000000>;
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
status = "okay";
};
你看,不用重新定义整个节点,只需要用 &uart0 引用它,然后添加板级特有的属性。这就是标签和引用的威力——实现了 SoC 级和板级代码的解耦。
2.6 实战中的常见坑
说了这么多,我分享几个实际项目中踩过的坑:
- 标签重复:两个文件里定义了同名的标签,编译时不会报错,但后面的会覆盖前面的。我曾经因为 include 了两个头文件,结果串口标签被覆盖,调试了整整一天。
- 引用未定义的标签:如果你写了
&some_label但 nowhere 定义了这个标签,编译会报错。检查一下是不是拼写错了,或者忘记 include 对应的头文件。 - 属性覆盖 vs 追加:如果你在同一个节点里重复定义同一个属性,后面的值会覆盖前面的。但如果你用
&标签追加,新属性和旧属性是合并的,不会覆盖。 - 分号遗漏:这个我提过,但值得再说一遍。设备树编译器对分号很敏感,少一个就报错。我习惯写完一个节点后,先数花括号和分号的数量,再编译。
2.7 知识体系总览
为了让你对本章内容有个整体印象,我画了一张图:
这张图把本章的核心内容串起来了。你看,根节点是起点,下面分支出节点定义、属性定义和引用机制三大块。节点定义关注的是「怎么描述一个硬件」,属性定义关注的是「用什么数据来描述」,而引用机制关注的是「怎么复用已有的描述」。三者缺一不可。
好了,设备树的基础语法上篇就到这里。下篇我们会深入 reg 属性的地址编码、中断映射、以及 #address-cells 和 #size-cells 这些让人头疼的概念。到时候咱们再接着聊。
arch/arm/boot/dts/ 目录,随便挑一个 .dts 文件,试着找出根节点、别名节点、以及至少三个带标签的节点。然后看看这些标签在其他地方是怎么被引用的。动手做一遍,比看十遍都管用。
可可精品资料库