1. 设备树基础概念

1.1 什么是设备树 (Device Tree)

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,它使用节点(node)和属性(property)的树形层次来完整描述一块电路板上所有可发现的硬件组件及其连接关系。设备树源文件(.dts)经过设备树编译器(dtc)编译后生成设备树二进制文件(.dtb),由 Bootloader 传递给内核。

核心定义:设备树是一个与体系结构无关的、描述硬件拓扑的扁平化数据模型,它替代了传统 Linux 内核中大量硬编码的 arch/arm/mach-* 板级文件。

一个典型的设备树节点结构如下:

/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 {
        status = "okay";
        pinctrl-0 = <&uart0_pins>;
    };
};
关键术语速查:
  • DTS — Device Tree Source,设备树源文件,人类可读的文本格式。
  • DTB — Device Tree Blob,设备树二进制文件,内核实际加载的格式。
  • DTC — Device Tree Compiler,将 DTS 编译为 DTB 的工具。
  • DTBO — Device Tree Blob Overlay,设备树叠加层,用于动态修改基础 DTB。
设备树文件编译与加载流程 .dts 源文件 .dtsi 头文件 DTC 编译器 .dtb 二进制 .dtbo 叠加层 Bootloader 加载 内核解析 DTB 驱动匹配 & 初始化

1.2 为什么需要设备树

在设备树出现之前(Linux 2.6 ~ 3.x 早期),ARM 架构的 Linux 内核维护着大量板级文件(arch/arm/mach-*),每个开发板都需要在 board-xxx.c 中硬编码硬件资源。这种模式带来了严重问题:

对比维度 传统板级文件方式 设备树方式
硬件描述位置 内核源码 arch/arm/mach-* 独立的 .dts 文件
新增板卡成本 需新增 board-xxx.c + 注册平台设备 仅需编写 .dts 文件
内核主线合入 大量冗余代码,维护者负担重 硬件描述与驱动分离,合入门槛低
多平台兼容 同一 SoC 不同板卡需不同内核镜像 同一内核镜像 + 不同 DTB 即可
硬件变更响应 需重新编译内核 仅需重新编译 DTB
历史教训:在 ARM Linux 社区,板级文件数量曾超过 600 个,每次内核版本升级都需要同步修改大量 board 文件,导致“一次编译,到处调试”的困境。设备树的引入彻底终结了这种混乱局面。

设备树带来的核心收益可归纳为三点:

  1. 硬件描述与驱动代码解耦 — 驱动开发者只需关注 compatible 匹配,无需关心具体板级布局。
  2. 单一内核镜像支持多板卡 — Bootloader 根据实际硬件选择对应的 DTB 传递给内核。
  3. 硬件配置动态化 — 通过 DTB Overlay 机制,可以在运行时动态添加或修改硬件节点。

1.3 设备树在 Linux 内核中的角色

设备树在内核启动和运行过程中扮演着“硬件数据库”的角色,其生命周期贯穿整个内核运行阶段:

设备树在内核中的角色与数据流 Bootloader U-Boot / Barebox 内核初始化阶段 unflatten_device_tree() 驱动模型 platform_driver 传递 DTB 地址 of_* API 访问 展开的设备树(struct device_node 树) 资源描述(reg/irq) 引脚配置(pinctrl) 时钟 & 电源管理 内核通过 of_* 系列 API(of_property_read_*、of_irq_get 等)查询设备树节点

设备树在内核中的具体角色体现在以下几个关键环节:

内核阶段 设备树参与方式 关键函数 / 机制
启动早期 Bootloader 将 DTB 物理地址写入 r2 寄存器(ARM)或通过 devicetree 参数传递 early_init_dt_scan()
内存初始化 扫描 /memory 节点获取内存布局 early_init_dt_scan_memory()
中断控制器 扫描 /interrupt-controller 节点注册中断域 irqchip_of_init()
时钟框架 扫描 /clocks 节点注册时钟提供者 of_clk_init()
平台设备创建 将 compatible 匹配的节点转换为 platform_device of_platform_populate()
驱动匹配 驱动通过 of_match_table 与设备树节点匹配 platform_match() → of_match_node()
运行时 驱动通过 of_* API 读取资源、配置引脚、获取时钟 of_property_read_u32() / of_iomap()
核心理解:设备树不是“驱动”,也不是“内核子系统”,它是内核与硬件之间的契约层。驱动通过 compatible 字符串声明自己能支持的硬件,设备树通过节点描述实际存在的硬件,内核负责将两者匹配起来。这种“声明式”设计使得硬件变更不再需要修改内核代码。

从数据结构角度看,内核将扁平化的 DTB 解析为一棵由 struct device_node 和 struct property 组成的树形结构:

// 内核中设备树节点的核心数据结构
struct device_node {
    const char *name;           // 节点名称(不含 unit-address)
    const char *type;           // device_type 属性值
    phandle phandle;            // 节点的 phandle 值
    const char *full_name;      // 完整路径名,如 "/soc/i2c@1000"
    struct property *properties; // 属性链表
    struct device_node *parent;  // 父节点
    struct device_node *child;   // 第一个子节点
    struct device_node *sibling; // 兄弟节点
    // ...
};

struct property {
    char *name;                 // 属性名,如 "compatible"
    int length;                 // 属性值长度(字节)
    void *value;                // 属性值指针
    struct property *next;      // 下一个属性
    // ...
};
排查要点:在调试设备树问题时,以下三个方向是最高频的故障点:
  • DTB 加载失败 — Bootloader 未正确传递 DTB 地址,或 DTB 被损坏。
  • 节点匹配失败 — 驱动 of_match_table 中的 compatible 与 DTS 中的值不一致。
  • 资源获取失败 — reg 属性格式错误、interrupts 描述与中断控制器不匹配。