2、设备树基础语法:节点、属性、值的定义与规则
好,咱们正式开始啃设备树的语法。说实话,很多初学者一看到.dts文件里那些花括号、分号、数字和字符串混在一起,就觉得头大。我当年第一次接触时也差不多——这玩意儿到底是描述硬件还是写配置文件?
其实你把它想象成一本「硬件说明书」就行了。设备树就是用一种树形结构,把CPU、内存、外设这些硬件资源,清清楚楚地告诉内核。今天我就带你把这本说明书从头到尾翻一遍。
2.1 节点:设备树的基本单元
设备树里的一切,都是从节点开始的。每个节点代表一个硬件设备或总线。节点可以嵌套,就像文件夹套文件夹一样。
一个最简单的节点长这样:
/ {
model = "MyBoard";
compatible = "vendor,myboard";
};
最外层的/是根节点,代表整个板子。里面可以挂CPU、内存、串口、I2C控制器等等。每个节点都有自己的名字,格式是node-name@unit-address。
节点命名规则:
node-name:通用名称,比如uart、i2c、gpio@unit-address:可选,通常是设备基地址,比如uart@fe001000
我个人习惯把节点名写得尽量通用。比如串口就叫serial,而不是my_uart_1。为什么?因为内核匹配驱动时,优先看compatible属性,节点名只是给人看的。你写得太花哨,同事接手时反而懵。
2.2 属性:描述节点的「形容词」
节点本身只是个空壳,真正描述硬件的是属性。每个属性都有一个名字和一个值。属性名是字符串,值可以是多种类型。
常见的属性类型有:
| 属性类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | model = "MyBoard" |
用双引号括起来 |
| 32位整数 | reg = <0x10000000 0x1000> |
尖括号括起来,空格分隔 |
| 64位整数 | reg = <0x00000000 0x10000000> |
用两个32位拼一个64位 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55] |
方括号括起来,十六进制 |
| 字符串列表 | compatible = "vendor,device", "generic-device" |
逗号分隔多个字符串 |
| 混合类型 | foo = <0x1234>, "string" |
用逗号分隔不同类型 |
嗯,这里要注意:compatible属性是最重要的,没有之一。内核驱动就是靠它来匹配设备的。格式通常是"厂商,设备型号",比如"ti,am3359-uart"。我在项目中遇到过有人把厂商名写反了,结果驱动死活加载不上,查了两天才发现是"uart,ti"写成了"ti,uart"——顺序反了!
2.3 值的编码规则:大端还是小端?
设备树里所有整数都是大端(Big-Endian)编码。这一点坑过不少人。你想想看,x86和ARM默认都是小端,但设备树偏偏用大端。为什么?因为设备树最初来自PowerPC,PowerPC就是大端。
举个例子:
reg = <0x12345678>;
在内存里实际存的是0x12 0x34 0x56 0x78(从低地址到高地址)。如果你用小端思维去读,就会得到0x78563412,完全错了。
避坑指南:我曾经在调试一个FPGA设备时,发现读出来的寄存器地址总是对不上。查了半天,原来是设备树里写的地址是大端,而驱动里用of_property_read_u32读出来自动转了CPU字节序。但那个FPGA的寄存器本身也是大端,结果转了两次,地址就乱了。所以记住:设备树里的整数是大端,但内核API会自动帮你转换,别自己再转一次。
2.4 特殊属性:标准化的「潜规则」
除了自定义属性,设备树规范定义了一些标准属性,每个设备节点都应该遵守。我挑几个最常用的说说:
- reg:描述设备的地址空间。格式是
<地址 长度>,可以有多个。 - #address-cells 和 #size-cells:告诉解析器,reg里的地址和长度各占几个32位整数。比如
#address-cells = <1>表示地址是32位,#address-cells = <2>表示64位。 - interrupts:描述中断号。具体格式取决于中断控制器。
- status:设备状态,常用值是
"okay"或"disabled"。调试时很有用。
举个例子,一个典型的UART节点:
uart0: serial@fe001000 {
compatible = "vendor,uart";
reg = <0xfe001000 0x1000>;
interrupts = <0 33 4>;
status = "okay";
};
这里reg表示UART的寄存器基地址是0xfe001000,长度0x1000字节。interrupts的三个数字分别表示中断类型、中断号、触发方式。具体含义要看中断控制器的绑定文档。
2.5 引用与标签:让设备树更灵活
设备树支持用标签(label)来引用节点。标签写在节点名前,用冒号分隔:
uart0: serial@fe001000 {
// ...
};
然后其他地方可以用&uart0来引用这个节点。这在覆盖或修改节点时特别有用。比如你想在板级设备树里使能某个UART:
&uart0 {
status = "okay";
};
这样就不用把整个节点重新写一遍。我习惯把所有外设节点都加上标签,方便后续扩展。你想想看,如果芯片有6个UART,每个都要写一遍status = "okay",没有标签的话得复制粘贴6次,万一改个名字还得改6处——太容易出错了。
2.6 包含文件:复用与模块化
设备树支持#include预处理指令,跟C语言一样。通常SoC厂商会提供一个.dtsi文件,定义芯片内部的所有外设。板级.dts文件再包含它,然后只修改需要改的部分。
// 芯片级定义:soc.dtsi
#include "soc.dtsi"
/ {
model = "MyBoard";
compatible = "vendor,myboard";
};
// 板级定义:myboard.dts
#include "soc.dtsi"
&uart0 {
status = "okay";
clock-frequency = <115200>;
};
这种分层设计,说白了就是「不要重复造轮子」。芯片厂商把通用部分写好,板级开发者只需要关注自己板子特有的东西。我在做项目时,经常看到有人把整个设备树从头写到尾,结果芯片升级时所有板子都得改——这就是没用好.dtsi的后果。
2.7 知识体系总览
说了这么多,咱们用一张图把设备树语法的核心脉络理清楚:
我的小建议:刚开始学设备树,别急着背语法。先找一块现成的开发板,打开它的.dts文件,对照着原理图看。看到某个外设节点,就去查它的compatible属性,然后在内核源码里搜对应的驱动。这样来回几次,语法自然就熟了。我当年就是这么干的,比死啃文档快得多。
好了,设备树的基础语法就这些。节点、属性、值,再加上引用和包含,你已经能看懂大部分.dts文件了。下一节咱们会深入reg属性和地址映射,那才是真正考验理解能力的地方。