3. 设备树基础语法(下):标准属性详解与地址映射
好,咱们接着聊设备树。上一节我们把基本语法框架搭起来了,这一节要深入几个最常用的标准属性。说实话,这些属性你写驱动时几乎天天见,但很多人只是照猫画虎,真出了问题就抓瞎了。
我个人习惯把设备树属性分成两类:一类是“身份标识”,告诉内核你是谁、你长什么样;另一类是“资源描述”,告诉内核你的寄存器在哪、中断怎么接。咱们一个个来看。
3.1 compatible:你的身份证
compatible 属性,说白了就是设备的名字。内核通过它来匹配驱动。格式很简单:
compatible = "厂商,型号", "通用型号";
注意顺序——越具体的越靠前。内核匹配时从左往右找,找到第一个能用的驱动就停。
核心规则:compatible 字符串必须与驱动中 of_match_table 里的内容完全一致,大小写都不能错。
我在项目中遇到过一件事:某次移植一个 SPI 触摸屏,驱动死活加载不上。查了两天,最后发现 compatible 写的是 "goodix,gt911",但驱动里匹配的是 "goodix,gt911s"。差一个字母,内核就不认。嗯,这种坑踩过一次就记住了。
3.2 reg 与地址编码:#address-cells 和 #size-cells
这两个属性是地址映射的基石。你想想看,CPU 访问外设寄存器,总得知道地址范围吧?reg 就是干这个的。
但问题来了——地址到底占几个 32 位?长度又占几个?这就需要 #address-cells 和 #size-cells 来告诉解析器。
// 父节点定义地址编码规则
soc {
#address-cells = <1>; // 地址用 1 个 cell(32位)
#size-cells = <1>; // 长度用 1 个 cell
uart0: serial@10000000 {
reg = <0x10000000 0x1000>; // 基址0x10000000,长度0x1000
};
};
如果地址是 64 位的,#address-cells 就设成 2:
bus {
#address-cells = <2>;
#size-cells = <1>;
device@0 {
reg = <0x00000000 0x80000000 0x1000>;
// 地址 = 0x00000000_80000000,长度 = 0x1000
};
};
我的经验:写 reg 时,先数清楚父节点的 #address-cells 和 #size-cells 各是多少。数错了,内核解析出来的地址就是错的,驱动读到的寄存器全是乱码。
3.3 interrupts:中断怎么接
中断描述比地址稍微复杂一点,因为它涉及中断控制器。基本格式:
device {
interrupt-parent = <&gic>; // 指定中断控制器
interrupts = <0 31 4>; // 中断号、类型、触发方式
};
这里 interrupts 每个 cell 的含义由中断控制器决定。以 ARM GIC 为例:
| Cell | 含义 | 说明 |
|---|---|---|
| 第1个 | 中断类型 | 0=SPI(共享外设中断),1=PPI(私有外设中断) |
| 第2个 | 中断号 | SPI 从 32 开始编号 |
| 第3个 | 触发方式 | 1=上升沿,2=下降沿,4=高电平,8=低电平 |
注意:中断号不是物理中断号,而是经过中断控制器映射后的逻辑号。我曾经在调试一个网卡驱动时,把物理中断号直接写进去了,结果中断一直触发不了。后来查手册才发现,SPI 中断号要从 32 开始算。
3.4 status:设备的开关
status 属性控制设备是否启用。常见值:
"okay"— 设备正常启用"disabled"— 设备禁用"reserved"— 保留,不启用"fail"— 设备检测失败
这个属性在板级适配时特别有用。比如 SoC 上有 4 个 I2C 控制器,但你的板子只用了 2 个,剩下的设成 disabled 就行,不用从设备树里删掉。
&i2c2 {
status = "disabled"; // 这个 I2C 控制器不用
};
3.5 model:板子叫什么名字
model 属性很简单,就是描述硬件平台名称的字符串。通常放在根节点:
/ {
model = "MyBoard v2.0 based on i.MX8M Plus";
compatible = "mycompany,myboard", "fsl,imx8mp";
};
内核启动时会把 model 打印出来,方便你确认加载的是哪个设备树。我调试时经常先看这个,确保没拿错 dtb 文件。
3.6 ranges:地址映射的桥梁
ranges 属性是地址转换表。它把子节点的地址空间映射到父节点的地址空间。格式:
ranges = <子地址 父地址 长度>;
举个例子:
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x40000000 0x10000000>;
// 子地址0x0 映射到 父地址0x40000000,长度0x10000000
ethernet@0 {
reg = <0x0 0x1000>;
// 实际物理地址 = 0x40000000 + 0x0 = 0x40000000
};
};
如果 ranges 为空(ranges;),表示子地址和父地址是 1:1 映射。如果没有 ranges 属性,子节点就不能被 CPU 直接访问——这在 PCIe 等总线中很常见。
关键理解:ranges 做的是地址翻译,不是地址分配。它告诉内核:子节点说的地址,在父节点视角下对应哪个地址。
3.7 知识体系总览
说了这么多,咱们用一张图把关系理清楚:
3.8 避坑指南:我踩过的那些坑
做设备树这么多年,有些坑是共通的,我列几个典型的:
- #address-cells 和 #size-cells 不匹配:父节点设了 2 个 cell,子节点 reg 只写了 1 个地址。内核解析时直接错位,读到的地址完全不对。
- interrupts 的 cell 数搞错:不同中断控制器要求的 cell 数不一样。GIC 要 3 个,有些简单控制器只要 1 个。写之前先看中断控制器的
#interrupt-cells。 - ranges 写反了:子地址和父地址的顺序不能搞混。我曾经把父地址写在前面,结果设备树编译不报错,但运行时地址映射全乱套了。
- compatible 字符串大小写:Linux 是大小写敏感的。"Gpio" 和 "gpio" 是两个不同的字符串。我建议统一用小写。
调试小技巧:内核启动时加 of_unittest 参数,可以运行设备树自测。另外 /sys/firmware/devicetree/base/ 目录下能看到解析后的设备树,是排查问题的好帮手。
好了,这一节的内容就到这。标准属性是设备树的骨架,理解了它们,你就能看懂大部分设备树文件了。下一节我们聊聊如何从头编写一个完整的设备树——从 SoC 到板级,一步步来。
可可精品资料库