设备树基础语法:节点、属性、标签、别名、包含文件
好,咱们今天来啃设备树最核心的语法部分。说实话,我刚接触Zephyr那会儿,看到设备树文件里一堆花括号和冒号,第一反应是「这玩意儿跟JSON长得有点像啊」。后来用多了才发现,它比JSON灵活得多,但也更容易踩坑。
设备树说白了就是一套描述硬件的「说明书」。你的板子上有哪些外设、它们挂在哪个总线上、寄存器地址是多少、中断号是多少……全写在这里。Zephyr的驱动会去读这份说明书,然后决定怎么初始化硬件。
节点(Node)—— 设备树的基本单元
节点就是设备树里的「一个硬件设备」或「一个总线」。每个节点用一对花括号 { } 包裹,里面可以放属性,也可以嵌套子节点。
/dts-v1/;
/ {
soc {
uart0: serial@40002000 {
compatible = "ns16550";
reg = <0x40002000 0x1000>;
interrupts = <10>;
};
};
};
你看,soc 是一个节点,serial@40002000 是它的子节点。节点名字后面跟 @地址 是一种惯例,表示这个外设的基地址。我个人习惯把地址写全,这样看代码时一眼就能知道外设映射到了哪里。
compatible 属性才是驱动匹配的关键。名字只是给人看的,compatible 才是给代码看的。
属性(Property)—— 描述硬件的「形容词」
属性就是键值对,用来描述节点的特征。常见的属性类型有:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ns16550"; |
驱动匹配的关键字 |
| 32位整数 | interrupts = <10>; |
中断号、时钟频率等 |
| 64位整数 | reg = <0x40002000 0x1000>; |
地址和长度对 |
| 布尔值 | status = "okay"; |
启用或禁用设备 |
| 字符串数组 | clock-names = "uart", "pclk"; |
多个字符串用逗号分隔 |
嗯,这里要注意 status 属性。我在项目中遇到过好几次,明明设备树里配好了外设,驱动就是加载不上。查了半天,发现 status = "disabled" 没改成 "okay"。Zephyr 默认很多外设是禁用的,你得手动打开。
reg 属性里写错了地址长度,导致驱动只映射了前 256 字节,后面的寄存器全读成了 0。调试了整整一个下午才发现。所以 reg 里的第二个值一定要跟芯片手册对得上。
标签(Label)—— 给节点起个「外号」
标签就是节点前面的那个名字,比如 uart0:。有了标签,你就可以在别的地方引用这个节点,而不需要写完整的路径。
uart0: serial@40002000 {
compatible = "ns16550";
reg = <0x40002000 0x1000>;
};
// 在别的文件里引用
&uart0 {
status = "okay";
current-speed = <115200>;
};
看到 &uart0 了吗?这就是通过标签引用节点。我建议你给每个重要的外设都加上标签,尤其是 UART、SPI、I2C 这些经常需要修改配置的。不然每次都要写 &soc/serial@40002000,手都酸了。
别名(Alias)—— 更短的「外号」
别名跟标签有点像,但它是全局的,放在根节点的 aliases 下面:
/ {
aliases {
uart-console = &uart0;
led0 = &green_led;
};
};
为什么需要别名?因为 Zephyr 的很多子系统会通过别名来查找设备。比如控制台默认找 uart-console 这个别名指向的 UART。你想想看,如果换个板子,UART 的节点路径变了,但只要别名不变,代码就不用改。
包含文件(.dtsi)—— 别重复造轮子
.dtsi 文件就是设备树的「头文件」。芯片厂商会把 SoC 的通用定义放在 .dtsi 里,板级文件通过 #include 把它拉进来,然后只改自己板子特有的部分。
// stm32f4.dtsi(芯片级)
/ {
soc {
uart1: serial@40011000 {
compatible = "st,stm32-uart";
reg = <0x40011000 0x400>;
interrupts = <37>;
status = "disabled";
};
};
};
// my_board.dts(板级)
#include "stm32f4.dtsi"
&uart1 {
status = "okay";
current-speed = <115200>;
};
你看,芯片级文件里把 UART 的寄存器地址、中断号都定义好了,板级文件只需要打开它、配个波特率就行。这就是设备树的「分层设计」思想。
说实话,我刚开始写设备树时总想把所有东西都塞到一个文件里。后来维护了三个板子之后,发现改一个 SoC 的公共配置要改三个文件,累得够呛。从那以后,我再也不敢偷懒了,老老实实把 SoC 级定义抽到 .dtsi 里。
.dtsi 文件可以嵌套包含。比如 stm32f4.dtsi 可以再包含 armv7-m.dtsi,把 Cortex-M4 内核的公共配置也抽出来。层级越多,复用性越好。
总结一下
设备树的这五个语法要素,说白了就是一套「硬件描述语言」:
- 节点:描述一个硬件设备或总线
- 属性:描述节点的具体参数
- 标签:给节点起个短名字,方便引用
- 别名:全局的、更抽象的引用方式
- 包含文件:把公共定义抽出来,避免重复
我个人觉得,设备树最难的不是语法本身,而是「什么时候该用标签,什么时候该用别名」。我的原则是:如果只是驱动内部引用,用标签;如果是给应用层或子系统用的,用别名。这样分工明确,代码也清晰。
下一章咱们聊聊怎么在 Zephyr 里实际写一个设备树文件,然后看驱动是怎么读取这些属性的。到时候我会拿一个真实的 I2C 传感器来演示,保证你能上手。