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 知识体系总览

说了这么多,咱们用一张图把关系理清楚:

设备树标准属性知识体系 身份标识类 compatible 驱动匹配的关键 model 硬件平台名称 status 设备启用/禁用 资源描述类 reg 寄存器地址/长度 interrupts 中断描述 ranges 地址映射转换 地址编码辅助属性 #address-cells #size-cells interrupt-parent 每个属性都有明确的语法规则和解析方式 写错一个 cell,驱动可能就跑飞了

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 到板级,一步步来。

可可精品资料库