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()函数执行实际的节点合并。
4.3 合并过程中的关键操作
内核在合并DTBO时,执行以下三个核心操作:
- 符号解析(Symbol Resolution):通过
__fixups__和__local_fixups__字段,将DTBO中引用的外部符号(如&i2c0)解析为Base DTB中的实际phandle值。 - 节点合并(Node Merge):将fragment中
__overlay__下的所有属性(properties)和子节点(child nodes)递归地添加到目标节点中。如果属性已存在,则覆盖;如果子节点已存在,则递归合并。 - 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或内核初始化代码完成。以下是标准流程:
- Bootloader阶段:读取分区中的DTBO镜像,将其加载到内存中,并将地址传递给内核。
- 内核初始化:在
start_kernel()早期,调用unflatten_device_tree()解析Base DTB。 - 覆盖应用:调用
of_overlay_fdt_apply(),传入DTBO内存地址,触发合并。 - 驱动探测:合并后的设备树被用于驱动匹配,覆盖新增的设备节点被对应的驱动识别并初始化。
性能影响: 覆盖合并操作仅在系统启动时执行一次,对运行时性能无影响。合并过程的时间复杂度为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+)。
4.8 实际应用中的覆盖策略
在Android多板型项目中,DTS覆盖通常遵循以下策略:
- 基础DTB:包含SoC公共外设、内存映射、时钟、中断控制器等所有板型共享的配置。
- 板型DTBO:每个板型一个独立的DTBO文件,包含该板型特有的外设(如触摸屏型号、传感器、显示屏参数)、GPIO复用配置、电源管理差异等。
- 运行时选择:Bootloader根据硬件ID(如board_id、revision寄存器)选择对应的DTBO加载。
最佳实践: 将不变的配置放入Base DTB,将可变的配置放入DTBO。例如,SoC内部总线、DMA控制器、时钟树等几乎不会变化的配置放在Base中;而外设型号、引脚复用、电压调节等因板型而异的配置放在DTBO中。这样可以最大化复用,减少维护成本。