设备树语法:节点、属性、标签、引用、包含文件
各位同学,咱们今天来啃设备树语法这块硬骨头。说实话,我刚入行那会儿,看到设备树文件里一堆花括号、冒号、分号,头都大了。后来在项目里被坑过几次,才真正摸透了它的脾气。
设备树说白了就是一套描述硬件信息的「说明书」。它用树形结构告诉内核:你有哪些外设?挂在哪个总线上?中断怎么接?GPIO怎么配?嗯,咱们今天就把这套语法的核心要素捋清楚。
1. 节点(Node)—— 设备树的基本单元
每个设备或总线都是一个节点。节点用花括号 { } 包裹,里面可以嵌套子节点。我习惯把节点想象成「硬件模块的身份证」。
/ {
soc {
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
};
};
};
看到没?根节点 / 下面挂了个 soc,soc 下面又挂了 serial@10000000。这个 @10000000 是节点的地址,用来区分同名设备。我在一个项目里遇到过两个串口都叫 serial,结果编译报错——就是因为忘了加地址后缀。
节点命名规则:
- 节点名 + 可选的
@地址 - 节点名不能以数字开头
- 同一层级下节点名必须唯一
2. 属性(Property)—— 描述硬件特征
属性就是键值对,用来描述节点的各种参数。常见的属性类型有:
| 属性类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ns16550"; |
驱动匹配的关键 |
| 32位整数 | reg = <0x10000000 0x1000>; |
地址和长度 |
| 布尔值 | status = "disabled"; |
控制设备启用/禁用 |
| 数组 | interrupts = <0 5 4>; |
中断描述 |
这里有个坑:compatible 属性是驱动和设备树之间的「接头暗号」。内核启动时,会遍历设备树,找到 compatible 匹配的驱动来初始化硬件。我曾经把 "ns16550" 写成了 "ns16550a",结果驱动死活不认——就少了个字母,查了一下午。
3. 标签(Label)—— 给节点起个外号
标签就是节点的别名。你看上面代码里的 uart0:,这就是标签。有了标签,其他地方引用这个节点就方便多了。
&uart0 {
status = "okay";
clock-frequency = <115200>;
};
我个人习惯给所有重要外设都加上标签,比如 i2c0、spi1、gpio2。这样在板级文件中修改配置时,不用去翻原始 .dtsi 文件里的完整路径。
小技巧:标签名建议用「外设类型+序号」的格式,比如 uart0、mmc1。别用 my_serial 这种名字,三个月后你自己都看不懂。
4. 引用(Reference)—— 把节点串起来
引用用 & 符号表示。它的作用是在一个地方定义节点,在另一个地方补充或修改它。这在实际项目中太常用了。
// 在 soc.dtsi 中定义
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
status = "disabled";
};
// 在 board.dts 中引用并修改
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
你想想看,如果所有板子都共用同一个 soc.dtsi,但不同板子的串口配置不一样。用引用机制,我们只需要在板级文件里覆盖 status 和 pinctrl 就行了。嗯,这就是设备树设计的精妙之处。
注意:引用时不能修改节点的 compatible 和 reg 属性。这些是硬件的「基因」,必须在原始定义中确定。我曾经试图在引用里改 reg 地址,结果内核直接崩溃——因为它找不到对应的硬件。
5. 包含文件(Include)—— 模块化管理
设备树支持 #include 预处理指令,跟 C 语言一样。通常我们会把 SoC 公共部分放在 .dtsi 文件中,板级特有部分放在 .dts 文件中。
// imx6ull.dtsi
#include "imx6ull-pinfunc.h"
/ {
soc {
uart1: serial@02020000 {
compatible = "fsl,imx6ull-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
};
};
};
// myboard.dts
#include "imx6ull.dtsi"
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
我在项目中见过有人把所有东西都塞进一个 .dts 文件,结果文件超过 5000 行,维护起来简直噩梦。正确的做法是:SoC 厂商提供的 .dtsi 不要动,板级文件只写差异部分。
文件组织建议:
.dtsi:SoC 公共定义、外设控制器、中断控制器.dts:板级配置、GPIO 引脚复用、设备使能.h:宏定义、引脚编号、中断号
知识体系总览
下面这张图帮你理清今天讲的内容。我画图时习惯把节点比作树干,属性是树叶,标签是树上的名牌,引用是树枝之间的连接线。
这张图把今天讲的五个要素串起来了。根节点下面挂 soc,soc 下面挂 uart,uart 节点有标签 uart0,节点内部有属性描述硬件信息,其他地方可以通过 &uart0 引用它,而整个文件通过 #include 实现模块化管理。
最后说个实战经验:调试设备树时,我最常用的命令是 dtc -I dtb -O dts 反编译,看看实际生效的设备树长什么样。有时候你写的 .dts 和编译后的 .dtb 可能不一样——嗯,预处理、宏展开、覆盖合并,中间环节多了去了。
好了,设备树语法就这些。记住:节点是骨架,属性是血肉,标签是名字,引用是纽带,包含文件是组织方式。把这五样玩熟了,设备树对你来说就是小菜一碟。
课后练习:找一个实际开发板的 .dts 文件,试着找出里面所有的节点、标签、引用和包含文件。然后自己动手写一个简单的设备树片段,描述一个 GPIO 按键和一个 LED 灯。