4、设备树基础概念:Device Tree的起源与作用、DTS/DTC/DTB文件格式、节点与属性的语法规则

4.1 为什么要有设备树?—— 一段“野蛮生长”的历史

在讲设备树之前,我想先聊聊没有设备树的日子。我入行那会儿,Linux 内核里全是板级文件。每个开发板一个 board-xxx.c,里面密密麻麻地注册各种硬件资源:中断号、GPIO、内存地址、时钟频率……全写死在 C 代码里。

这有什么问题?问题大了。你想想看,一个 SoC 可能对应几十种开发板,每个板子外设稍有不同,就得改内核源码重新编译。内核社区维护这些板级文件,简直像在泥潭里走路。我记得有一次,为了给一块新板子移植内核,我光改 board-xxx.c 就花了两天,还因为一个 GPIO 号写错,点不亮 LCD,排查了整整一个下午。

所以,设备树(Device Tree)应运而生。说白了,就是把硬件描述从内核源码里“剥离”出来,变成一份独立的数据结构。内核启动时解析这份数据,动态构建设备模型。从此,换板子不再需要改内核,改一份 .dts 文件就行。

核心思想: 硬件描述与内核代码分离。一份内核镜像,通过更换设备树,适配不同硬件。

4.2 设备树的“三兄弟”:DTS、DTC、DTB

这三个缩写你肯定见过,但它们的区别和关系,我建议你一次理清。

名称 全称 本质 谁在用
DTS Device Tree Source 文本文件,人类可读 开发者编写
DTC Device Tree Compiler 编译器,将 DTS 编译为 DTB 构建系统调用
DTB Device Tree Blob 二进制文件,机器可读 Bootloader 传递给内核

流程很简单:你写 .dts 文件,然后用 dtc 工具编译成 .dtb 文件。Bootloader(比如 U-Boot)在启动内核时,把 .dtb 的地址通过寄存器传给内核。内核解析 DTB,构建设备模型。

嗯,这里要注意:.dtsi 文件是“头文件”或“包含文件”,通常放 SoC 公共部分。比如 imx6ull.dtsi 定义 i.MX6ULL 芯片内部所有外设,而具体的板子文件 myboard.dts 通过 #include 引用它,再添加板级差异。

我的习惯: 把 SoC 级定义放在 .dtsi,板级差异放在 .dts。这样复用性最好,也方便后续升级内核版本。

4.3 节点与属性:设备树的“骨架”和“血肉”

设备树的结构,本质上是一棵倒置的树。根节点是 /,下面挂 CPU、内存、外设总线等。每个节点代表一个设备或总线,节点里用属性描述设备的细节。

我直接给你看一个典型的设备树片段,比讲一百句都管用:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "My i.MX6ULL Board";
    compatible = "mycompany,imx6ull-board", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB RAM
    };

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "heartbeat";
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            linux,default-trigger = "heartbeat";
        };
    };

    &uart1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

我们来拆解一下这个例子里的关键概念。

4.3.1 节点(Node)

  • 根节点:/,整棵树的起点。
  • 子节点:chosen、memory、leds、uart1 都是根节点的子节点。
  • 节点名:比如 led0,通常格式是 名称@地址,当地址唯一时可以不写。
  • 节点路径:从根到节点的完整路径,比如 /leds/led0。

4.3.2 属性(Property)

  • compatible:最重要的属性。内核通过它匹配驱动。格式是 "厂商,设备型号"。比如 "mycompany,imx6ull-board"。内核驱动里会声明一个 of_match_table,设备树里的 compatible 和驱动里的匹配上,驱动才会 probe。
  • reg:描述地址空间。格式是 <起始地址 长度>。比如内存的 reg = <0x80000000 0x20000000> 表示从 0x80000000 开始,512MB 空间。
  • status:设备状态。常用值:"okay"(启用)、"disabled"(禁用)。
  • gpios:描述 GPIO 引脚。格式是 <&gpio控制器 引脚号 标志>。
  • pinctrl-*:引脚复用配置。通过 pinctrl-names 和 pinctrl-0 引用 pinmux 节点。
我曾经踩过的坑: compatible 字符串写错一个字母,驱动死活不 probe。排查了半天,最后发现是 "fsl,imx6ull" 写成了 "fsl,imx6ul"。少了一个 'l',内核不认。所以,compatible 一定要和驱动里的 of_match_table 严格一致。

4.4 设备树的核心语法规则

我整理了一份速查表,你写 DTS 时可以对照着看:

语法元素 规则 示例
节点定义 节点名 { ... }; uart1 { ... };
属性赋值 属性名 = 值; status = "okay";
字符串 双引号包裹 "okay"
32位整数 <数值> <0x80000000>
64位整数 两个32位整数组合 <0x00000000 0x80000000>
字节数组 [十六进制字节] [00 01 02]
引用 &节点标签 &uart1
包含文件 #include "文件" #include "imx6ull.dtsi"

4.5 设备树的核心结构图

下面这张图,我建议你多看几遍。它把设备树的层级关系、节点属性、以及和内核驱动的匹配逻辑都串起来了。

设备树核心结构图 根节点 / model = "My Board" compatible = "mycompany,board" chosen memory leds stdout-path = &uart1 reg = <0x80000000 0x20000000> led0 label = "heartbeat" gpios = <&gpio1 3 ...> 设备树与内核驱动匹配流程 1. Bootloader 将 DTB 地址传给内核 2. 内核解析 DTB,构建 device_node 树 3. 内核遍历 device_node,根据 compatible 匹配驱动 4. 匹配成功 → 调用驱动的 probe() 函数,读取 reg、gpios 等属性 5. 匹配失败 → 设备不被注册,驱动不加载

4.6 避坑指南:我见过的设备树“翻车”现场

做设备树配置,有几个地方特别容易出错。我把自己踩过的坑和帮别人排查的案例整理一下:

  • reg 属性地址写错:我曾经把内存的起始地址写成了 0x80000000,但实际硬件是 0x60000000。内核启动到一半就崩了。排查方法:用 dtc -I dtb -O dts 反编译 DTB,核对地址值。
  • 中断号不对应:设备树里的中断号是硬件中断号,不是 Linux IRQ 号。我见过有人直接把 cat /proc/interrupts 看到的号写进设备树,结果完全不工作。记住:设备树里写的是 硬件中断号,内核会做映射。
  • pinctrl 配置冲突:两个设备共用了同一个引脚,但都设成了 status = "okay"。结果一个设备工作正常,另一个完全没反应。排查时用 cat /sys/kernel/debug/gpio 看引脚状态,才发现冲突。
  • 忘记加 #include 或路径不对:编译 DTS 时,如果引用了 .dtsi 但路径没写对,DTC 会报错。我建议在 Makefile 里用 DTC_FLAGS 指定搜索路径,避免这种低级错误。
我的调试三板斧:
1. 反编译 DTB:dtc -I dtb -O dts -o dump.dts boot.dtb,看实际传给内核的数据对不对。
2. 查看设备树在 /sys 下的映射:ls /sys/firmware/devicetree/base/,看节点是否被正确解析。
3. 查看驱动是否 probe:cat /sys/kernel/debug/devices_deferred,看哪些设备被延迟 probe 了。

4.7 小结

设备树是嵌入式 Linux 驱动开发的基石。你不需要背下所有语法,但一定要理解它的设计思想:硬件描述与代码分离。DTS 是源码,DTC 是编译器,DTB 是内核吃进去的二进制数据。节点描述设备,属性描述细节,compatible 是驱动匹配的钥匙。