3. 设备树核心属性:compatible、reg 与 interrupts

设备树里属性很多,但真正天天打交道的,就那么几个。我个人习惯把 compatible、reg 和 interrupts 称为「三大件」—— 你只要把这三个搞明白,80% 的设备树节点你都能看懂、能改、能调。

咱们一个一个来。先说最关键的。

3.1 compatible 属性:你是谁?你兼容谁?

compatible 说白了就是告诉内核:「我这个设备是啥型号,以及我能兼容哪些驱动。」内核在启动时,会拿着这个字符串去驱动列表里匹配。匹配上了,驱动就 probe;匹配不上,设备就「躺尸」。

标准格式:

compatible = "厂商,型号", "通用型号";

第一个字符串要尽量具体,第二个可以写一个更通用的兼容型号。内核匹配时从左往右试,试到哪个算哪个。

举个例子,我在项目中调过一块 i.MX6ULL 的板子,它的 UART 节点长这样:

uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
};

你看,它写了三个字符串。内核会先试 fsl,imx6ul-uart,如果内核里没有这个驱动,就试 fsl,imx6q-uart,再不行就试 fsl,imx21-uart。这种「降级兼容」的做法,在芯片升级换代时特别有用。

我的习惯:写 compatible 时,永远把最精确的型号放第一位。别偷懒只写一个通用型号,否则哪天换了新芯片,驱动匹配出问题,你查起来会想哭。

3.2 reg 属性与地址编码:你的设备在哪?

reg 描述的是设备的地址空间 —— 说白了就是告诉内核:「我的寄存器从哪个地址开始,占多大地方。」

它的格式是:

reg = <地址1 长度1 地址2 长度2 ...>;

地址和长度的位数,由父节点的 #address-cells 和 #size-cells 决定。这两个值很关键,我见过不少新手在这上面翻车。

属性 含义 常见值
#address-cells 地址占几个 32 位单元 1(32位地址)或 2(64位地址)
#size-cells 长度占几个 32 位单元 1(32位长度)或 0(无长度)

举个例子,一个 64 位地址空间的设备:

parent {
    #address-cells = <2>;
    #size-cells = <2>;

    device@10000000 {
        reg = <0x0 0x10000000 0x0 0x1000>;
        // 地址: 0x10000000, 长度: 0x1000
    };
};

这里 #address-cells = 2,所以地址用两个 32 位数拼成 64 位:0x0 0x10000000 就是 0x00000000_10000000。长度同理。

我曾经踩过的坑:有一次我写 reg 时,父节点是 #address-cells = <1>,但我习惯性地写了两个地址值。结果内核解析出来的地址完全不对,设备根本没法工作。查了整整半天才发现是 cells 数对不上。记住:reg 里的每个地址/长度占几个 32 位,完全由父节点说了算,不是你随便写的。

3.3 interrupts 属性与中断控制器

interrupts 描述的是设备的中断信息。它告诉内核:「我这个设备用哪个中断号,什么触发方式。」

格式通常是:

interrupts = <中断类型 中断号 触发方式>;

但具体几个 cell,由父中断控制器的 #interrupt-cells 决定。不同的中断控制器,格式不一样。

中断控制器 #interrupt-cells 格式说明
GIC(ARM 通用中断控制器) 3 <中断类型 SPI/PPI 中断号 触发方式>
老式中断控制器(如 8259) 2 <中断号 触发方式>
简单中断控制器 1 <中断号>

以 ARM GIC 为例,三个 cell 的含义:

  • 第一个 cell:中断类型。0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)。
  • 第二个 cell:中断号。SPI 从 32 开始编号,PPI 从 16 开始。
  • 第三个 cell:触发方式。1 表示上升沿触发,4 表示高电平触发。

实际例子:

interrupt-parent = <&gic>;
interrupts = <0 26 4>;  // SPI 类型,中断号 26,高电平触发

注意:如果设备树里没有写 interrupt-parent,那设备默认继承父节点的中断控制器。但为了可读性,我建议每个设备都显式写上 interrupt-parent,哪怕它和父节点一样。这样别人看代码时不用往上翻好几层才知道中断连到哪了。

3.4 三者关系:一张图说清楚

这三个属性其实描述了一个设备的三个核心维度:你是谁(compatible)、你在哪(reg)、你怎么通知 CPU(interrupts)。它们共同构成了内核识别和控制一个设备的基础。

下面这张图,是我自己画的一个设备树节点与内核驱动的对应关系,你看完应该就全明白了:

设备树核心属性与内核驱动对应关系 设备树核心属性与内核驱动对应关系 设备树节点 compatible "厂商,型号" 驱动匹配的关键 reg <地址 长度> 寄存器基地址与范围 interrupts <类型 号 触发方式> 中断连接信息 内核驱动 of_match_table 匹配 compatible 决定是否 probe of_iomap / platform_get_resource 解析 reg 获取地址 映射到虚拟地址空间 request_irq / of_irq_get 解析 interrupts 注册中断处理函数 三个属性共同决定了内核能否正确识别、访问和控制一个硬件设备

3.5 实战中的常见问题

我这些年调试设备树,遇到最多的问题就出在这三个属性上。总结几个典型场景:

  1. compatible 写错了:驱动明明编译进去了,但就是 probe 不了。查一下 compatible 字符串,往往是一个字母大小写不对,或者厂商名写错了。
  2. reg 的 cells 数不对:父节点是 #address-cells = <2>,但 reg 里只写了一个地址值。内核解析出来的地址完全错位,设备读写寄存器直接挂掉。
  3. interrupts 的触发方式不匹配:设备是高电平触发,但你在设备树里写成了上升沿触发。结果中断要么不触发,要么频繁误触发。
  4. 忘了写 interrupt-parent:设备没有显式指定中断控制器,而父节点也没有默认的中断控制器。内核找不到中断路由,设备的中断功能完全失效。

我的建议:拿到一个新板子,第一件事就是去读它的设备树源文件。把每个节点的 compatible、reg、interrupts 都过一遍,对照芯片手册确认。这一步做好了,后面能省 80% 的调试时间。

嗯,三大核心属性就讲到这里。你想想看,一个设备节点无非就是这三件事:告诉内核你是谁、你在哪、你怎么通知我。搞明白了,设备树就算入门了。