1. 设备树基础:什么是设备树(DTS)、为什么需要设备树、设备树在Linux内核中的角色

1.1 什么是设备树(Device Tree)

设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来记录系统中所有可访问的设备、总线、中断控制器、时钟、GPIO、内存地址等硬件信息。设备树源文件(.dts)经过编译生成设备树二进制文件(.dtb),在系统启动时由引导加载程序传递给内核。

核心定义:设备树是一个硬件描述文件,它将板级硬件细节从内核代码中剥离,使同一份内核镜像可以支持多种硬件平台。

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

/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";
            device_type = "cpu";
            reg = <0x0>;
        };
    };

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

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};
术语速查:DTS(Device Tree Source)→ 设备树源文件;DTB(Device Tree Blob)→ 设备树二进制文件;DTC(Device Tree Compiler)→ 设备树编译器;DTBO(Device Tree Blob Overlay)→ 设备树覆盖二进制文件。

1.2 为什么需要设备树

在设备树出现之前,Linux 内核使用大量的板级文件(board-*.c)来注册硬件信息。这种方式导致内核代码膨胀、维护困难、不同板卡之间代码重复严重。设备树的引入解决了以下核心问题:

对比维度 传统板级文件方式 设备树方式
硬件描述位置 硬编码在 C 代码中 独立的外部描述文件
内核镜像通用性 每块板卡需编译专用内核 同一内核镜像适配多板
硬件变更代价 修改内核源码并重新编译 仅修改 DTS 文件并重新编译
代码维护成本 高,大量重复代码 低,结构清晰可复用
热插拔支持 不支持动态添加设备 支持 Overlay 动态加载
社区主流架构 ARM 32 位早期版本 ARM64、RISC-V、x86 等
关键转折:Linux 内核主线从 2011 年(v3.0 左右)开始强制要求 ARM 架构使用设备树,拒绝接受新的板级文件。这一决策彻底改变了嵌入式 Linux 的硬件适配方式。

设备树带来的直接收益包括:

  • 内核与硬件解耦:硬件描述独立于内核代码,板卡厂商只需提供 DTS 文件。
  • 二进制兼容:同一份 zImage 配合不同的 .dtb 即可启动不同硬件。
  • 社区协作效率:SoC 核心 IP 的 DTSI 文件可复用,板级差异仅体现在 DTS 中。
  • 动态覆盖能力:通过 Overlay 机制,支持运行时添加/移除设备,为热插拔驱动奠定基础。

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

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

  1. 启动阶段:Bootloader(如 U-Boot)将 DTB 加载到内存,并将 DTB 物理地址传递给内核。
  2. 内核早期初始化:内核解压后,setup_arch() 函数解析 DTB,建立内存映射、CPU 拓扑、中断控制器等基础信息。
  3. 驱动匹配与设备注册:内核遍历 DTB 中的节点,通过 compatible 属性匹配驱动,自动创建设备对象。
  4. 运行时资源查询:驱动通过 OF(Open Firmware)API 读取 DTB 中的寄存器地址、中断号、DMA 通道、时钟频率等资源。
  5. 动态覆盖(Overlay):支持在系统运行后加载 DTBO 文件,动态添加或修改设备节点,实现热插拔外设驱动。
核心角色总结:设备树是内核与硬件之间的“翻译官”,它将硬件描述标准化、外部化、动态化,使 Linux 内核能够以统一的方式管理千差万别的嵌入式硬件。
设备树在 Linux 内核中的角色与数据流 硬件描述层 DTS 源文件 .dts / .dtsi DTC 编译器 dtc 工具 DTB 二进制 .dtb 文件 DTBO 覆盖 .dtbo 文件 Bootloader 加载 DTB 到内存 传递地址给内核 内核处理层 setup_arch() 早期解析 DTB unflatten_device_tree() 展开为 device_node 树 of_platform_populate() 创建 platform_device 驱动匹配与 probe compatible 属性匹配 Overlay 处理 of_overlay_apply() 动态添加/移除节点 驱动与用户空间 OF API 调用 of_property_read_*() 中断/GPIO/时钟 资源申请与映射 设备文件接口 /dev/xxx 节点 sysfs 导出 /sys/firmware/devicetree 用户空间工具 dtc / configfs libfdt 库 传递 DTB 设备注册 Overlay 加载

1.4 设备树的核心数据结构

内核中使用 struct device_node 和 struct property 两个核心结构体来管理设备树:

// 设备节点结构体
struct device_node {
    const char *name;           // 节点名称
    const char *type;           // device_type 属性
    phandle phandle;            // 节点句柄
    const char *full_name;      // 完整路径名
    struct property *properties; // 属性链表
    struct device_node *parent;  // 父节点
    struct device_node *child;   // 子节点
    struct device_node *sibling; // 兄弟节点
    ...
};

// 属性结构体
struct property {
    char *name;                 // 属性名
    int length;                 // 属性值长度
    void *value;                // 属性值指针
    struct property *next;      // 下一个属性
    ...
};
关键 API 速查:驱动开发中常用的 OF API 包括 of_find_node_by_path()、of_property_read_u32()、of_iomap()、irq_of_parse_and_map()、of_get_named_gpio() 等,这些函数构成了驱动与设备树之间的桥梁。

1.5 设备树与驱动匹配机制

Linux 内核通过 compatible 属性实现设备与驱动的匹配。匹配过程遵循以下优先级:

匹配方式 匹配依据 示例 优先级
compatible 精确匹配 驱动 of_match_table 中的第一个 compatible 值 "ti,am3352-uart" 最高
compatible 兼容匹配 驱动 of_match_table 中的后续 compatible 值 "ti,omap3-uart" 次高
设备名称匹配 节点 name 属性与驱动 id_table 匹配 "serial" 中
设备类型匹配 节点 device_type 属性匹配 "cpu" 低
实战要点:编写设备树时,compatible 属性应遵循“具体在前,通用在后”的原则。例如 "ti,am3352-uart", "ti,omap3-uart",这样既能精确匹配特定型号,又能兼容同系列驱动。