Linux 设备树深度解析之设备树覆盖层 (Overlays)
第1章 设备树使用模型
1.1 什么是设备树
/** * @brief 设备树的本质定义 * * 设备树 (Devicetree, DT) 是一种描述硬件的数据结构和语言。 * 它是一个由命名节点组成的树状无环图,每个节点可以有任意数量的 * 命名属性来封装任意数据。 * * 核心设计理念:让操作系统在运行时发现硬件拓扑,而不是在编译时硬编码。 * * 三个主要用途: * 1. 平台识别 (Platform Identification) * 2. 运行时配置 (Runtime Configuration) * 3. 设备填充 (Device Population) * * @note 绑定 (bindings) 是一组约定,定义了如何在树中描述典型硬件特性。 * 尽可能使用现有绑定,避免重复造轮子。 */
1.2 历史渊源
设备树最初由 Open Firmware 创建,用于从固件向客户端程序(操作系统)传递数据。PowerPC 和 SPARC 平台长期使用设备树。
2005 年,PowerPC Linux 进行重大清理合并时,决定要求所有 powerpc 平台支持 DT。为此创建了扁平化设备树 (Flattened Device Tree, FDT),作为二进制 blob 传给内核,无需真正的 Open Firmware。U-Boot、kexec 等引导程序随后也被修改为支持传递和修改 dtb。
FDT 基础设施后来被泛化,目前 6 个主线架构(arm, microblaze, mips, powerpc, sparc, x86)和 1 个非主线架构(nios)都支持设备树。
1.3 平台识别
内核使用 DT 数据识别具体机器。在 ARM 架构上,setup_arch() 调用 setup_machine_fdt(),该函数搜索 machine_desc 表,找到与设备树数据最匹配的项。
匹配依据是根节点的 compatible 属性,该属性包含一个从最具体到最不具体的排序字符串列表:
/** * @brief compatible 属性的匹配逻辑 * * 示例: * compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3"; * compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3"; * * 匹配规则: * 1. 列表从最具体(精确板卡型号)到最不具体(SoC 族系)排序 * 2. 一个 machine_desc 可支持多种板卡,通过声明"不太兼容"的值实现 * 3. setup_machine_fdt() 返回"最兼容"的 machine_desc * 4. 如果无匹配项,返回 NULL * * @warning 避免在板卡级别声明与另一块板卡兼容,因为板间变化通常很大。 * @note 所有 compatible 字符串必须在 Documentation/devicetree/bindings 中记录。 */
1.4 运行时配置
/chosen 节点包含运行时和配置数据:
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
bootargs 包含内核命令行参数;initrd-* 定义 initrd 的位置和大小。注意 initrd-end 是 initrd 映像后的第一个地址,与 struct resource 的通常语义不同。
早期启动时,架构设置代码使用不同辅助回调多次调用 of_scan_flat_dt() 来解析设备树数据。
1.5 设备填充
设备树在驱动开发中最核心的应用就是设备填充——将 DT 节点转换为实际的 platform_device。
/** * @brief 设备填充的核心规则 * * 1. 任何具有 'compatible' 属性的节点通常都代表某种设备 * 2. 树根部的节点要么直接连接到处理器总线,要么是无法通过其他方式描述的系统设备 * 3. 对于根级别节点,Linux 分配并注册 platform_device * 4. I2C/SPI 等总线子节点由对应总线驱动在其 .probe() 中注册 * * 调用 of_platform_populate(NULL, NULL, NULL, NULL) 启动根级别设备发现。 * 若只注册设备,.init_machine() 可仅包含此调用。 */
“simple-bus”兼容节点:第二个参数 of_default_bus_match_table 使匹配“simple-bus”的节点自动遍历其子节点,将子节点也注册为 platform_device。
1.6 AMBA 设备
ARM Primecell 设备通过 AMBA 总线连接。当 DT 节点兼容 "arm,amba-primecell" 时,of_platform_populate() 将其注册为 amba_device 而非 platform_device。
第2章 设备树 ABI 规范
2.1 稳定绑定的定义
/** * @brief 稳定绑定的核心原则 * * 1. 稳定绑定 = 新内核不会破坏旧设备树,但绑定本身并非永远冻结 * 2. 新增属性时:若属性缺失,默认使用先前行为 * 3. 真正需要不兼容变更时:同时更改 compatible 字符串,驱动同时绑定新旧版本 * 4. 维护者准则:不要让完美成为良好的敌人 * 5. 使用具体的 compatible 字符串,为未来添加功能(如 DMA)预留空间 * 6. 绑定可增强,但驱动在收到旧绑定时不应崩溃 * 7. 不要提交暂存或不稳定的绑定 */
第3章 绑定编写规范(DOs and DON'Ts)
3.1 总体设计
| 准则 | 说明 |
|---|---|
| DO 尽量完整 | 即使驱动不支持某些功能(如中断),也要包含对应属性 |
| DON'T 提及 Linux | 绑定应基于硬件本身,而非某个操作系统或驱动 |
| DO 使用标准节点名 | DT Spec 定义了标准的节点类名,如没有可考虑添加新的 |
| DO 验证示例 | 修改后确保示例仍与文档匹配 |
| DON'T 为实例化驱动而建节点 | 多功能设备仅在子节点有独立 DT 资源时才需要子节点 |
| DON'T 单独使用 'syscon' | 'syscon' 硬件块须有足够具体的 compatible 字符串 |
3.2 属性规范
/** * @brief 属性设计准则 * * 'compatible' 属性: * - DO 具体化,DON'T 使用通配符 * - DO 使用回退兼容(当设备是先前实现的子集时) * - DO 在新功能或修复 bug 时添加新 compatible * * 供应商属性: * - DO 使用供应商前缀 * - 检查同类设备是否可通用 * * 通用属性: * - DON'T 重新定义,直接引用定义并添加设备特定约束 * * 单位后缀: * - DO 使用通用后缀,参考 property-units.yaml * * 约束定义: * - DO 以约束形式定义属性:多少条目?可能的值?顺序? */
3.3 典型场景与注意事项
-
phandle 条目:
clocks/dmas/interrupts/resets应显式排序。超过一个 phandle 时必须包含对应的*-names属性 -
命名规范:
{clock,dma,interrupt,reset}-names中不加后缀,如用 "tx" 而非 "txirq" -
schema 设计:包含其他 schema 时使用
unevaluatedProperties:false,其他场景通常使用additionalProperties:false -
子组件:SoC 块使用基于设备的 compatible(如 "vendor,soc1234-i2c"),而非自定义版本号
3.4 板级/SoC dts 文件
/** * @brief 板级 dts 设计准则 * * - DO 将所有 MMIO 设备放在总线节点下,而非顶层 * - DO 使用非空 'ranges' 来限制子总线/设备的地址范围 * (64位平台不需要所有设备都使用64位地址和大小) */
第4章 补丁提交规范
4.1 提交者指南
| 步骤 | 要求 |
|---|---|
| 分离补丁 | Documentation/ 和 include/dt-bindings/ 部分单独成补丁 |
| 主题前缀 | 使用 "dt-bindings: <binding dir>: ..." |
| 格式 | 使用 json-schema 词汇和 YAML 文件格式,必须通过 make dt_binding_check 验证 |
| 许可证 | 双许可证:(GPL-2.0-only OR BSD-2-Clause) |
| 提交 | 发送到 devicetree@vger.kernel.org,抄送 DT 维护者 |
| 顺序 | Documentation/ 部分应在代码实现之前 |
| 兼容字符串 | 芯片/板级 DTS 文件中使用的任何 compatible 字符串必须先记录在绑定文件中 |
| 通配符 | 可使用 <chip> 通配符,但需记录已知的具体值 |
4.2 维护者指南
/** * @brief 内核维护者的职责 * * 1. 如对绑定审查不确定,回复并请求 DT 维护者指导 * 2. 驱动(非子系统)绑定:若熟悉且 DT 维护者数周未反馈,可直接接受 * 3. 子系统绑定(影响多个设备):必须由 DT 维护者审查 * 4. 跨多棵树的系列:绑定补丁应随使用该绑定的驱动一起 */
第5章 设备树变更集
5.1 设计理念:原子性操作
/** * @brief Devicetree Changesets 的核心价值 * * 变更集是一种在实时树(live tree)上应用修改的方法,保证: * - 原子性:要么所有修改全部应用,要么完全不应用 * - 可回滚:如果应用过程中发生错误,树会回退到之前的状态 * - 可撤销:已成功应用的变更集可以被移除 * * 通知时机: * 所有变更在发出 OF_RECONFIG 通知器之前一次性应用到树中。 * 这确保了通知器的接收方看到一个完整且一致的树状态。 */
5.2 变更集的操作序列
/** * @brief 变更集的完整生命周期 * * 1. of_changeset_init() - 初始化变更集 * * 2. 准备变更(无顺序要求,此阶段不修改活跃树): * - of_changeset_attach_node() - 附加节点 * - of_changeset_detach_node() - 分离节点 * - of_changeset_add_property() - 添加属性 * - of_changeset_remove_property() - 移除属性 * - of_changeset_update_property() - 更新属性 * 所有操作记录在 changeset 的 'entries' 列表中。 * * 3. of_changeset_apply() - 应用变更到活跃树 * 如果出错,树将恢复到之前状态。 * 核心通过锁机制确保正确的序列化。 * * 4. of_changeset_revert() - 移除已应用的变更集 */
设计精髓:这种设计将“准备”和“提交”分离,实现了类似于数据库事务的语义。驱动开发者可以在复杂操作(如 FPGA 部分重配置)中构建一组相关的 DT 修改,然后作为一个整体原子地提交。
场景调试:如果在应用变更集时遇到“Duplicate name”错误,通常是因为在同一个父节点下试图附加两个同名节点。使用 of_changeset_attach_node() 前,应检查目标父节点是否已有同名子节点。
5.3 变更集操作详解
/** * @brief 各操作的语义与使用场景 * * attach_node: 将一个 dt_node 附加到 live tree 的指定父节点。 * - 如果父节点已有子节点,新节点会替换当前子节点,并将旧子节点变为其兄弟节点。 * - 典型场景:插入一个新设备的描述。 * * detach_node: 从 live tree 中分离一个节点。 * - 节点的父指针会被更新或兄弟指针会被重新链接。 * - 典型场景:移除一个热拔插设备的描述。 * * add_property: 向节点添加新属性。 * - 如果同名属性已存在,操作将失败。 * * remove_property: 从节点移除现有属性。 * * update_property: 更新现有属性的值。 * - 如果属性不存在,则添加;如果存在,则替换。 */
第6章 设备树动态解析器
6.1 解析器的工作原理
动态解析器处理带有 /plugin/ 标签的设备树,用于解析覆盖层中的引用。它生成 __fixups__ 和 __local_fixups__ 节点。
/** * @brief 解析器的六个步骤 * * 1. 获取 live tree 的最大 phandle 值 + 1 * * 2. 调整待解析树中所有本地 phandle(增加上述值), * 避免与 live tree 中的 phandle 冲突 * * 3. 使用 __local_fixups__ 节点信息以相同量调整所有本地引用 * * 4. 对 __fixups__ 节点中的每个属性: * 在 live tree 中定位它所引用的节点(使用标签) * * 5. 获取 fixup 目标的 phandle * * 6. 对属性中的每个 fixup: * 定位 node:property:offset 位置,将原值替换为 phandle 值 */
/**
* @brief 解析器的应用场景
*
* 当驱动通过 of_overlay_fdt_apply() 加载一个设备树覆盖层时,
* 解析器负责:
* - 解决标签引用(如 &label 语法)
* - 确保 phandle 不冲突
* - 将覆盖层的节点正确地插入 live tree
*
* @note 如果基础 DT 未使用 -@ 选项编译,则 "&ocp" 标签不可用。
* 此时可使用目标路径语法:&{/ocp}
*/
设计精髓:phandle 的偏移调整机制确保了覆盖层的 phandle 不会与 live tree 中的冲突。这是一种优雅的命名空间隔离策略——覆盖层作者无需关心 live tree 中已使用了哪些 phandle 值。
场景调试:如果覆盖层加载失败且日志中提到“invalid phandle”,通常是因为基础 DT 未使用 -@ 选项编译。解决方法是使用 DTC_FLAGS="-@" 重新编译基础 DT。
第7章 设备树覆盖层 (Overlays)
7.1 设计理念:动态修改实时树
覆盖层的目的是修改内核的实时树,并使修改反映到内核状态中——新设备节点应创建对应的设备,移除或禁用的节点应注销对应设备。
/** * @brief 覆盖层的核心机制 * * 覆盖层允许在运行时动态修改内核的设备树,主要应用场景: * - FPGA 部分重配置后的设备注册 * - 热插拔子板(如 BeagleBone Capes、Raspberry Pi HATs) * - 动态加载的设备树片段 * * 覆盖层使用 dtc 的 -@ 选项编译,生成 __fixups__ 和 __local_fixups__ 节点。 * 解析器在应用覆盖层之前解决这些引用。 */
7.2 覆盖层示例
基础设备树 (foo.dts):
/dts-v1/;
/ {
compatible = "corp,foo";
/* shared resources */
res: res { };
/* On chip peripherals */
ocp: ocp {
/* peripherals that are always instantiated */
peripheral1 { ... };
};
};
覆盖层 (bar.dts):
/dts-v1/;
/plugin/;
&ocp {
/* bar peripheral */
bar {
compatible = "corp,bar";
... /* various properties and child nodes */
};
};
当覆盖层被加载并解析后,结果树 (foo+bar.dts) 变为:
/ {
compatible = "corp,foo";
res: res { };
ocp: ocp {
peripheral1 { ... };
bar {
compatible = "corp,bar";
... /* various properties and child nodes */
};
};
};
/**
* @brief 标签语法 vs 路径语法
*
* 标签语法 (label syntax): &ocp { ... };
* - 需要基础 DT 使用 -@ 选项编译
* - 更灵活:覆盖层可应用于任何包含该标签的基础 DT
*
* 路径语法 (path syntax): &{/ocp} { ... };
* - 不需要 -@ 选项
* - 路径硬编码,可移植性较差
*
* @note 标签语法是推荐的方式。
*/
7.3 覆盖层的内核 API
/** * @brief 覆盖层的操作接口 * * 1. of_overlay_fdt_apply(ovcs_id, fdt_base): * 创建并应用一个覆盖层变更集。 * 返回值:错误码或标识此覆盖层的 cookie。 * * 2. of_overlay_remove(cookie): * 移除并清理之前通过 of_overlay_fdt_apply() 创建的覆盖层变更集。 * 如果当前覆盖层被另一个覆盖层堆叠,不允许移除。 * * 3. of_overlay_remove_all(): * 一次性按正确顺序移除所有覆盖层。 * * 通知器机制: * of_overlay_notifier_register() / of_overlay_notifier_unregister() * 通知类型:OF_OVERLAY_PRE_APPLY, OF_OVERLAY_POST_APPLY, * OF_OVERLAY_PRE_REMOVE, OF_OVERLAY_POST_REMOVE */
/** * @warning 覆盖层内存管理的注意事项 * * 1. OF_OVERLAY_PRE_APPLY / POST_APPLY / PRE_REMOVE 回调中 * 可以存储指向覆盖层节点的指针,但这些指针不能保留到 * OF_OVERLAY_POST_REMOVE 回调之后。 * 覆盖层的内存在 OF_OVERLAY_POST_REMOVE 后被 kfree()。 * * 2. drivers/of/dynamic.c 中的变更集通知器是第二类通知器, * 不允许存储指向覆盖层树节点的指针。 * * 3. 任何在覆盖层移除后仍持有指向覆盖层节点或数据指针的代码 * 都应视为 bug,因为指针将指向释放后的内存。 * * 4. 驱动程序必须在覆盖层移除后不保留任何节点引用。 * 特别注意在覆盖层应用后加载的驱动或子系统, * 如果它们扫描整个设备树(包括覆盖层节点),将持有无效指针。 */
场景调试:如果卸载覆盖层后出现 kernel panic,通常是因为某处代码仍持有指向覆盖层节点的指针。使用 CONFIG_DEBUG_KOBJECT_RELEASE 和 CONFIG_DEBUG_DEVRES 可以帮助追踪这类问题。
第8章 设备树单元测试框架
8.1 测试目标
/** * @brief OF Selftest 的设计目标 * * 测试 include/linux/of.h 提供给设备驱动开发者的接口, * 用于从 unflattened 设备树数据结构中获取设备信息。 * * 测试数据动态附加到 live tree,与机器架构无关。 * * @note 阅读本文档前建议先了解: * - Documentation/devicetree/usage-model.rst * - http://www.devicetree.org/Device_Tree_Usage */
8.2 详细输出与预期消息
/** * @brief EXPECT 标记的作用 * * 当 unittest 检测到问题时,会在控制台打印警告或错误。 * 由于 unittest 使用故意错误的测试数据,会触发来自其他内核代码的 * 警告和错误。这可能导致混淆:这些消息是测试的预期结果还是真正的问题? * * EXPECT 标记解决了这个问题: * EXPECT \ : text (开始标记) - 在触发警告/错误之前打印 * EXPECT / : text (结束标记) - 在触发警告/错误之后打印 * * scripts/dtc/of_unittest_expect 脚本可过滤这些详细输出, * 并高亮不匹配的警告/错误。使用 --help 查看详细信息。 */
8.3 测试数据结构
测试数据的 DTS 文件位于 drivers/of/unittest-data/testcases.dts,包含以下子文件:
/** * @brief 测试数据包含的子文件 * * - tests-interrupts.dtsi : 中断相关测试 * - tests-platform.dtsi : 平台设备测试 * - tests-phandle.dtsi : phandle 引用测试 * - tests-match.dtsi : 匹配逻辑测试 * * 当内核以 CONFIG_OF_SELFTEST=y 编译时: * 1. 测试 DTS 被编译为 dtb (通过 dtc 规则) * 2. dtb 被汇编器包装为 .dtb.S 文件 * 3. 汇编文件被编译为 .dtb.o 目标文件 * 4. .dtb.o 被链接到内核镜像中 * 5. 内核符号 __dtb_testcases_begin 和 __dtb_testcases_end * 标记测试数据 blob 的起始和结束地址 */
8.4 测试数据的附加
/** * @brief 测试数据附加到 live tree 的过程 * * 1. selftest_data_add() 被调用 * 2. 读取通过内核符号引用的 FDT 数据 * 3. 调用 of_fdt_unflatten_tree() 将扁平化 blob 转为树结构 * 4. 若 live tree 存在,将测试数据树附加到其上 * 否则,将测试数据作为 live tree * * attach_node_and_children() 使用 of_attach_node() 附加节点: * - 新节点作为给定父节点的子节点 * - 若父节点已有子节点,新节点替换当前子节点, * 并将其变为自己的兄弟节点 * - 这使得后附加的节点成为"更前"的子节点 */
附加前后树结构变化示意:
附加前(live tree 部分):
root ('/')
|
child1 -> sibling2 -> sibling3 -> sibling4 -> null
| | | |
... ... ... null
附加的测试数据:
testcase-data | test-child0 -> test-sibling1 -> test-sibling2 -> test-sibling3 -> null | test-child01
附加后:
root ('/')
|
testcase-data -> child1 -> sibling2 -> sibling3 -> sibling4 -> null
| | | | |
| ... ... ... null
|
test-sibling3 -> test-sibling2 -> test-sibling1 -> test-child0 -> null
| | | |
null null null test-child01
/** * @note 注意:附加后 test-child0 成为最后一个兄弟节点。 * 这是因为附加操作是逐个进行的: * 先附加 test-child0,然后附加 test-sibling1, * test-sibling1 将 test-child0 推为兄弟节点,自己成为子节点。 * * 重复节点处理: * 若 live tree 中已存在同名节点,不会创建新节点, * 而是调用 update_node_properties() 更新现有节点的属性。 */
8.5 测试数据的移除
/** * @brief 测试数据移除过程 * * 测试执行完毕后,selftest_data_remove() 被调用: * 1. 调用 detach_node_and_children() * 2. 该函数使用 of_detach_node() 从 live tree 分离节点 * 3. 移除顺序:从叶节点开始,逐步向上移除父节点 * 4. 最终整个测试数据树被移除 * * of_detach_node() 的操作: * - 更新给定节点父节点的子指针,指向其兄弟节点 * - 或将前一个兄弟节点附加到给定节点的兄弟节点上 */
第9章 设备树内核 API 参考
9.1 核心函数
| 源文件 | 主要功能 |
|---|---|
| drivers/of/base.c | 节点遍历、属性读取、phandle 解析 |
| include/linux/of.h | OF API 的核心头文件,包含 of_property_read_* 系列函数 |
| drivers/of/property.c | 统一设备属性接口 |
| include/linux/of_graph.h | 设备图(port/endpoint)相关结构 |
| drivers/of/address.c | 地址转换(of_translate_address 等) |
| drivers/of/irq.c | 中断解析(of_irq_get 等) |
| drivers/of/fdt.c | FDT blob 解析与遍历 |
9.2 驱动模型函数
| 源文件 | 主要功能 |
|---|---|
| include/linux/of_device.h | struct of_device_id 定义 |
| drivers/of/device.c | 设备与 OF 节点的关联操作 |
| include/linux/of_platform.h | of_platform_populate 声明 |
| drivers/of/platform.c | platform_device 创建与注册 |
9.3 覆盖层与动态 DT 函数
| 源文件 | 主要功能 |
|---|---|
| drivers/of/resolver.c | 覆盖层符号解析 |
| drivers/of/dynamic.c | 动态节点/属性操作,变更集通知器 |
| drivers/of/overlay.c | 覆盖层应用/移除/全部清除 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)