2. 设备树基础

设备树(Device Tree)是 Linux 内核用于描述硬件平台信息的一种数据结构。它把“板级硬件长什么样”从内核源码中剥离出来,交给独立的二进制数据文件表达。内核启动时读取这份数据,动态识别 CPU、内存、总线、外设、中断控制器、时钟、引脚等资源,从而做到“同一份内核镜像,适配多种硬件板卡”。理解 DTS、DTSI、DTB、DTC 四个基本概念,是后续跨平台迁移工作的起点。

一、四个基本概念与它们的关系

设备树在开发与运行阶段以不同形态存在,四者构成一条清晰的“源码 → 编译 → 二进制 → 内核解析”链路:

  • DTS(Device Tree Source):设备树源文件,通常以 .dts 结尾,描述某一块具体板卡的硬件。它是人类可读的文本,包含根节点、CPU、内存、外设节点及其属性。
  • DTSI(Device Tree Source Include):设备树包含文件,通常以 .dtsi 结尾,用于存放多个板卡共用的部分,例如 SoC 内部外设定义。DTS 通过 #include 引入 DTSI,实现“公共部分复用 + 板级差异覆盖”。
  • DTC(Device Tree Compiler):设备树编译器,把 DTS/DTSI 编译成 DTB,也可反向把 DTB 反编译为 DTS,是迁移调试中最常用的工具。
  • DTB(Device Tree Blob):编译产物,扁平化二进制设备树,由 Bootloader 加载并传递给内核,内核在启动早期解析它。

它们的关系可以概括为:DTS + DTSI 经 DTC 编译生成 DTB,DTB 由 Bootloader 传给内核,内核据此完成平台设备初始化。下图展示了这一完整链路及关键工具位置。

设备树源码到内核解析的完整链路 DTS 板级源文件 board.dts DTSI SoC 公共定义 soc.dtsi DTC 设备树编译器 dtc -I dts -O dtb 也可反编译 dtb→dts DTB 扁平化二进制 board.dtb 由 Bootloader 加载 Linux 内核启动早期 解析 DTB → 建立 platform_device / 匹配 driver → 初始化外设

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

在引入设备树之前,ARM Linux 内核中充斥着大量 board-xxx.c 板级文件,每支持一块新板就要改内核、重新编译。设备树改变了这一模式,其作用集中在启动早期:

  1. Bootloader 阶段:U-Boot 等引导程序把 DTB 加载到内存,并在跳转内核时通过寄存器(如 ARM64 的 x0)或 ATAGS 传递 DTB 地址。
  2. 内核解压与早期初始化:内核解析 DTB,读取 /memory 节点确定物理内存布局,读取 /cpus 确定 CPU 数量与特性。
  3. 平台设备创建:内核遍历 DTB 中 compatible 属性,把每个节点转换为 platform_device,并与已注册的 platform_driver 匹配。
  4. 驱动 probe:匹配成功后调用驱动的 probe 函数,完成寄存器映射、中断申请、时钟与引脚配置,外设正式可用。
设备树的核心价值:内核不再“认识”具体板卡,只认识设备树描述的硬件拓扑。硬件差异被下沉到 DTB,内核镜像得以通用。

三、DTS/DTSI 的组织方式与常用语法

实际项目中,DTS 通常只写板级差异,公共部分放在 DTSI。下面是一个典型结构:

// soc.dtsi —— SoC 公共定义
/ {
    compatible = "vendor,soc";
    #address-cells = <1>;
    #size-cells = <1>;

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <10>;
        status = "disabled";
    };
};

// board-a.dts —— 板级差异
#include "soc.dtsi"

/ {
    model = "Board A";
    compatible = "vendor,board-a", "vendor,soc";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };
};

&uart0 {
    status = "okay";
    clock-frequency = <100000000>;
};

其中 &uart0 是“引用并覆盖”语法,用于在板级文件中修改 DTSI 里已定义的节点。这种“公共 + 覆盖”的写法,正是跨平台迁移时最需要掌握的组织方式。

四、DTC 常用命令与调试手段

DTC 是迁移过程中最常用的工具,掌握以下命令即可完成大部分编译与排障:

命令 作用 典型场景
dtc -I dts -O dtb -o board.dtb board.dts DTS 编译为 DTB 构建板级设备树
dtc -I dtb -O dts -o board.dts board.dtb DTB 反编译为 DTS 分析厂商提供的二进制设备树
fdtdump board.dtb 查看 DTB 头部与结构 确认 magic、版本、大小
ls /proc/device-tree/ 查看内核实际解析结果 验证节点是否生效
cat /proc/device-tree/model 读取板卡型号 确认加载了正确 DTB

五、跨平台迁移的意义与典型场景

跨平台迁移的本质,是把一套已经跑通的设备树工程,从 A 平台搬到 B 平台。设备树机制让这件事变得可控:

  • 内核复用:同一内核镜像可同时支持多块板卡,只需更换 DTB,无需重新编译内核。
  • 差异隔离:SoC 公共部分放 DTSI,板级差异放 DTS,迁移时主要修改 DTS 和少量 DTSI 覆盖。
  • 快速验证:通过 DTC 反编译对比两块板卡的 DTB,可快速定位节点、属性、地址差异。
  • 降低维护成本:硬件变更不再牵动内核代码,驱动与板级描述解耦。
迁移时优先对比三样东西:compatible 字符串、reg 地址范围、interrupts 中断号。这三者不一致,往往是驱动 probe 失败的首要原因。

六、常见误区与复盘

初学者在设备树迁移中容易踩以下坑:

  1. 把 DTS 当 C 代码写:DTS 没有变量、循环和函数,只有节点、属性与引用,语法错误会直接导致 DTC 编译失败。
  2. 忽略 status 属性:DTSI 中外设默认 disabled,板级 DTS 必须显式改为 okay,否则驱动不会 probe。
  3. 地址与中断号照搬:不同 SoC 的寄存器基地址和中断号不同,直接复制会导致驱动读写错误地址。
  4. 只改 DTS 不改 DTSI:若公共 DTSI 中已固定某外设参数,板级覆盖必须使用 &label 引用方式,否则不生效。
  5. 不验证实际加载结果:编译通过不等于内核解析正确,务必查看 /proc/device-tree/ 确认节点存在。
迁移完成后,建议用 dtc 把新旧两块板卡的 DTB 都反编译成 DTS,做一次结构化 diff。差异清单就是迁移改动清单,也是后续回归测试的重点。