一、Overlay概念与背景:为什么需要动态加载?传统设备树的局限性

各位同学,咱们今天聊点实在的。

做嵌入式Linux开发,设备树(Device Tree)这东西,大家肯定不陌生。但说实话,我在早期做项目时,对设备树的态度就是——写一次,烧进去,再也不动。直到有一次,我被一个硬件改版折腾得够呛,才真正意识到:传统设备树,其实挺死板的。

1.1 传统设备树的工作方式

传统设备树,说白了就是一个描述硬件信息的静态数据结构。它把CPU、内存、外设、中断、GPIO这些东西,统统用树形节点表示出来。内核启动时,Bootloader把设备树二进制(DTB)传给内核,内核解析后,就知道该怎么初始化硬件了。

流程大概是这样的:

设备树源文件(.dts) → 编译 → 设备树二进制(.dtb) → Bootloader加载 → 内核解析 → 硬件初始化

嗯,看起来挺顺的。但问题在哪呢?

问题就在于——这个DTB是固定的。一旦编译好,硬件就不能变了。你想想看,如果产品有多个硬件版本,或者用户想插个扩展板,怎么办?

核心痛点:传统设备树是静态的、全局的、一次性的。它不支持运行时修改,也不支持模块化组合。

1.2 我在项目中踩过的坑

我记得有一次,做一个工业控制板。主板上有几个扩展接口,可以接不同的功能模块——有的接摄像头,有的接传感器,有的接电机驱动。硬件设计上,这些模块共用同一组引脚,通过I2C或SPI通信。

按传统做法,我得把所有可能的硬件配置都写到一个dts文件里。结果呢?

  • dts文件变得巨长,光节点就几百个
  • 很多外设其实根本不会同时存在
  • 每次硬件改版,都得重新编译整个设备树
  • 最要命的是——引脚冲突。两个模块共用同一组引脚,但dts里同时使能了,内核直接崩掉

我当时就在想:能不能像Linux内核模块那样,需要什么硬件,就动态加载什么设备树片段?

嗯,这就是Overlay的由来。

1.3 Overlay是什么?

设备树Overlay,说白了就是设备树的“补丁”或“插件”。它允许你在系统运行时,动态地添加、修改或删除设备树中的节点。

打个比方:

  • 传统设备树 = 一张完整的建筑蓝图,盖好就不能改
  • Overlay = 乐高积木,你可以随时拼一块新的上去

Overlay的编译和加载流程:

Overlay源文件(.dts) → 编译 → Overlay二进制(.dtbo) → 内核动态加载 → 硬件生效

注意看,这里多了一个“动态加载”的步骤。而且Overlay文件是.dtbo后缀,不是.dtb

1.4 为什么需要动态加载?

我总结了一下,主要有这几个场景:

场景 传统做法 Overlay做法
FPGA动态重配置 必须提前在dts里预留所有可能的外设 FPGA加载后,动态添加对应的设备树节点
扩展板/子卡 每个扩展板都得重新编译整个设备树 插上扩展板,加载对应的Overlay即可
硬件版本差异 用多个dts文件,编译多个dtb 一个基础dtb + 多个Overlay,按需组合
热插拔外设 不支持,必须重启 支持,动态加载/卸载
调试与开发 改一次硬件,烧一次固件 直接加载Overlay,不用重启

说白了,Overlay解决的就是“硬件变化”这个核心问题。你不需要为每一种硬件组合都准备一个完整的设备树,只需要准备一个基础设备树,再加上若干个Overlay片段就行。

1.5 传统设备树的局限性

咱们再深入聊聊,传统设备树到底有哪些局限?

  1. 静态性:设备树在编译时就固定了,运行时不能改。内核启动后,你再想加一个I2C设备?对不起,不行。
  2. 全局性:一个dts文件描述整个板子的硬件。如果两个外设共用引脚,dts里必须手动处理冲突,否则编译都过不了。
  3. 不可组合:没法把多个独立的设备树片段拼在一起。比如A模块的dts和B模块的dts,不能直接合并,因为可能有节点ID冲突。
  4. 调试困难:改一次硬件,就得重新编译、烧写、重启。我当年调试一个SPI设备,一天重启了上百次,人都麻了。
  5. 维护成本高:产品线多了,每个硬件版本都得维护一个完整的dts文件。稍微改个引脚,就得开分支。

注意:传统设备树并不是一无是处。对于固定硬件的产品(比如手机、路由器),它依然是最佳选择。Overlay主要解决的是硬件可变的场景。

1.6 Overlay的核心思想

Overlay的核心思想其实很简单:分而治之,动态组合

你把一个完整的设备树,拆成两部分:

  • 基础设备树(Base DTB):描述板上固定的硬件,比如CPU、内存、时钟、串口等
  • Overlay片段(DTBO):描述可变的、可插拔的硬件,比如扩展板、FPGA外设等

系统启动时,先加载基础设备树。然后,根据实际硬件情况,动态加载一个或多个Overlay。Overlay可以添加新节点,也可以修改已有节点(比如修改引脚复用、调整时序参数)。

我画了一张图,帮你理解这个关系:

基础设备树 (Base DTB) CPU / 内存 / 时钟 串口 / GPIO控制器 固定硬件描述 Overlay 1 摄像头模块 I2C + 中断 Overlay 2 传感器模块 SPI + GPIO Overlay 3 电机驱动 PWM + 中断 Linux 内核 动态加载 Overlay 设备树Overlay动态加载架构 基础设备树 + 多个Overlay片段 → 按需组合 → 动态加载到内核

你看,基础设备树是固定的,Overlay是可选的。系统启动时,内核先加载基础设备树。然后,根据实际插了什么硬件,再加载对应的Overlay。这样,一个基础设备树就能应对多种硬件组合。

我的建议:刚开始接触Overlay时,别想得太复杂。你就把它当成“设备树的补丁文件”。基础设备树是主文件,Overlay是补丁。补丁可以随时打上,也可以随时撤销。

1.7 一个简单的例子

咱们来看一个最简单的Overlay长什么样:

// 基础设备树片段 (base.dts)
/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard";

    i2c0: i2c@ff000000 {
        compatible = "mycompany,i2c";
        reg = <0xff000000 0x1000>;
        status = "okay";
    };
};

// Overlay片段 (overlay.dts)
/dts-v1/;
/plugin/;

&i2c0 {
    temperature-sensor@48 {
        compatible = "mycompany,tmp102";
        reg = <0x48>;
    };
};

注意看,Overlay文件开头多了个/plugin/;,这是告诉编译器:这是一个Overlay,不是完整的设备树。然后通过&i2c0引用基础设备树中的节点,往里面添加子节点。

编译命令也不一样:

# 编译基础设备树
dtc -I dts -O dtb -o base.dtb base.dts

# 编译Overlay
dtc -I dts -O dtb -o overlay.dtbo overlay.dts

Overlay编译出来是.dtbo后缀,不是.dtb。这个细节要注意,我当年就因为这个后缀搞错过,内核死活不认。

1.8 小结

好了,咱们把这一章的核心内容捋一捋:

  • 传统设备树是静态的、全局的、一次性的,适合固定硬件
  • Overlay是设备树的动态补丁,允许运行时添加/修改硬件描述
  • 核心价值:解决硬件可变、模块化组合、热插拔、调试效率等问题
  • 使用方式:基础DTB + 多个DTBO,按需加载

下一章,咱们会深入Overlay的语法细节,看看/plugin/;到底做了什么,以及如何正确地编写Overlay文件。嗯,到时候我会分享一些我在项目中踩过的坑,保证让你少走弯路。


专注资料整理