4. 设备树基础概念:为什么需要设备树?

各位同学,咱们今天聊设备树。说实话,我刚入行那会儿,Linux 内核还没设备树这玩意儿。那时候板级文件满天飞,一个 BSP 包里的 arch/arm/mach-xxx 目录,动辄几百个 C 文件,全是硬编码的寄存器地址、中断号、GPIO 映射。每次换个 Flash 型号,都得改源码重新编译内核。那叫一个痛苦。

后来设备树出现了。说白了,它就是一套用来描述硬件资源的“数据文件”,把硬件信息从内核源码里剥离出来。内核不再关心“你的板子用哪个串口”,它只负责解析设备树,然后按图索骥去初始化驱动。

4.1 为什么需要设备树?

我总结过三个核心痛点,设备树正好解决了它们:

  • 硬件描述与内核代码解耦:以前改个 GPIO 电平极性,得改 board.c 重新编译。现在改设备树,编译 dtb,重启就行。内核镜像不用动。
  • 多平台统一启动方式:ARM 社区当年最头疼的就是每个 SoC 厂商自己搞一套 bootloader 传参方式。设备树统一了 struct machine_desc 的匹配机制,内核启动时通过 dtb 里的 compatible 字符串找到对应板级支持。
  • 减少内核源码树膨胀:我见过一个项目,Linux 3.0 时代,单是 arch/arm/mach-omap2 目录就超过 10 万行。设备树普及后,新板子只需要加一个 dts 文件,几百行搞定。

核心观点:设备树不是内核的一部分,它是内核的“食材”。内核是厨师,设备树是菜谱。同样的厨师(内核),换本菜谱(dtb),就能做出不同口味的菜(适配不同硬件)。

4.2 DTS、DTC、DTB 的关系

这三个缩写,我建议你像记“源文件、编译器、目标文件”一样记牢:

缩写 全称 本质 后缀
DTS Device Tree Source 文本源文件,人类可读写 .dts / .dtsi
DTC Device Tree Compiler 编译器,把 DTS 转成 DTB 可执行程序
DTB Device Tree Blob 二进制目标文件,内核解析 .dtb

它们的流转关系很简单:你写 .dts 文件,用 dtc 工具编译成 .dtb,bootloader 把 dtb 加载到内存,内核启动时解析它。

我习惯在开发时加 -@ 选项编译:

dtc -I dts -O dtb -@ -o myboard.dtb myboard.dts

-@ 会生成符号节点,方便后续 overlay 动态加载。这个在调试阶段特别有用,后面讲故障定位时我会细说。

小技巧:用 dtc -I dtb -O dts 可以把 dtb 反编译回 dts。我经常用这招查看别人板子的硬件配置,或者确认自己编译出来的 dtb 有没有问题。

4.3 设备树的基本语法

设备树的语法其实不复杂。你把它想象成一棵倒着长的树,根节点是 /,下面挂 CPU、内存、外设。每个节点都有名字和属性。

4.3.1 节点(Node)

节点用 节点名@地址 表示。地址通常是寄存器基址,用来区分同名节点:

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

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            compatible = "arm,cortex-a7";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  // 1GB RAM
    };
};

注意 cpus 和 memory 是特殊节点,内核启动时会优先解析它们。其他外设节点,比如 uart、i2c、spi,都挂在根节点下或总线节点下。

4.3.2 属性(Property)

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

  • compatible:最重要的属性。内核通过它匹配驱动。格式是 "厂商,型号"。我建议至少写两个,第一个精确匹配,第二个做 fallback。
  • reg:描述地址空间。配合 #address-cells 和 #size-cells 使用。前者表示地址占几个 32 位单元,后者表示长度占几个。
  • interrupts:中断号。需要配合 interrupt-parent 指定中断控制器。
  • status:常用 "okay" 或 "disabled"。调试时我经常把不用的外设设成 "disabled",避免驱动冲突。

注意:reg 属性的长度单位是 #size-cells 指定的。如果 #size-cells = <1>,那 reg = <0x1000 0x200> 表示基址 0x1000,长度 0x200 字节。如果 #size-cells = <0>,那 reg 只表示地址,不表示长度——常见于中断控制器或只占用一个寄存器的设备。

4.3.3 标签(Label)

标签就是给节点起个别名,方便引用。语法是 标签名: 节点名@地址:

uart1: serial@10001000 {
    compatible = "vendor,uart";
    reg = <0x10001000 0x1000>;
    interrupts = <0 33 4>;
};

然后在其他地方用 &uart1 引用它。这在 .dtsi 文件中特别有用——公共芯片文件里定义节点,板级文件里通过标签追加属性:

// 在 board.dts 中
&uart1 {
    status = "okay";
    clock-frequency = <115200>;
};

我见过不少新手直接在 .dtsi 里改属性,结果一更新芯片文件就全丢了。用标签追加才是正确做法。

4.4 设备树与 ACPI 的对比

ACPI(高级配置与电源管理接口)是 x86 平台的硬件描述标准。很多从 x86 转过来的同学会问:设备树和 ACPI 到底啥区别?我画了张图帮大家理解:

设备树 vs ACPI 对比框架 设备树 (Device Tree) 嵌入式 / ARM / RISC-V 静态编译,启动时固定 源码开放,可读可改 驱动匹配:compatible 字符串 ACPI x86 / 服务器 / PC 运行时动态表,支持热插拔 二进制表,厂商固件提供 驱动匹配:HID / CID 字符串 两者本质都是硬件描述,但设计哲学不同:设备树偏静态嵌入式,ACPI 偏动态 PC 生态

我整理了一个更详细的对比表:

对比维度 设备树 (DT) ACPI
起源 Open Firmware,PowerPC 时代 1990 年代,Intel/Microsoft/Toshiba 联合制定
文件格式 文本 (.dts) → 二进制 (.dtb) ASL 源码 → AML 二进制表
修改方式 改 dts,重新编译 dtb,重启 改 DSDT/SSDT,需固件更新或 Override
电源管理 弱,需驱动自己实现 强,内置 _PS0/_PS3 等对象
热插拔 不支持原生热插拔描述 原生支持 PCIe/USB 热插拔事件
内核复杂度 低,解析器仅几千行 高,ACPI 子系统数万行
适用场景 嵌入式、IoT、ARM 服务器 PC、笔记本、企业级服务器

我的建议:做嵌入式开发,设备树是必修课。ACPI 可以了解,但不用深究。不过如果你做 ARM 服务器或者 RISC-V 的 ACPI 支持,那就另当别论了。我去年帮客户调试过一个 RISC-V 平台的 ACPI 适配,那真是……嗯,一言难尽,后面有机会再聊。

好了,这一章的核心就是:设备树是嵌入式 Linux 的硬件描述语言。你写 dts,编译 dtb,内核解析它,驱动匹配它。搞懂节点、属性、标签这三个基本概念,你就已经迈过了设备树的第一道门槛。