4. DTS覆盖(Overlay)原理:DTBO的工作原理、overlay dtbo与base dtb的合并机制

设备树覆盖(Device Tree Overlay,DTO)是Linux内核支持的一种动态修改设备树的技术。在Android多板型开发中,它允许将基础设备树(Base DTB)与覆盖设备树(DTBO)在运行时合并,从而实现“一套基础配置,多板型差异化适配”的目标。本章深入剖析DTBO的工作原理、合并机制及内核中的实现细节。

核心概念: DTS覆盖不是修改原始DTS源文件,而是在编译后的二进制DTB层面进行片段(fragment)的叠加。每个fragment包含一个目标路径(target)和覆盖属性(__overlay__),内核在启动时或运行时将这些片段合并到基础DTB的对应节点中。

4.1 DTBO文件格式与结构

DTBO文件是经过dtc编译器编译后的二进制覆盖文件,其内部结构与标准DTB类似,但包含特殊的覆盖标记。一个典型的DTBO文件由多个fragment组成,每个fragment对应一个独立的覆盖操作。

字段 描述 示例值
fragment@0 第一个覆盖片段,包含target和__overlay__ target = &i2c0
target 指定要覆盖的基础DTB节点路径或phandle引用 <0xffffffff> 或 &i2c0
__overlay__ 包含要添加或修改的属性/子节点 status = "okay";
__fixups__ 记录需要延迟解析的符号引用(可选) i2c0 = /fragment@0/__overlay__:target
__local_fixups__ 记录本地phandle重定位信息 phandle偏移量列表
关键区别: 基础DTB中的节点是静态存在的,而DTBO中的节点通过fragment包裹,内核在合并时才会“激活”这些覆盖内容。未合并前,DTBO中的节点对系统不可见。

4.2 覆盖合并机制:从DTBO到完整DTB

DTBO与Base DTB的合并过程分为编译时和运行时两个阶段。编译时由dtc工具完成符号解析和phandle重定位;运行时由内核的of_overlay_apply()函数执行实际的节点合并。

DTBO与Base DTB合并机制流程图 Base DTB 基础硬件配置 DTBO文件 覆盖片段集合 加载到内存 内核覆盖管理器 (of_overlay_apply) 解析fragment → 查找target → 合并__overlay__ 合并过程详细步骤 ① 遍历DTBO中的每个fragment ② 解析target字段,定位Base DTB中的目标节点 ③ 将__overlay__中的属性/子节点复制到目标节点 ④ 处理phandle重定位,更新符号引用 完整运行时DTB

4.3 合并过程中的关键操作

内核在合并DTBO时,执行以下三个核心操作:

  1. 符号解析(Symbol Resolution):通过__fixups__和__local_fixups__字段,将DTBO中引用的外部符号(如&i2c0)解析为Base DTB中的实际phandle值。
  2. 节点合并(Node Merge):将fragment中__overlay__下的所有属性(properties)和子节点(child nodes)递归地添加到目标节点中。如果属性已存在,则覆盖;如果子节点已存在,则递归合并。
  3. phandle重定位(Phandle Relocation):当DTBO中定义了新的节点并分配了phandle时,内核需要确保这些phandle不与Base DTB中的现有phandle冲突,并更新所有引用这些phandle的路径。
注意事项: 合并操作是不可逆的。一旦DTBO被应用,无法通过卸载DTBO来恢复原始状态(除非内核支持动态撤销,但Android中通常不支持)。因此,在应用覆盖前,必须确保所有依赖关系正确。

4.4 覆盖语法与DTS示例

在DTS源文件中,覆盖通过/dts-v1/;和/plugin/;指令声明。以下是一个典型的DTBO源文件示例:

// i2c-overlay.dts
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &i2c0;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            touchscreen@38 {
                compatible = "edt,edt-ft5406";
                reg = <0x38>;
                interrupt-parent = <&gpio1>;
                interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
            };
        };
    };

    fragment@1 {
        target = &uart2;
        __overlay__ {
            status = "disabled";
        };
    };
};

上述示例中,fragment@0向i2c0节点添加了一个触摸屏子节点,并启用该I2C控制器;fragment@1则禁用了uart2节点。编译时,dtc会生成对应的__fixups__段,记录i2c0、gpio1等符号的引用位置。

4.5 合并后的设备树状态

合并完成后,内核中的设备树表现为一个统一的、完整的树结构。以下表格对比了合并前后的关键差异:

特性 合并前(Base DTB) 合并后(完整DTB)
节点完整性 缺少覆盖节点(如touchscreen) 包含所有覆盖节点和属性
phandle空间 仅包含基础phandle 包含基础+覆盖phandle,无冲突
符号引用 无外部引用 所有引用已解析为实际phandle
内存占用 较小 增加覆盖部分的大小
驱动匹配 只能匹配基础设备 可以匹配覆盖新增的设备

4.6 内核中的覆盖应用流程

在Android系统中,DTBO的加载和应用通常由bootloader或内核初始化代码完成。以下是标准流程:

  1. Bootloader阶段:读取分区中的DTBO镜像,将其加载到内存中,并将地址传递给内核。
  2. 内核初始化:在start_kernel()早期,调用unflatten_device_tree()解析Base DTB。
  3. 覆盖应用:调用of_overlay_fdt_apply(),传入DTBO内存地址,触发合并。
  4. 驱动探测:合并后的设备树被用于驱动匹配,覆盖新增的设备节点被对应的驱动识别并初始化。
性能影响: 覆盖合并操作仅在系统启动时执行一次,对运行时性能无影响。合并过程的时间复杂度为O(n*m),其中n为fragment数量,m为每个fragment中的节点数量。对于典型的Android设备(10-20个fragment),合并时间通常在微秒级别。

4.7 覆盖与基础DTB的依赖关系

DTBO对Base DTB存在强依赖。如果Base DTB中缺少DTBO引用的节点(如i2c0),合并将失败。内核通过以下机制确保依赖正确:

  • 编译时检查:dtc在编译DTBO时,如果发现target引用的符号在Base DTB中不存在,会报错并终止编译。
  • 运行时验证:内核在合并前,会检查每个fragment的target是否有效。如果target为0xffffffff(无效phandle),则跳过该fragment并打印警告。
  • 版本兼容性:Android要求DTBO与Base DTB使用相同的dts-v1格式,且内核版本支持覆盖功能(Linux 4.4+)。
覆盖依赖关系与错误处理 Base DTB DTBO 合并成功 target无效 合并失败 依赖检查:DTBO中所有target必须在Base DTB中存在,否则合并失败并回滚

4.8 实际应用中的覆盖策略

在Android多板型项目中,DTS覆盖通常遵循以下策略:

  • 基础DTB:包含SoC公共外设、内存映射、时钟、中断控制器等所有板型共享的配置。
  • 板型DTBO:每个板型一个独立的DTBO文件,包含该板型特有的外设(如触摸屏型号、传感器、显示屏参数)、GPIO复用配置、电源管理差异等。
  • 运行时选择:Bootloader根据硬件ID(如board_id、revision寄存器)选择对应的DTBO加载。
最佳实践: 将不变的配置放入Base DTB,将可变的配置放入DTBO。例如,SoC内部总线、DMA控制器、时钟树等几乎不会变化的配置放在Base中;而外设型号、引脚复用、电压调节等因板型而异的配置放在DTBO中。这样可以最大化复用,减少维护成本。