深入解析DTS、DTSO、DTBO的协作机制与动态加载实践

📅 发布时间:2026/7/3 17:44:44 👁️ 浏览次数:
深入解析DTS、DTSO、DTBO的协作机制与动态加载实践
1. 设备树技术入门从“说明书”到“乐高积木”大家好我是老张在嵌入式这行摸爬滚打十几年从早期的硬编码寄存器配置到如今主流的设备树Device Tree可以说是看着这套机制一步步成熟起来的。今天我们不聊那些深奥晦涩的理论就从一个嵌入式开发者的日常视角来聊聊DTS、DTSO和DTBO这三个文件到底是怎么一回事它们是怎么“打配合”的以及我们怎么在实际项目里玩转动态加载。你可以把整个设备树系统想象成组装一台电脑。DTS文件就是那本主板说明书上面白纸黑字写着主板上有什么CPU插槽、几个内存条插槽、PCIe通道怎么分配这些都是板上钉钉、出厂就焊死的硬件信息。这本说明书DTS需要被翻译成机器能直接看懂的二进制格式也就是DTB这就好比把厚厚的说明书压缩成一张小小的二维码系统启动时“扫一扫”就知道硬件家底了。那DTSO和DTBO又是干嘛的呢想象一下你现在想给这台电脑加装一块独立声卡。你不需要为了加声卡就去重写整个主板说明书对吧你只需要另一份“扩展说明书”上面写着“在第三个PCIe插槽上插入一块型号为XXX的声卡它的中断号是XX需要加载某某驱动”。这份“扩展说明书”的源码就是DTSO。同样它也需要编译成二进制格式DTBO也就是一张“声卡二维码”。系统启动后你可以随时把这张“声卡二维码”扫进去主板基础DTB就知道多了一个设备该干嘛干嘛去。这就是动态加载的核心魅力不改动基础固件就能灵活扩展或修改硬件配置。我见过很多刚接触设备树的朋友容易把DTSO和DTS的关系搞混认为必须把它们“揉在一起”编译才行。其实完全不是DTSO是一份独立的、只描述“差异”的源码它编译成DTBO的过程完全不需要知道基础DTS的存在。真正的“合并”动作是发生在系统运行时由内核的设备树覆盖Overlay框架来完成的。理解这一点是你玩转设备树动态覆盖的关键第一步。2. 庖丁解牛DTS、DTSO、DTBO的协作全景图光说比喻可能还不够直观我们直接上干货看看这三者在技术流程里具体是怎么串联起来的。2.1 核心文件定义与角色再澄清为了让大家印象更深我画个更接地气的表格来说明| 文件类型 | 全称 | 角色定位 | 生活化比喻 | 最终产出 | | :--- | | :--- | :--- | :--- | |DTS| Device Tree Source |基础架构师。定义SoC、内存、时钟等板上固定不变的硬件拓扑。 | 建筑的主体结构图和承重墙设计图。 | 编译为DTB(Device Tree Blob) | |DTSO| Device Tree Source Overlay |室内设计师。定义外设、传感器、GPIO复用等可变的、模块化的硬件配置。 | 装修方案图比如哪里加个插座哪个房间铺地板。 | 编译为DTBO(Device Tree Blob Overlay) | |DTBO| Device Tree Blob Overlay |施工队手里的装修清单。是DTSO编译后的二进制文件系统能直接加载并执行的“覆盖指令集”。 | 按图施工的标准化作业指导书。 | 无需再编译可直接被内核加载 |这里有个我踩过的坑务必提醒大家DTSO文件的开头必须包含/dts-v1/;和/plugin/;这两个指令。/plugin/这个标记特别关键它是在告诉编译器“喂老兄我这是个覆盖片段编译的时候你得给我生成那些用于动态绑定的符号和引用别把我当完整设备树来处理了” 少了它编译可能不报错但生成的DTBO加载时十有八九会失败。2.2 协作流程图解与“编译时” vs “运行时”很多资料里的流程图过于简化我结合自己的实践画一个更反映真实协作关系的图------------------- 编译 ------------------- | 基础DTS源码 | ----------- | 基础DTB二进制 | | (base.dts) | (dtc命令) | (base.dtb) | ------------------- ------------------- | | 系统启动时加载 | 作为硬件“基座” v ------------------- 编译 ------------------- 运行时动态叠加 ----------------------- | 覆盖DTSO源码 | ----------- | 覆盖DTBO二进制 | ------------------- | 最终生效的设备树 | | (overlay.dtso) | (dtc -) | (overlay.dtbo) | (写入sysfs接口) | (DTB DTBO) | ------------------- ------------------- -----------------------这个图清晰地揭示了一个最重要的事实DTS到DTB和DTSO到DTBO是两条独立的编译流水线。它们在“编译时”是井水不犯河水的。它们的交汇点是在“运行时”在内核的设备树覆盖框架中。这种解耦的设计带来了巨大的灵活性。比如一个硬件平台基础DTB可以对应多个不同功能的扩展模块多个DTBO出厂后由用户按需加载实现高度的定制化。3. 实战演练手把手完成编译与动态加载理论说得再多不如动手做一遍。咱们直接进入实战环节我会把每个步骤的细节和可能遇到的“坑”都讲明白。3.1 基础环境准备与工具链首先你得有设备树编译器dtc。现在主流的Linux发行版基本都自带用dtc -v检查一下。如果没有安装也很简单# Ubuntu/Debian sudo apt-get install device-tree-compiler # CentOS/RHEL sudo yum install dtc我建议至少使用1.4.7以上版本的dtc对覆盖语法的支持更完善。用dtc --version查看。准备两个文件就是我们刚才说的“主板说明书”和“声卡扩展说明书”1. 基础DTS文件 (base.dts)/dts-v1/; / { compatible my-company,my-board; #address-cells 1; #size-cells 1; soc { #address-cells 1; #size-cells 1; compatible simple-bus; ranges; // 假设这是一个I2C控制器默认禁用 i2c_controller: i2c40000000 { compatible vendor,generic-i2c; reg 0x40000000 0x1000; #address-cells 1; #size-cells 0; status disabled; // 注意初始状态是 disabled }; }; };这个文件定义了一个最简化的板子只有一个I2C控制器而且默认是关闭的。2. 覆盖DTSO文件 (overlay.dtso)/dts-v1/; /plugin/; // 关键指令声明这是一个覆盖插件 // 这个片段的目标是启用I2C控制器并挂载一个温度传感器 i2c_controller { // 使用 符号引用基础DTB中的节点标签 status okay; // 覆盖属性将状态改为启用 // 在I2C总线上添加一个新的设备节点 temperature_sensor: sensor48 { compatible ti,tmp102; reg 0x48; // I2C设备地址 // 可以添加更多属性比如中断引脚 // interrupts-extended gpio 16 1; }; };这个覆盖文件做了两件事一是启用那个被禁用的I2C控制器二是在该控制器下挂载一个具体的I2C设备。注意语法我们通过i2c_controller直接引用了基础DTS中定义的节点标签这是覆盖技术的核心语法之一。3.2 分步编译独立与协作步骤一编译基础DTBdtc -O dtb -o base.dtb base.dts这个命令很直接-O dtb指定输出格式为DTB二进制。生成的base.dtb就是你的“主板二维码”。步骤二独立编译覆盖DTBOdtc - -O dtb -o overlay.dtbo overlay.dtso这里多了个-参数。这个参数是编译DTSO文件时必须加上的它的作用是允许编译器处理未解析的符号引用。因为我们的overlay.dtso里用到了i2c_controller这个标签这个标签的定义在基础DTS里不在当前文件。-就是告诉dtc“先别管这个标签具体在哪生成一个带有占位符的DTBO等运行时再去找它绑定。” 少了-编译会报错“未定义的标签”。至此两个二进制文件base.dtb和overlay.dtbo已经独立生成完毕。你可以把overlay.dtbo拷贝到任何有相同基础硬件即base.dtb描述的平台的系统上使用无需重新编译基础DTS。3.3 动态加载让覆盖层生效在Linux系统中动态加载DTBO主要通过sysfs文件系统接口。这是最常用、最灵活的方式。确保内核支持你的内核需要配置了CONFIG_OF_OVERLAY。可以通过zcat /proc/config.gz | grep OF_OVERLAY或检查/boot/config-$(uname -r)文件来确认。加载操作# 切换到root用户 sudo su # 创建一个覆盖层目录名字可以自定义比如“my_sensor” mkdir /sys/kernel/config/device-tree/overlays/my_sensor # 将DTBO文件的内容写入该目录的dtbo文件 cat overlay.dtbo /sys/kernel/config/device-tree/overlays/my_sensor/dtbo如果操作成功这条命令执行后是没有输出的。此时你可以通过dmesg | tail查看内核日志应该能看到类似这样的成功信息[ 12.345678] of_overlay: Creating overlay my_sensor [ 12.345690] of_overlay: overlay: #0 fragment0 [ 12.345700] of_overlay: overlay: node i2c40000000 - new status: okay [ 12.345710] of_overlay: overlay: adding node sensor48 under i2c40000000验证效果# 查看I2C总线是否出现了新设备 ls /sys/bus/i2c/devices/ # 你应该能看到类似 0-0048 这样的设备节点0是总线号48是设备地址 # 或者直接查看设备树 cat /proc/device-tree/soc/i2c40000000/status # 输出应该是 okay cat /proc/device-tree/soc/i2c40000000/sensor48/compatible # 输出应该是 ti,tmp102卸载覆盖层# 非常简单直接删除创建的目录即可 rmdir /sys/kernel/config/device-tree/overlays/my_sensor卸载后由该覆盖层添加的节点和修改的属性会被移除系统状态回退到加载覆盖层之前。4. 进阶技巧与避坑指南掌握了基本流程我们来看看一些更实际的场景和容易踩的坑。4.1 处理标签与引用__overlay__与__fixups__在更复杂的覆盖文件中你可能会看到fragmentX和__overlay__的写法。这其实是DTSO语法的一种等价形式。我们之前的简化写法i2c_controller { ... }编译器在处理时内部就是把它转换成这种格式。有时候你需要覆盖的节点在基础DTB中没有标签没有label:只有完整路径。这时就必须使用fragment语法并通过target-path来指定目标。例如/dts-v1/; /plugin/; / { fragment0 { target {/soc/i2c40000000}; // 使用路径引用注意 和花括号 __overlay__ { status okay; some_new_property value; }; }; };编译后你可以用fdtdump overlay.dtbo命令查看DTBO内容会发现一个叫__fixups__的节点。这里面记录的就是需要运行时解析的符号引用列表。这是-参数起作用的关键证据。4.2 常见错误与排查手段在我带项目的过程中新手加载DTBO失败90%集中在以下几个问题基础DTB中目标节点不存在或路径错误。症状加载DTBO时内核日志报错OF: overlay: target node not found。排查先用fdtdump base.dtb仔细检查基础设备树的结构确认你要覆盖的节点路径或标签名完全正确。注意大小写和地址如i2c40000000中的地址。DTSO文件语法错误或缺少/plugin/。症状编译DTSO时可能报错或者编译通过但加载失败内核日志报语法相关错误。排查用dtc -I dtb -O dts overlay.dtbo反编译你生成的DTBO文件看看结构是否和你预期的一致。确保DTSO第一行是/dts-v1/;第二行是/plugin/;。属性冲突或合并规则不理解。场景基础DTB中某个属性是status okay;你的覆盖文件里又写了一个status disabled;那么最终结果是覆盖层生效变为disabled。但如果基础节点有一个gpios gpio 10 0;的属性覆盖层想添加第二个GPIO不能直接写gpios gpio 11 0;这会完全替换。对于这种数组属性需要在覆盖层中重新列出全部值。建议对于复杂的属性覆盖务必查阅内核文档中关于设备树覆盖的属性合并规则。内核配置或版本问题。症状/sys/kernel/config/device-tree/overlays目录根本不存在。排查确认内核编译时开启了CONFIG_OF_OVERLAY。一些较旧或裁剪过的内核如某些嵌入式定制内核可能未包含此功能。4.3 动态加载的应用场景工厂模式与产品变体最后分享两个我觉得非常实用的高级应用场景。场景一工厂测试模式。我们有一款智能硬件产品基础DTB描述了核心板。在工厂生产线上需要测试Wi-Fi、蓝牙、多个传感器等。如果把这些外设全部写进基础DTB并启用会拖慢启动速度且某些模块冲突。我们的做法是基础DTB只包含最简配置。产线工装PC上存放着test-wifi.dtbo,test-bluetooth.dtbo,test-all-sensors.dtbo等文件。每个工位根据测试项通过脚本自动加载对应的DTBO。测试失败时直接rmdir卸载覆盖层硬件状态复位干净利落。场景二管理多个硬件变体。公司基于同一款核心板衍生出“基础版”只有I2C传感器、“高级版”多一个SPI屏幕、“旗舰版”再加一个PCIe网卡三个产品。如果维护三个完全不同的DTS文件后期同步核心板修改会非常痛苦。我们的解决方案是一个通用基础DTB。然后为每个变体制作一个DTBOvariant-basic.dtbo: 启用I2C传感器。variant-advanced.dtbo: 启用I2C传感器 SPI屏幕。variant-flagship.dtbo: 启用I2C传感器 SPI屏幕 PCIe网卡。在批量生产时只需在烧录系统后根据产品型号将对应的DTBO文件重命名为user.dtbo并放到固定分区。系统启动脚本自动加载这个user.dtbo。这样硬件差异化的管理变得模块化、可插拔极大降低了维护成本。设备树覆盖技术本质上是一种“声明式”的硬件配置管理。它把硬件描述从僵化的内核镜像中解耦出来给了我们运行时动态调整的巨大自由。刚开始可能会觉得它的语法和加载方式有点绕但一旦掌握你就会发现它在嵌入式产品开发、尤其是需要灵活适配多种硬件配置的场景下是一个不可或缺的利器。多动手写几个例子多看看内核输出的日志信息遇到问题按部就班地排查很快就能得心应手。