第二节:设备树基础语法——节点、属性、标签、路径的规则与写法
好,咱们正式开始啃设备树这块硬骨头。说实话,我刚接触Zephyr那会儿,看到.dts文件里一堆花括号和分号,第一反应是「这玩意儿是C语言吗?」后来才明白,设备树有自己的语法体系,跟C八竿子打不着。今天我就把节点、属性、标签、路径这四个核心概念掰开揉碎了讲给你听。
一、节点(node)——设备树的骨架
节点是什么?说白了就是设备树里的「文件夹」。每个节点代表一个硬件设备或总线,比如UART、GPIO、SPI控制器。节点用花括号包裹,里面可以嵌套子节点。
看个最简单的例子:
/ {
soc {
uart0 {
compatible = "ns16550";
reg = <0x40002000 0x1000>;
};
};
};
这里/是根节点,soc是子节点,uart0是孙节点。每个节点都以分号结尾,花括号里的内容就是节点的「内部世界」。
i2c_controller而不是I2CController。Zephyr社区大部分设备树都这么写,保持一致能少踩很多坑。
二、属性(property)——节点的「身份证」
属性就是节点的配置信息。常见的属性类型有:
- 字符串:
compatible = "ns16550";—— 告诉内核这个设备兼容哪个驱动 - 整数:
reg = <0x40002000 0x1000>;—— 地址和大小 - 布尔值:
status = "okay";或status = "disabled"; - 数组:
interrupts = <0 1>;
属性值用分号结尾,多个值用空格隔开。注意看< >尖括号,它表示一个32位整数的数组。
避坑指南:我曾经在项目里把reg属性的地址写成了十进制,结果编译通过但运行时外设死活不工作。后来发现设备树里地址默认是十六进制,但< >里写十进制也不会报错——这就是个隐形的坑。所以,我建议所有地址都写成十六进制,比如0x40002000,一眼就能看出是地址。
三、标签(label)——给节点起个「外号」
标签就是节点的别名。为什么需要标签?因为设备树里节点路径可能很长,比如/soc/uart0,每次引用都写全路径太累赘。标签用冒号跟在节点名后面:
uart0: serial@40002000 {
compatible = "ns16550";
reg = <0x40002000 0x1000>;
};
这里uart0就是标签。之后在别的节点里引用时,直接用&uart0就行:
&uart0 {
status = "okay";
current-speed = <115200>;
};
你想想看,如果没有标签,你得写成&/soc/serial@40002000,多麻烦。标签让代码更简洁,也更容易维护。
uart0、uart1,SPI就叫spi0、spi1。别用my_device这种名字,三个月后你自己都看不懂。
四、路径(path)——从根到叶的「导航」
路径就是设备树里从根节点到目标节点的完整路线。路径用斜杠分隔,比如:
/—— 根节点/soc—— soc节点/soc/uart0—— uart0节点/soc/i2c0/touch@1a—— 挂在i2c0总线上的触摸芯片
路径在&引用时也会用到。比如你想在另一个文件里修改某个节点的属性,就得用路径或标签来定位它。
嗯,这里要注意:路径里的节点名可以包含@符号,后面跟的是地址或寄存器偏移。比如serial@40002000,这个@40002000不是必须的,但加上后能让人一眼看出这个外设的基地址。
五、综合示例——把四个概念串起来
咱们看一个完整的例子,把节点、属性、标签、路径都揉在一起:
/dts-v1/;
/ {
model = "My Custom Board";
compatible = "mycompany,myboard";
chosen {
zephyr,console = &uart0;
};
soc {
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@40002000 {
compatible = "ns16550";
reg = <0x40002000 0x1000>;
interrupts = <10 1>;
status = "disabled";
};
i2c0: i2c@40003000 {
compatible = "snps,designware-i2c";
reg = <0x40003000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
touch: touch@1a {
compatible = "mycompany,touch";
reg = <0x1a>;
interrupt-gpios = <&gpio0 5 0>;
};
};
};
};
这个例子里:
/是根节点,有model和compatible属性chosen节点是Zephyr的特殊节点,用来指定系统控制台uart0和i2c0是标签,分别指向serial@40002000和i2c@40003000touch节点是i2c0的子节点,路径是/soc/i2c0/touch@1a&uart0和&gpio0是引用标签的写法
重要提醒:设备树里#address-cells和#size-cells这两个属性决定了reg属性的解析方式。#address-cells = <1>表示地址占1个32位整数,#size-cells = <1>表示大小也占1个32位整数。如果写错了,驱动读到的地址会完全不对。我曾经在这个问题上折腾了一整天,最后发现是#size-cells写成了0,导致驱动认为reg里只有地址没有大小。
六、常见错误与调试技巧
写设备树时最容易犯的几个错误:
- 漏了分号:每个属性、每个节点结束都要有分号。漏一个分号,整个文件编译不过。
- 花括号不匹配:节点嵌套多了,很容易少写一个花括号。我建议用编辑器的高亮匹配功能。
- 标签重复:同一个设备树里不能有两个相同的标签。Zephyr编译时会报错,但错误信息可能不太直观。
- 路径写错:引用节点时路径必须完全匹配,大小写敏感。
调试小技巧:用dtc -I dts -O dtb your.dts把设备树编译成二进制,再用dtc -I dtb -O dts your.dtb反编译回来,看看有没有语法错误。Zephyr的构建系统也会在编译时检查设备树,但提前用dtc检查能更快发现问题。
好了,这一节的内容就这些。节点、属性、标签、路径这四个概念是设备树的基石,搞懂了它们,后面学设备树绑定、覆盖、宏定义就轻松多了。下一节咱们聊聊设备树绑定——就是那个告诉Zephyr「这个节点对应哪个驱动」的机制。