4. 设备树机制:设备树语法、dts文件编写、设备树与驱动的匹配过程

设备树,英文叫 Device Tree,是嵌入式 Linux 里绕不开的一个东西。我刚开始接触它的时候,觉得这玩意儿就是个“硬件描述清单”,后来踩了几个坑才明白——它远不止是清单,它是操作系统和板级硬件之间的“翻译官”。

说白了,设备树就是用来告诉内核:我这块板子上挂了哪些外设,它们挂在哪个地址上,用哪个中断,时钟频率多少。没有它,内核就得靠一堆硬编码的板级文件来认硬件,那维护起来简直是灾难。

核心观点:设备树把硬件描述从内核源码里剥离出来,变成一份独立的数据结构。换板子改 dts,不用重新编译内核。

4.1 设备树语法基础

设备树的源文件是 .dts,编译后变成 .dtb(二进制),内核启动时解析它。语法其实不复杂,我总结一下最常用的几个要素。

4.1.1 节点(Node)

每个外设或总线都是一个节点。节点可以嵌套,形成树状结构。

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard";

    cpu@0 {
        device_type = "cpu";
        reg = <0>;
    };

    serial@101f0000 {
        compatible = "ns16550";
        reg = <0x101f0000 0x1000>;
        interrupts = <1 0>;
    };
};

看到没?@ 后面跟的是地址或单元号,reg 属性描述地址范围。我习惯把 compatible 当作节点的“身份证”,驱动就是靠它来认领设备的。

4.1.2 属性(Property)

属性就是键值对。常见的有:

属性名 含义 示例
compatible 设备兼容标识,驱动匹配的关键 "vendor,device"
reg 地址/长度对,描述寄存器空间 <0x1000 0x200>
interrupts 中断号与触发方式 <0 2>
clocks 时钟句柄 <&clk 1>
status 设备状态:okay 或 disabled "okay"
我的习惯:写 dts 时,先把 compatible 和 reg 写对,其他属性可以慢慢补。这两个错了,驱动根本认不到设备。

4.2 dts 文件编写实战

写 dts 文件,说白了就是“对着原理图填参数”。我拿一个 I2C 控制器挂温度传感器的例子来说明。

/dts-v1/;

#include "soc.dtsi"

/ {
    model = "MySensorBoard";
    compatible = "vendor,sensorboard";

    &i2c1 {
        status = "okay";
        clock-frequency = <100000>;

        temp_sensor: tmp102@48 {
            compatible = "ti,tmp102";
            reg = <0x48>;
            interrupt-parent = <&gpio1>;
            interrupts = <15 2>;
        };
    };
};

这里有几个要点:

  • #include 可以引用 SoC 厂商提供的 .dtsi 文件,省去重复定义。
  • &i2c1 是引用节点,用来覆盖或追加属性。
  • 子节点 tmp102@48 的 reg 是 I2C 从机地址,不是内存地址。
我曾经踩过的坑:I2C 设备的 reg 写成了内存地址,结果驱动 probe 的时候读寄存器全返回 0xFF。排查了半天,才发现是地址类型搞混了。记住:reg 的含义由父节点的 #address-cells 和 #size-cells 决定。

4.3 设备树与驱动的匹配过程

这部分是重点。内核怎么把 dts 里的节点和驱动代码对应起来?我画了一张图,你看完就明白了。

设备树与驱动匹配流程 设备树与驱动匹配流程 DTS 节点 compatible = "ti,tmp102" 驱动 of_match_table = "ti,tmp102" 内核匹配 probe() 被调用 匹配过程说明: 1. 内核遍历所有设备节点,提取 compatible 属性。 2. 遍历已注册驱动的 of_match_table,逐一比较字符串。 3. 匹配成功 → 调用驱动的 probe() 函数,传入设备节点信息。

匹配的核心就是 compatible 字符串的比较。内核里每个驱动都会声明一个 of_match_table,里面放一串它支持的设备名。当内核解析 dts 时,拿着节点的 compatible 去这个表里找,找到了就调用驱动的 probe()。

4.3.1 匹配的三种方式

匹配方式 原理 典型场景
compatible 匹配 字符串精确匹配 绝大多数外设驱动
设备名匹配 节点名称匹配(已废弃) 旧版内核兼容
ACPI 匹配 ACPI 表匹配(x86 平台) 服务器、PC 平台
关键点:嵌入式平台几乎只用 compatible 匹配。你写驱动时,of_match_table 里的字符串必须和 dts 里的 compatible 完全一致,大小写、逗号、空格都不能错。

4.4 避坑指南

  • compatible 写反了:我见过有人把 "ti,tmp102" 写成 "tmp102,ti",内核匹配不上,驱动 probe 不执行。记住:厂商名在前,设备名在后。
  • reg 的 cells 数不对:父节点定义了 #address-cells = <1>,子节点 reg 却写了两个地址,内核解析会错位。
  • status 忘了写:很多 SoC 的 .dtsi 里默认把外设节点 status = "disabled",你必须在板级 .dts 里显式改成 "okay"。
调试小技巧:内核启动时加 earlycon 和 dtoc 参数,可以打印设备树的解析过程。看到 "OF: ..." 开头的日志,就是设备树在干活。

嗯,设备树这部分内容不少,但核心就三件事:语法、写 dts、理解匹配流程。你只要把 compatible 和 reg 搞明白,再配合内核的打印信息,大部分问题都能定位。我当年第一次调设备树驱动,就是靠加打印一步步看匹配结果,最后发现是 dts 里少了个逗号——这种错误,犯过一次就再也不会忘了。