3. 设备树语法:节点、属性、标签、引用的完整解析
设备树(Device Tree)这东西,说白了就是一块描述硬件信息的“数据板子”。Linux内核启动时,Bootloader把这块板子传给内核,内核就知道自己跑在什么硬件上了。我刚开始接触设备树时,觉得它不就是个配置文件嘛,后来踩了几个坑才发现,它的语法细节远比想象中讲究。
今天咱们就把设备树的四个核心概念——节点、属性、标签、引用——彻底捋一遍。嗯,保证你学完就能自己写一个简单的设备树。
3.1 节点:硬件设备的“身份证”
设备树里的每个硬件设备,都对应一个节点。节点用花括号 {} 包裹,里面可以嵌套子节点。来看个最基础的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard-v1";
cpu@0 {
device_type = "cpu";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
根节点 / 是整棵树的起点。每个节点都有个名字,比如 cpu@0,@ 后面跟的是地址或单元号。我个人习惯把节点名写得尽量短,但一定要能一眼看出它代表什么硬件。
关键点:节点名中的 @ 地址部分不是必须的,但如果有多个同类型设备(比如两个UART),建议加上地址来区分。
3.2 属性:描述硬件的“标签”
属性就是键值对,写在节点内部。常见的属性类型有:
- 字符串:
compatible = "vendor,device"; - 整数:
reg = <0x1000 0x100>; - 布尔值:
status = "okay";或status = "disabled"; - 字节数组:
local-mac-address = [00 11 22 33 44 55];
我在项目中遇到过最坑的事,就是 compatible 属性写错了。驱动匹配全靠它,一旦拼写错误,内核根本认不出你的设备。你想想看,调试了半天,结果发现是少了个逗号,那感觉……
我的建议:写 compatible 时,遵循“厂商,设备型号”的格式,比如 "ti,am335x-uart"。这样既规范,又不容易冲突。
3.3 标签:给节点起个“外号”
标签(Label)就是给节点起个别名,方便在其他地方引用。语法很简单,在节点名前加个冒号:
uart0: serial@44e09000 {
compatible = "ti,am3352-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里的 uart0 就是标签。有了它,你可以在别处用 &uart0 来引用这个节点。为什么要这么做?因为设备树里经常需要覆盖或扩展某个节点,直接写全路径太啰嗦了。
我曾经在一个项目里,设备树有上百个节点,没有标签的话,每次修改都得翻半天路径。后来我强制团队所有新节点都加标签,效率提升了一大截。
3.4 引用:把节点“串”起来
引用就是通过标签或路径,把多个节点关联起来。最常见的用法是 &标签:
&uart0 {
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
这段代码的意思是:找到标签为 uart0 的节点,给它添加或覆盖属性。注意,引用不会创建新节点,它只是修改已有节点。
还有一种引用是 <&节点路径>,但说实话,我很少用。标签引用更直观,也更不容易出错。
注意:引用时,如果标签不存在,编译会报错。我曾经因为复制粘贴忘了改标签名,排查了整整一下午。所以,写完后记得用 dtc -I dts -O dtb 编译检查一下。
3.5 实战:一个完整的设备树片段
咱们把上面学的东西串起来,写一个完整的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard-v1";
chosen {
stdout-path = &uart0;
};
soc {
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@44e09000 {
compatible = "ti,am3352-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
gpio0: gpio@44e07000 {
compatible = "ti,am3352-gpio";
reg = <0x44e07000 0x1000>;
interrupts = <96>;
gpio-controller;
#gpio-cells = <2>;
};
};
};
&uart0 {
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
这个例子包含了根节点、子节点、标签、引用,以及常见的属性。你仔细看看 chosen 节点里的 stdout-path,它引用了 uart0,告诉内核把控制台输出到这个串口。
3.6 设备树语法结构图
下面这张图,帮你把设备树的语法结构理清楚:
从这张图可以看得很清楚:根节点下面挂子节点,子节点可以带标签,标签用来做引用,属性则描述节点的具体信息。整个结构就像一棵倒挂的树,根在上,枝叶在下。
3.7 避坑指南
最后,分享几个我踩过的坑:
- 标签不能重复:整个设备树里,标签名必须唯一。我曾经在两个不同文件里定义了同名标签,编译时没报错,但运行时就乱套了。
- 引用不会覆盖整个节点:
&uart0 { ... };只会添加或覆盖花括号里的属性,不会删除节点原有的其他属性。这一点很多人搞混。 - 路径引用要小心:如果用
&/soc/uart0这种路径引用,一旦节点路径变了,引用就失效了。标签引用更安全。
我的习惯:写设备树时,先画个草图,把节点、标签、引用关系理清楚。然后按“根节点 → 子节点 → 标签 → 引用”的顺序写,这样不容易乱。
好了,设备树的语法就讲到这里。你只要把节点、属性、标签、引用这四个概念吃透,写设备树就跟搭积木一样简单。下次咱们聊聊设备树的编译和调试,那才是真正见真章的地方。