2. 设备树基础

在嵌入式 Linux 的早期阶段,板级硬件信息几乎全部以 C 代码的形式硬编码在内核源码中。以 ARM 架构为例,arch/arm/mach-xxx/ 目录下充斥着大量 board-xxx.c 文件,每个文件描述一块特定开发板的串口地址、I2C 控制器、GPIO 引脚、时钟频率等。这种做法带来三个严重问题:一是内核源码树被大量板级文件污染,二是同一 SoC 的不同板卡需要复制粘贴大量重复代码,三是硬件变更必须重新编译内核。设备树(Device Tree)正是为解决这些问题而引入的硬件描述机制。

2.1 历史背景:从 OpenFirmware 到 ARM 的全面采用

设备树并非 Linux 原创,它最早源自 OpenFirmware(IEEE 1275)标准,用于在 PowerPC 平台上描述硬件。2005 年前后,PowerPC 社区已经普遍使用设备树,而 ARM 社区仍坚持硬编码方式。随着 ARM SoC 数量爆发式增长,内核维护者 Linus Torvalds 在 2011 年发出著名邮件,明确要求 ARM 平台必须转向设备树,否则拒绝合并新的板级代码。此后,ARM Linux 全面采用设备树,mach-xxx 目录逐步被清理,硬件描述从内核代码中剥离出来,成为独立的、可被 bootloader 传递的数据结构。

这一转变的核心价值在于:内核与硬件描述解耦。同一份内核镜像可以支持多块板卡,只要它们使用相同的 SoC 架构;硬件差异通过不同的设备树二进制文件在启动时注入。

2.2 DTS、DTC、DTB 三者关系

设备树在工程实践中涉及三种形态,理解它们的关系是掌握设备树的第一步:

  • DTS(Device Tree Source):人类可读的文本源文件,通常以 .dts 为后缀,描述板级硬件。复杂 SoC 会拆分为 .dtsi(include 文件)供多块板卡复用。
  • DTC(Device Tree Compiler):编译工具,将 DTS 编译为 DTB,也可将 DTB 反编译回 DTS。内核源码中的 scripts/dtc/ 即其实现。
  • DTB(Device Tree Blob):二进制格式,由 bootloader(如 U-Boot)在启动时传递给内核,内核解析后生成 device_node 与 platform_device。

三者的转换关系可以用下面的信号链图清晰表达:

DTS 设备树源文件 .dts / .dtsi DTC 设备树编译器 scripts/dtc DTB 设备树二进制 .dtb Linux 内核 解析并展开设备节点 编译 输出 bootloader 传递 反编译 dtc -I dtb -O dts

典型编译命令如下:

# 编译 DTS 为 DTB
dtc -I dts -O dtb -o board.dtb board.dts

# 反编译 DTB 为 DTS,用于调试
dtc -I dtb -O dts -o board_decompiled.dts board.dtb

# 内核构建系统中通常使用
make ARCH=arm CROSS_COMPILE=arm-linux- dtbs

2.3 设备树在 Linux 启动流程中的作用

设备树在启动流程中扮演“硬件清单”的角色。以 ARM 平台为例,典型流程如下:

  1. BootROM 加载 bootloader(如 U-Boot)到内存并运行。
  2. U-Boot 将内核镜像(zImage/Image)与 DTB 加载到内存指定地址。
  3. U-Boot 通过 bootz 或 bootm 命令跳转内核,同时将 DTB 地址写入寄存器(如 ARM 的 x0 或 r2)。
  4. 内核启动早期,setup_arch() 调用 unflatten_device_tree() 解析 DTB,生成 device_node 树。
  5. 内核根据设备树中的 compatible 属性匹配驱动,将节点转换为 platform_device,触发驱动 probe()。
关键点:设备树只描述“有什么硬件、地址是多少、如何连接”,不描述“驱动如何工作”。驱动逻辑仍在 C 代码中,设备树负责告诉内核“去哪里找硬件”。

2.4 基本术语与核心概念

设备树采用树形结构,每个节点描述一个设备或总线。以下术语必须掌握:

术语 含义 示例
node 设备树中的一个节点,代表一个设备或总线 uart0: serial@101f1000
property 节点的属性,描述硬件参数 compatible = "arm,pl011";
compatible 驱动匹配的关键属性,格式为“厂商,型号” "ti,omap4-uart"
reg 寄存器地址与长度,格式为 <地址 长度> reg = <0x101f1000 0x1000>;
interrupts 中断号与触发类型 interrupts = <0 12 4>;
clocks 时钟源引用 clocks = <&clk_uart0>;
phandle 节点引用标识,用于跨节点引用 &uart0 编译后生成唯一 phandle
status 设备使能状态 status = "okay"; 或 "disabled";

2.5 案例:一块虚拟开发板的设备树片段

下面是一个简化的 DTS 片段,描述一个 SoC 上的 UART0 与 I2C 控制器:

/dts-v1/;
/ {
    model = "MyBoard v1.0";
    compatible = "myvendor,myboard", "myvendor,mysoC";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;

        uart0: serial@101f1000 {
            compatible = "arm,pl011";
            reg = <0x101f1000 0x1000>;
            interrupts = <0 12 4>;
            clocks = <&clk_uart0>;
            status = "okay";
        };

        i2c0: i2c@101f2000 {
            compatible = "arm,pl031-i2c";
            reg = <0x101f2000 0x1000>;
            clock-frequency = <100000>;
            status = "disabled";
        };
    };
};

内核启动后,可以通过 /proc/device-tree/ 查看解析结果:

# 查看根节点 compatible
cat /proc/device-tree/compatible

# 查看 uart0 的 reg 属性(二进制需 hexdump)
hexdump -C /proc/device-tree/soc/serial@101f1000/reg

# 查看所有设备树节点
ls /proc/device-tree/soc/

2.6 常见误区与复盘

误区一:设备树能替代驱动。设备树只描述硬件资源,驱动仍需实现读写逻辑。没有对应驱动,节点不会被 probe。
误区二:DTS 中的地址就是物理地址。对于有 IOMMU 或总线地址转换的系统,reg 中的地址可能是总线地址,需结合 ranges 属性理解。
误区三:修改 DTS 后忘记重新编译 DTB。内核启动使用的是 DTB,不是 DTS。修改后必须重新执行 make dtbs 并更新 bootloader 加载的 DTB 文件。
复盘建议:调试设备树问题时,优先使用 dtc 反编译实际加载的 DTB,确认内核看到的节点与预期一致;再结合 dmesg | grep -i of_ 查看内核解析日志,定位 compatible 匹配失败或 reg 地址冲突等问题。

掌握 DTS、DTC、DTB 三者的转换关系,理解设备树在启动流程中的传递与解析时机,并熟悉 compatible、reg、interrupts 等核心属性,是后续进行板级资源管理与驱动适配的基础。设备树不是“配置文件”,而是内核与硬件之间的契约,契约的准确性直接决定驱动能否正确加载。