2、DTS语法入门:节点、属性、值的表示方法,以及include机制
好,咱们正式开始啃DTS语法这块硬骨头。
说实话,我刚开始接触设备树时,看着那一堆花括号和分号,心里直犯嘀咕:这不就是个配置文件吗?怎么搞得这么复杂?后来被硬件折腾了几回,才明白——设备树其实是内核与硬件之间的「翻译官」。你写对了,内核就认识你的板子;写错了,系统直接罢工给你看。
2.1 节点:设备树的骨架
节点是DTS最基本的组成单元。你可以把它想象成一个文件夹,每个节点代表一个硬件设备或总线。
节点长这样:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
看到没?根节点是 /,里面挂着cpu和memory两个子节点。每个节点用花括号包裹,末尾加分号。
节点命名规则:
- 格式:
节点名@地址 - 节点名不能有空格、特殊字符
- 地址部分通常用十六进制,比如
cpu@0、i2c@1c00000
我个人习惯把节点名写得尽量短,但别短到看不懂。比如 uart0 就比 serial@44e09000 好记,但后者更精确。看项目要求吧。
2.2 属性:描述设备的「形容词」
属性就是节点的特征描述。每个属性都有名字和值,用等号连接,分号结尾。
常见的属性类型:
| 属性名 | 含义 | 示例 |
|---|---|---|
| compatible | 设备兼容性标识 | "vendor,device" |
| reg | 寄存器地址/长度 | <0x1000 0x100> |
| interrupts | 中断号 | <0 29 4> |
| status | 设备状态 | "okay" 或 "disabled" |
嗯,这里要注意:compatible 是内核匹配驱动的关键。我见过有人把厂商名写反了,结果驱动死活加载不上。排查了半天,最后发现是 "ti,am335x" 写成了 "am335x,ti"——这种低级错误,犯一次就记住了。
2.3 值的表示方法:五花八门但都有规矩
DTS里的值类型挺多的,我列个表给你看:
| 类型 | 写法 | 说明 |
|---|---|---|
| 字符串 | "hello" | 双引号包裹 |
| 32位整数 | <0x12345678> | 尖括号包裹 |
| 64位整数 | <0x00000000 0x80000000> | 两个32位拼起来 |
| 字节数组 | [01 02 03 04] | 方括号包裹 |
| 字符串列表 | "str1", "str2" | 逗号分隔 |
| 混合值 | <0x1000> "info" | 空格分隔不同类型 |
举个例子,一个完整的I2C控制器节点:
i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
status = "okay";
clock-frequency = <100000>;
};
你看,reg 用了两个32位整数表示基地址和长度,interrupts 用一个整数表示中断号,clock-frequency 直接给数值。每种值都有自己的一套写法,别搞混了。
小技巧:64位地址一定要用两个32位整数表示。我曾经在项目里写 reg = <0x800000000>,编译直接报错。后来才知道,DTC编译器默认32位,超过范围的地址得拆开写。
2.4 include机制:代码复用的利器
一个复杂的板子,设备树可能有几千行。全写在一个文件里?那维护起来简直是噩梦。
DTS支持 #include 预处理指令,用法和C语言一模一样:
// myboard.dts
#include "soc.dtsi"
#include "myboard-pinmux.dtsi"
/ {
model = "MyBoard v2";
compatible = "vendor,myboard-v2";
&uart0 {
status = "okay";
pinctrl-0 = &uart0_pins;
};
};
这里 soc.dtsi 定义了芯片内部的外设,myboard-pinmux.dtsi 定义了引脚复用。主文件只需要引用并覆盖需要的部分就行。
为什么会用 .dtsi 后缀?说白了,这是设备树的头文件。就像C语言的 .h 文件一样,.dtsi 专门放公共定义,.dts 放板级定制内容。
避坑指南:我曾经在一个项目里,把 #include 写成了 /include/,这是老式语法,新版本DTC已经不支持了。编译报错时我还纳闷:明明语法没错啊?后来查文档才发现,从2015年起,DTC就只认 #include 了。
2.5 节点引用与覆盖
include进来之后,怎么修改已有的节点?用 & 引用:
// soc.dtsi 中定义了
uart0: serial@44e09000 {
compatible = "ti,omap4-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
// myboard.dts 中覆盖
&uart0 {
status = "okay";
clock-frequency = <48000000>;
};
注意看,uart0: 是标签(label),后面用 &uart0 引用。这样就不用把整个节点重新写一遍,只改需要改的属性就行。
我个人觉得,这种设计很优雅。就像面向对象里的继承——基类定义通用属性,子类只覆盖差异部分。内核开发者们确实用心了。
2.6 实战:一个完整的DTS文件长啥样
把上面这些知识点串起来,一个典型的设备树文件大概是这样:
/dts-v1/;
#include "am335x.dtsi"
#include "am335x-bone-common.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB
};
leds {
pinctrl-names = "default";
pinctrl-0 = <&user_led_pins>;
led@2 {
label = "beaglebone:green:heartbeat";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
&uart0 {
status = "okay";
};
&i2c0 {
status = "okay";
clock-frequency = <400000>;
};
你看,根节点定义板级信息,memory节点描述内存,leds节点描述板载LED,最后用引用打开UART和I2C。结构清晰,层次分明。
核心要点总结:
- 节点用花括号,属性用等号,分号结尾不能忘
- compatible 是驱动匹配的关键,写错就认不出硬件
- 64位地址要拆成两个32位整数
- #include 复用公共定义,&引用覆盖节点
- .dtsi 是头文件,.dts 是板级文件
嗯,DTS语法其实不难,难的是把硬件手册里的寄存器地址、中断号、引脚配置准确翻译成设备树。我刚开始写的时候,经常对着几百页的datasheet一个一个数地址,眼睛都快看花了。但熟练之后,你会发现设备树其实就是硬件的「地图」——节点是地标,属性是路标,include就是地图的图例。把这张图画好了,内核就能在你的板子上跑得顺风顺水。
这张图把DTS语法的四个核心要素串起来了。你写设备树时,就按这个流程走:先include公共头文件,再定义节点,填充属性,最后用引用做定制。顺序对了,思路就清晰了。