设备树(Device Tree)解析:dts 文件结构、绑定规则与硬件描述

设备树,说白了就是一套描述硬件的“身份证”。

我刚开始接触 Linux 驱动时,最头疼的就是看那些 .dts 文件。密密麻麻的层级,一堆看不懂的 compatible 字符串。后来被一个 bug 折磨了三天,才发现是设备树里一个中断号写错了。嗯,从那以后,我再也不敢小看这个“树”了。

1. 为什么需要设备树?

在 ARM Linux 早期,所有硬件信息都硬编码在 board-xxx.c 文件里。换一个板子就要重新编译内核,维护成本极高。设备树的出现,把硬件描述从内核源码中剥离出来,变成一份独立的数据结构。

你想想看,内核启动时只需要解析这个树形结构,就能知道:

  • CPU 是什么型号
  • 内存有多大
  • 外设挂在哪里
  • 中断怎么路由
  • 时钟频率多少

说白了,设备树就是硬件和驱动之间的“翻译官”。

2. dts 文件结构

一个典型的 .dts 文件长这样:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  /* 256 MB */
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};

我来拆解一下这个结构:

  • 根节点:/ 是整个树的起点,包含 model 和 compatible 属性
  • 节点名称:格式是 node-name@unit-address,比如 uart0: serial@44e09000
  • 属性:键值对,描述硬件特性
  • reg:地址和长度信息,由 #address-cells 和 #size-cells 决定
个人习惯:我写 dts 时,喜欢把节点别名(如 uart0)和完整路径都写上。调试时用 find /sys/firmware/devicetree/ 查找非常方便。

3. 绑定规则:compatible 的玄机

compatible 是设备树里最重要的属性。它决定了驱动和设备如何“配对”。

格式是 "厂商,型号",比如 "ti,am3352-uart"。内核驱动通过 of_match_table 来匹配这个字符串。

我遇到过一个问题:某款传感器芯片,厂商给的 dts 里 compatible 写的是 "bosch,bmp280",但内核驱动里匹配的是 "bmp280"。结果驱动死活加载不上。查了两天才发现是字符串不匹配。

避坑指南:compatible 字符串必须和驱动里的 of_device_id 表完全一致,大小写、逗号、空格都不能错。我曾经因为多了一个空格,浪费了一整天。

4. 硬件描述的核心属性

除了 compatible,还有几个属性是驱动开发必须掌握的:

属性 作用 示例
reg 描述地址空间 reg = <0x44e09000 0x2000>
interrupts 中断号 interrupts = <0 72 4>
clocks 时钟源 clocks = <&clk_uart0>
gpios GPIO 引脚 gpios = <&gpio1 15 0>
status 设备状态 status = "okay" 或 "disabled"

这里要注意 interrupts 的格式。它通常包含三个字段:中断控制器类型、中断号、触发方式。不同架构的编码方式不一样,ARM GIC 和 RISC-V PLIC 就有差异。

5. 设备树的核心逻辑:一张图看懂

下面这张图是我自己总结的设备树解析流程,每次写驱动前我都会过一遍:

设备树解析与驱动绑定流程 dts 文件 dtc 编译 dtb 二进制 内核 of 子系统解析 匹配 compatible 读取 reg/irq 等 关键点:compatible 字符串必须完全一致,否则驱动无法绑定

6. 实战:如何编写一个简单的设备树节点

假设我们要添加一个 I2C 温度传感器,挂在 I2C0 总线上,地址是 0x48。我会这样写:

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

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

这里有几个要点:

  • &i2c0 是引用已有的 I2C 控制器节点
  • temperature@48 是子节点名称,@后面跟设备地址
  • interrupt-parent 指定中断控制器,这个很容易漏掉
我的经验:写设备树时,先看芯片手册里的内存映射表。把外设的基地址、中断号、时钟源都列出来,再对照写 dts。这样不容易出错。

7. 避坑指南:我踩过的几个坑

  • reg 的 cells 数量搞错:#address-cells 和 #size-cells 决定了 reg 的解析方式。ARM 平台通常是 1 个地址 + 1 个长度,但有些总线是 2 个地址 + 1 个长度。写错了驱动就读不到正确的地址。
  • 中断号偏移:不同中断控制器对中断号的编码不一样。GIC 的 SPI 中断号从 32 开始,但有些 dts 里写的是硬件原始号。我遇到过驱动里中断号对不上,导致中断无法触发的 case。
  • status 默认是 okay:有些 dts 里没写 status,默认就是 enabled。但如果你引用了某个节点,又在另一个文件里把它 disable 了,就会产生冲突。我建议显式写出 status。

8. 调试技巧

设备树写对了没有?我一般用这几个方法验证:

  1. dtc 编译检查:dtc -I dts -O dtb -o test.dtb test.dts,看有没有语法错误
  2. 内核启动日志:dmesg 里搜 OF: fdt,能看到设备树解析过程
  3. sysfs 查看:ls /sys/firmware/devicetree/base/,看节点是否生成
  4. 驱动 probe 日志:在驱动里加 dev_info,看 probe 是否被调用
重要提醒:修改设备树后,一定要重新编译并烧录 dtb 文件。我见过有人只改了 dts,忘了编译,结果调试了一整天。

设备树这东西,说难不难,说简单也不简单。关键是要理解它的设计思想:把硬件描述和驱动代码解耦。你只要掌握了节点、属性、compatible 匹配这三个核心概念,剩下的就是查手册、对着写。

嗯,这一章的内容就到这里。记住,写设备树时多想想:如果我是内核,看到这个节点,能不能找到对应的驱动?能不能正确初始化硬件?想通了这两个问题,你就出师了。