第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_RELEASECONFIG_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 覆盖层应用/移除/全部清除
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐