1. 设备树基础概念:DTS 的起源、作用与基本语法结构

1.1 设备树的起源:从“硬编码”到“描述式”的演进

在早期的嵌入式 Linux 系统中,板级硬件信息(如内存地址、外设寄存器基址、中断号、GPIO 引脚分配)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每更换一款硬件平台,就必须修改内核源码并重新编译,导致内核镜像与硬件高度耦合,维护成本急剧上升。

为了解决这一问题,PowerPC 社区率先提出了 设备树(Device Tree) 的概念,随后被 ARM、RISC-V、x86 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核代码中剥离出来,以结构化数据的形式独立存在,内核在启动时解析该数据,动态构建设备模型。

对比维度 传统板级文件方式 设备树方式
硬件描述位置 内核源码 arch/arm/mach-xxx 独立的 .dts / .dtsi 文件
硬件变更影响 需修改内核源码并重新编译 仅修改设备树文件,内核无需重编译
多板支持 每个板子一个内核镜像 同一内核镜像 + 不同设备树文件
驱动与硬件耦合度 高(驱动直接引用板级宏定义) 低(驱动通过设备树节点匹配)
关键理解: 设备树不是“驱动”,而是硬件的“数据清单”。驱动代码通过设备树节点中的属性值来获取硬件参数,实现“数据与逻辑分离”。

1.2 设备树的作用:硬件资源的“结构化清单”

设备树在嵌入式 Linux 启动流程中扮演着 硬件信息传递者 的角色。Bootloader(如 U-Boot)将设备树二进制文件(.dtb)加载到内存,并将首地址传递给内核。内核通过 unflatten_device_tree() 函数将其解析为树状数据结构,供驱动子系统使用。

设备树主要描述以下硬件资源:

  • CPU 信息:核心数量、架构、时钟频率、缓存大小。
  • 内存布局:物理内存起始地址与大小。
  • 总线与外设:I2C、SPI、UART、MMC、USB 等控制器及其从设备。
  • 中断控制器:中断控制器类型、中断号映射、中断触发方式。
  • GPIO 控制器与引脚复用:GPIO 控制器基址、引脚功能配置。
  • 时钟与复位:时钟源、分频系数、复位信号连接。
  • 电源管理:电压调节器、电源域、唤醒源。
设备树在嵌入式 Linux 启动流程中的角色 硬件板 CPU / 内存 / 外设 Bootloader 加载 .dtb 到内存 设备树 (.dtb) 结构化硬件描述 Linux 内核 解析设备树 创建设备模型 硬件探测 生成 .dtb 传递 DTB 地址 说明: 设备树文件(.dts)经过 Device Tree Compiler(dtc)编译为二进制 .dtb 文件。 Bootloader 负责将 .dtb 加载到内存,并通过特定寄存器(如 r2)将地址传递给内核。

1.3 设备树的基本语法结构

设备树源文件(.dts)采用树形层次结构,以 节点(node) 和 属性(property) 为核心元素。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。

1.3.1 节点定义与层次关系

/dts-v1/;

/ {
    model = "Embedded Linux Board v1.0";
    compatible = "vendor,board-v1", "vendor,board";

    #address-cells = <1>;
    #size-cells = <1>;

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

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

        uart0: serial@10000000 {
            compatible = "vendor,uart";
            reg = <0x10000000 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        i2c0: i2c@10001000 {
            compatible = "vendor,i2c";
            reg = <0x10001000 0x1000>;
            interrupts = <0 34 4>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            eeprom@50 {
                compatible = "atmel,24c02";
                reg = <0x50>;
                pagesize = <8>;
            };
        };
    };
};

1.3.2 核心语法要素

语法元素 格式 说明
根节点 / { ... }; 设备树的顶层容器,所有设备节点都位于其下
子节点 node-name@unit-address node-name 为节点名称,unit-address 为设备基址(可选)
标签(label) uart0: serial@10000000 为节点定义别名,便于在其它地方引用或覆盖
属性 property-name = value; 描述节点特性,值可以是字符串、整数、数组、二进制数据等
reg 属性 reg = <address size>; 描述设备地址空间,由 #address-cells 和 #size-cells 决定单元长度
compatible 属性 compatible = "vendor,device"; 驱动匹配的关键字符串,内核通过该属性查找对应驱动
interrupts 属性 interrupts = <type irq flags>; 描述中断连接,具体格式由中断控制器定义
status 属性 status = "okay" / "disabled"; 控制设备是否启用,常用于覆盖机制中动态修改

1.3.3 属性值类型速查

  • 字符串:compatible = "vendor,device";
  • 32 位整数:reg = <0x10000000 0x1000>;
  • 64 位整数:reg = <0x00000000 0x80000000 0x00000000 0x10000000>;(需配合 #address-cells = <2>)
  • 字符串列表:compatible = "vendor,device", "generic,device";
  • 二进制数据:mac-address = [00 11 22 33 44 55];
  • 布尔值:dma-coherent;(仅存在即表示 true)
实践要点: 编写设备树时,compatible 属性应遵循“具体在前,通用在后”的原则。例如 "vendor,board-v1", "vendor,board",内核会按顺序匹配驱动,优先使用最具体的驱动。

1.4 设备树如何描述硬件资源:一个完整的实例

以下是一个典型的嵌入式 SoC 设备树片段,展示了如何描述 CPU、内存、中断控制器、GPIO 和 I2C 总线等核心硬件资源:

/dts-v1/;

/ {
    model = "Cortex-A7 Development Board";
    compatible = "myvendor,devboard";

    #address-cells = <1>;
    #size-cells = <1>;

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

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

        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a7";
            reg = <0x1>;
            clock-frequency = <1000000000>;
        };
    };

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

    intc: interrupt-controller@1c00000 {
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0x01c00000 0x1000>,
              <0x01c01000 0x1000>;
    };

    gpio0: gpio@1c20000 {
        compatible = "myvendor,gpio";
        reg = <0x1c20000 0x1000>;
        interrupts = <0 45 4>;
        #gpio-cells = <2>;
        gpio-controller;
    };

    i2c0: i2c@1c30000 {
        compatible = "myvendor,i2c";
        reg = <0x1c30000 0x1000>;
        interrupts = <0 46 4>;
        #address-cells = <1>;
        #size-cells = <0>;
        clock-frequency = <100000>;
        status = "okay";

        temp_sensor: sensor@48 {
            compatible = "national,lm75";
            reg = <0x48>;
        };
    };
};
设备树节点层次结构示例 / (根节点) cpus memory@80000000 intc gpio0 i2c0 cpu@0 cpu@1 temp_sensor 层次关系说明: • 根节点 / 包含所有硬件资源,是设备树的唯一入口。 • cpus 节点下包含多个 cpu 子节点,每个 cpu 节点描述一个核心。 • i2c0 节点下挂载 temp_sensor 从设备,体现总线与设备的从属关系。

1.5 设备树覆盖机制的引入背景

在实际嵌入式项目中,经常遇到以下场景:

  • 硬件变体:同一款 SoC 被用于多个不同外设配置的板卡。
  • 扩展模块:通过接插件连接不同的子板(如摄像头、显示屏、传感器模块)。
  • 运行时配置:系统启动后根据硬件检测结果动态启用或禁用某些设备。

如果为每种组合都维护一份完整的 .dts 文件,会导致文件数量爆炸式增长,且维护困难。设备树覆盖(Device Tree Overlay) 机制应运而生:它允许在基础设备树(base DTB)之上,动态加载或卸载增量修改片段(.dtbo),实现硬件描述的模块化与运行时灵活性。