设备树 Overlay 机制实战详解:运行时动态加载硬件配置而不重新编译内核的方案
设备树 Overlay 机制实战详解:运行时动态加载硬件配置而不重新编译内核的方案
一、每换一个外设就重编一次内核的噩梦:设备树静态描述的根本局限
在嵌入式 Linux 产品开发中,一个令人头疼的问题是:同一款 SOC 的不同应用板卡,外设配置完全不同。以瑞芯微 RK3568 为例,客户 A 的板卡使用了 SPI1 连接 CAN 控制器、客户 B 的板卡将 SPI1 用于连接 TFT 屏幕——但内核只编译了一个rk3568-evb.dtb。传统的解决方案是维护多个 DTS 文件,每个都对应一块具体板卡的完整硬件描述。当板卡种类超过 10 种时,DTS 的维护成本线性增长。
设备树 Overlay 机制从 Linux 4.4 内核开始引入,允许在主设备树(base DT)已经加载后,在运行时动态地添加、修改或删除设备树节点。这一机制最初的设计目标是为 BeagleBone 的 Capes 扩展板服务,但现在已广泛应用于各种嵌入式 Linux 平台。核心思想类似于面向对象编程中的"继承+覆盖":base DT 定义 SOC 级别的公共硬件,overlay 仅描述差异部分。
二、Overlay 的编译、加载与合并机制:从 .dts 片段到 /proc/device-tree 的全路径
设备树 overlay 的编译和加载过程涉及不同于普通 DT 的处理流程。
flowchart TB A[Overlay 源文件<br/>my_board_overlay.dts] B[dtc -@ 编译<br/>生成 __symbols__ 节点] C[my_board_overlay.dtbo<br/>DT overlay blob] D[Base 设备树<br/>rk3568-base.dtb] C --> E["用户空间加载: echo overlay.dtbo > /sys/kernel/config/device-tree/overlays/add"] E --> F["内核 of_overlay_fdt_apply()<br/>解析 __fixups__ 和 __local_fixups__"] F --> G["符号解析: 根据 __symbols__ 查找 target 节点"] G --> H["属性合并: overlay 属性覆盖 base 属性"] H --> I["生成新的子节点<br/>/proc/device-tree/ 更新的节点可见"] D --> G subgraph 错误处理路径 H -->|"phandle 冲突"| J["of_overlay_apply() 返回 -EINVAL<br/>overlay 加载失败,base DT 不受影响"] H -->|"内存不足"| K["kmalloc 失败, 回滚已创建的节点"] end编译 overlay 与普通 DT 的关键区别在于必须使用dtc -@参数。-@启用符号表生成(__symbols__节点),这使得 overlay 中的target = <&spi1>这类符号引用能够被内核解析。不带-@编译的 .dtbo 文件没有符号表,加载时会静默失败——设备节点不出现,但没有任何错误提示。
加载机制的核心是of_overlay_fdt_apply()函数。该函数执行以下步骤:
- 解析 FDT 格式的 overlay blob,提取
__fixups__节点(定义对 base DT 符号的引用关系) - 根据
__symbols__表解析每个 fixup 的目标 phandle - 对目标节点执行属性合并:overlay 中声明的属性覆盖 base DT 中同名属性;overlay 中新增的属性直接添加
- 调用
of_changeset_apply()原子性地应用所有修改
关键在于步骤 3 和 4 之间的原子性保证:如果 overlay 中的任何节点无法成功应用(phandle 冲突、内存不足),整个 changeset 回滚,base DT 不会受到任何污染。
三、完整的 Overlay 开发工作流:从编写 .dts 到运行时动态加载
3.1 Overlay 源文件编写
/* * rk3568_can_overlay.dts —— 为 RK3568 基板添加 CAN 总线支持 * * Overlay 编写要点: * 1. 必须包含 /plugin/; 声明(告诉 dtc 这是 overlay 而非完整 DT) * 2. target 使用符号引用(&label),而非硬编码 phandle * 3. 需要开启的状态用 status = "okay" * 4. 需要关闭的状态用 status = "disabled" */ /dts-v1/; /plugin/; /* 关键标记:声明此文件为 overlay */ / { /* * fragment@0: 启用在 base DT 中默认关闭的 SPI1 控制器 * target-path 方式:用于没有 label 的节点 */ fragment@0 { target-path = "/soc/spi@fe620000"; __overlay__ { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi1m1_csn1 &spi1m1_pins>; }; }; /* * fragment@1: 在 SPI1 总线下添加 CAN 控制器子节点 * target 方式:使用 symbol label(推荐,更清晰) */ fragment@1 { target = <&spi1>; __overlay__ { #address-cells = <1>; #size-cells = <0>; can0: mcp2518fd@0 { compatible = "microchip,mcp2518fd"; reg = <0>; /* SPI 片选 CS0 */ spi-max-frequency = <20000000>; /* 20MHz */ interrupts-extended = <&gpio3 RK_PC0 IRQ_TYPE_LEVEL_LOW>; clocks = <&can0_osc>; status = "okay"; }; }; }; /* * fragment@2: 添加 CAN 控制器所需的固定频率时钟 * 全局性资源可以挂在根节点下 */ fragment@2 { target-path = "/"; __overlay__ { can0_osc: can0-oscillator { compatible = "fixed-clock"; #clock-cells = <0>; clock-frequency = <40000000>; /* 40MHz */ }; }; }; /* * fragment@3: 修改 pinmux 配置 —— * 将原本用于 UART2 的 GPIO1_B0/B1 改为 SPI1 的 CS1 和功能引脚 * 注意:这可能使 UART2 不可用,需要确认硬件不使用 UART2 */ fragment@3 { target = <&pinctrl>; __overlay__ { spi1_pins: spi1-pins { rockchip,pins = /* SPI1_CLK */ <1 RK_PB1 0 &pcfg_pull_none>, /* SPI1_MOSI */ <1 RK_PB0 0 &pcfg_pull_none>, /* SPI1_MISO */ <1 RK_PA7 0 &pcfg_pull_none>, /* SPI1_CS1 */ <1 RK_PB2 0 &pcfg_pull_none>; }; }; }; };3.2 编译与加载脚本
#!/bin/bash # ============================================================ # overlay_deploy.sh —— Overlay 编译、验证、加载全流程 # 依赖: device-tree-compiler (dtc) 包 # 内核需开启 CONFIG_OF_OVERLAY 编译选项 # ============================================================ set -e OVERLAY_SRC="rk3568_can_overlay.dts" OVERLAY_DTBO="rk3568_can_overlay.dtbo" CONFIGFS_PATH="/sys/kernel/config/device-tree/overlays" # ---- Step 1: 编译 Overlay ---- echo "[1/5] 编译 overlay..." dtc -@ -I dts -O dtb -o "$OVERLAY_DTBO" "$OVERLAY_SRC" # 参数说明: # -@ : 生成符号表 (__symbols__),这是 overlay 能解析 target 引用的关键 # -I dts : 输入格式为设备树源文件 # -O dtb : 输出格式为设备树二进制 if [ $? -ne 0 ]; then echo "错误: 编译失败,请检查 .dts 语法" exit 1 fi # ---- Step 2: 语法验证 ---- echo "[2/5] 验证 overlay 语法..." # fdtdump 可检查 dtbo 文件的内部结构是否合法 fdtdump "$OVERLAY_DTBO" > /dev/null 2>&1 || { echo "错误: 生成的 dtbo 文件格式异常" exit 1 } # ---- Step 3: 对比加载前后的设备树差异 ---- echo "[3/5] 记录当前设备树状态..." DT_DIFF_BEFORE=$(mktemp) find /proc/device-tree/ -type f -exec sh -c \ 'echo -n "{}:"; xxd -p "{}" 2>/dev/null || echo "N/A"' \; \ | sort > "$DT_DIFF_BEFORE" # ---- Step 4: 加载 Overlay ---- echo "[4/5] 加载 overlay 到内核..." # 确保 configfs 已挂载 if ! mountpoint -q /sys/kernel/config 2>/dev/null; then mount -t configfs none /sys/kernel/config fi # 创建 overlay 加载目录 mkdir -p "$CONFIGFS_PATH" # 将 dtbo 内容写入 configfs 触发内核加载 # dtc 命令输出的 dtb 格式通过 cat 传入 cat "$OVERLAY_DTBO" > "$CONFIGFS_PATH/can_overlay/dtbo" 2>&1 || { echo "错误: overlay 加载失败" echo "可能原因:" echo " - 内核未编译 CONFIG_OF_OVERLAY=y" echo " - target 符号在 base DT 中不存在" echo " - phandle 冲突" exit 1 } # ---- Step 5: 验证加载结果 ---- echo "[5/5] 验证 overlay 是否成功应用..." # 查找 CAN 设备节点是否出现 if find /proc/device-tree/ -name "mcp2518fd*" 2>/dev/null | grep -q .; then echo "成功: CAN 控制器节点已创建" # 列出新增节点 find /proc/device-tree/ -name "mcp2518fd*" 2>/dev/null else echo "警告: 未在 /proc/device-tree 中找到 CAN 节点" echo "可能是 overlay 加载成功但设备名不同" fi # 检查 SPI1 状态是否已变为 "okay" SPI1_STATUS=$(cat /proc/device-tree/soc/spi@fe620000/status 2>/dev/null || echo "N/A") echo "SPI1 控制器状态: $SPI1_STATUS" # 记录加载后的设备树差异 DT_DIFF_AFTER=$(mktemp) find /proc/device-tree/ -type f -exec sh -c \ 'echo -n "{}:"; xxd -p "{}" 2>/dev/null || echo "N/A"' \; \ | sort > "$DT_DIFF_AFTER" echo "--- 设备树变化 ---" diff "$DT_DIFF_BEFORE" "$DT_DIFF_AFTER" || true rm -f "$DT_DIFF_BEFORE" "$DT_DIFF_AFTER"3.3 卸载 Overlay
# 卸载已加载的 overlay # 机制:移除 configfs 目录后,内核自动调用 of_overlay_remove() rmdir /sys/kernel/config/device-tree/overlays/can_overlay # 验证:SPI1 控制器应回到 disabled 状态 cat /proc/device-tree/soc/spi@fe620000/status # 期望输出:disabled四、Overlay 机制的适用边界与工程陷阱
Phandle 冲突是最隐蔽的陷阱。Overlay 中声明的节点如果引用了一个在 base DT 中不存在或在 overlay 之间冲突的 phandle,of_overlay_fdt_apply()返回-EINVAL但没有任何描述性错误信息。调试的唯一手段是启用CONFIG_OF_DYNAMIC和CONFIG_OF_OVERLAY的内核调试日志。
Pinmux 冲突无法自动解决。当两块 overlay 同时声明了对同一组 GPIO 引脚的互斥功能(如一个声明为 SPI CLK,另一个声明为 I2C SCL),内核不会检测冲突——两个 overlay 都会成功加载,但实际硬件引脚上会产生电气冲突。这在可插拔扩展板场景中是一个真实风险。
Overlay 不支持修改已有节点的基本属性。compatible字符串、reg地址范围、interrupts等关键属性在节点创建后不可通过 overlay 修改——overlay 只能修改声明在__overlay__子节点内的属性。删除已有节点同样不被支持(overlay 只能添加或修改,不能删除)。
适用场景:
- 同一 SOC 的多种板卡变体(最常见的使用场景)
- 运行时动态加载/卸载外设(如 USB 转 SPI/CAN 适配器)
- FPGA 部分重配置场景(FPGA 比特流加载后对应更新设备树)
不适用场景:
- 需要在不同厂商的 SOC 之间切换(base DT 不同,overlay 不跨平台兼容)
- 需要修改 CPU 核心频率、DDR 时序等 SOC 级基础配置(必须在 base DT 中定义)
五、总结
设备树 Overlay 机制解决了嵌入式 Linux 平台"同一 SOC、多种板卡"场景下的硬件描述复用问题。核心知识点:
- Overlay 编译必须使用
dtc -@,缺少符号表是加载静默失败的最常见原因。 /plugin/;声明是 overlay 文件的语法标志,告知 dtc 该文件将被作为 overlay 处理。- 加载通过 configfs 接口,将 .dtbo 写入
/sys/kernel/config/device-tree/overlays/<name>/dtbo触发内核处理。 - Phandle 冲突和 Pinmux 冲突不会产生显式错误,需要人工审查 overlay 之间的交互关系。
- Overlay 支持添加和修改节点,不支持删除节点或修改已创建节点的核心属性。
