嵌入式Linux设备树覆盖overlay动态配置实战 - 第1节

1.1 什么是设备树(Device Tree)

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,它使用树形节点来记录处理器、内存、外设、中断、时钟、GPIO 等硬件信息。在嵌入式 Linux 系统中,设备树替代了传统平台代码(board file)中硬编码的硬件描述,成为内核与硬件之间的标准接口。

核心定义:设备树是一个由节点(node)和属性(property)组成的树状结构,每个节点代表一个硬件设备或总线,属性描述该设备的配置参数(如寄存器地址、中断号、时钟频率等)。

设备树在系统中的位置

设备树文件(.dts)经过编译生成设备树二进制文件(.dtb),由 bootloader(如 U-Boot)传递给内核。内核在启动阶段解析 .dtb,动态创建平台设备(platform device)并注册驱动,从而完成硬件初始化。

设备树在嵌入式系统中的位置与数据流 硬件设计 (SoC / 板级) .dts 源文件 (文本描述) dtc 编译器 (.dts → .dtb) .dtb 二进制 (扁平化设备树) Bootloader (U-Boot 加载) Linux 内核 (解析 & 驱动注册) 说明:.dts 源文件由硬件工程师或 BSP 开发者编写,dtc 编译为 .dtb,Bootloader 将 .dtb 传递给内核。 内核通过设备树 API 读取节点属性,自动匹配驱动并完成硬件初始化。

1.2 为什么需要设备树

在设备树出现之前,嵌入式 Linux 内核使用大量 board file(板级文件)来硬编码硬件信息。每个板子都需要在 arch/arm/mach-xxx 目录下编写数百行 C 代码,导致内核代码冗余、维护困难、升级缓慢。设备树解决了以下核心问题:

问题 传统方式(board file) 设备树方式
硬件描述耦合 硬件信息硬编码在 C 代码中,修改需重新编译内核 硬件描述与内核代码分离,修改 .dts 后只需编译 .dtb
多板支持 每个板子一个 board file,内核镜像膨胀 同一内核镜像可加载不同 .dtb,支持多板
内核升级 board file 随内核版本变化,需持续适配 设备树绑定稳定,.dts 可跨内核版本复用
社区维护 ARM 社区被 board file 淹没,维护成本极高 设备树统一硬件描述,社区聚焦驱动开发
动态配置 不支持运行时硬件变更 设备树覆盖(Overlay)支持热插拔与动态配置
关键收益:设备树使得“一次编译内核,多处部署”成为可能。Bootloader 根据实际板型选择对应的 .dtb 文件,内核无需重新编译即可适配不同硬件。

1.3 设备树源文件(.dts)基本语法与结构

.dts 文件采用树形结构,以 节点(node) 和 属性(property) 为核心元素。每个节点以 /dts-v1/; 开头,根节点用 / { ... }; 表示,子节点嵌套在父节点内部。

1.3.1 基本语法规则

  • 节点定义:node-name@unit-address { ... };,其中 unit-address 通常为设备基地址。
  • 属性赋值:property-name = <value>;,支持整数、字符串、字节数组、phandle 等类型。
  • 标签(label):label: node-name { ... };,用于在其它位置引用节点。
  • 包含文件:#include "soc.dtsi",.dtsi 为公共头文件,类似 C 语言的 .h 文件。
  • 注释:支持 /* ... */ 和 // ... 风格。

1.3.2 典型 .dts 文件结构

/dts-v1/;

#include "imx6ull.dtsi"

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

    chosen {
        stdout-path = &uart1;
    };

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

    leds {
        compatible = "gpio-leds";
        led0: led-red {
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            label = "red-led";
        };
    };

    &uart1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

1.3.3 节点与属性详解

元素 语法示例 说明
根节点 / { ... }; 设备树的顶层容器,所有设备节点都在其内
子节点 uart1: serial@02020000 { ... }; uart1 为标签,serial@02020000 为节点名+地址
属性:reg reg = <0x02020000 0x4000>; 描述设备地址范围(起始地址 + 长度)
属性:compatible compatible = "fsl,imx6ull-uart", "ns16550"; 驱动匹配字符串,内核据此查找驱动
属性:interrupts interrupts = <0 58 IRQ_TYPE_LEVEL_HIGH>; 中断号、触发类型,依赖 interrupt-parent
属性:status status = "okay"; 或 "disabled" 控制设备是否启用,常用于覆盖(overlay)
引用节点 &uart1 { ... }; 通过标签引用节点,追加或修改属性

1.3.4 设备树覆盖(Overlay)的语法基础

设备树覆盖允许在运行时动态添加或修改节点,其语法与 .dts 基本一致,但需要额外包含 /plugin/; 声明,并使用 &node 引用目标节点:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &uart1;
        __overlay__ {
            status = "okay";
            baudrate = <115200>;
        };
    };
};
注意:覆盖文件必须使用 /plugin/; 标记,且每个 fragment 指定 target 和 __overlay__ 段。这是后续章节动态配置的核心机制。
设备树节点层级结构示例 /(根节点) chosen memory@80000000 soc uart1@02020000 i2c1@021a0000 gpio1@0209c000 说明:根节点下包含 chosen、memory、soc 等子节点;soc 节点下挂载 uart、i2c、gpio 等外设节点。 每个节点通过 compatible 属性与驱动绑定,reg 属性描述地址空间。

1.3.5 常用设备树 API(内核视角)

驱动开发者通过以下 API 从设备树中读取硬件信息:

API 函数 功能 返回值
of_find_node_by_name() 根据节点名查找节点 struct device_node *
of_property_read_u32() 读取 32 位整数属性 int(0 成功)
of_property_read_string() 读取字符串属性 int(0 成功)
of_get_property() 获取任意类型属性指针 const void *
of_irq_parse_one() 解析中断描述 int(0 成功)
of_iomap() 将 reg 地址映射到虚拟地址 void __iomem *
实战提示:编写设备树时,优先查阅内核文档 Documentation/devicetree/bindings/ 目录下的绑定文档,确保 compatible 字符串与驱动完全匹配。错误的 compatible 会导致驱动无法加载。