当前位置:首页>Linux>Linux 设备树编译器 dtc 源码解读:你的 .dts 怎么变成 .dtb,phandle 如何决议

Linux 设备树编译器 dtc 源码解读:你的 .dts 怎么变成 .dtb,phandle 如何决议

  • 2026-09-02 16:21:50
Linux 设备树编译器 dtc 源码解读:你的 .dts 怎么变成 .dtb,phandle 如何决议

每个嵌入式 Linux 项目里都有 .dts 文件。你知道它是设备树源码——描述板子上的 GPIO 、 I2C 设备、内存映射。你知道编译命令:

dtc-Idts-Odtb-omyboard.dtbmyboard.dts

但你大概率不知道 .dts 是怎么变成 .dtb 的。知道这个过程不是学术兴趣——当你的设备树 overlay 没生效、当你的 compatible 字符串匹配不上驱动、当你的 DTB 比预期大了一倍——理解编译过程能让你直接定位问题。

dtc[1] 是设备树编译器——Linux 内核源码树的一部分,在 scripts/dtc/ 目录下。它核心做两件事:词法/语法解析(.dts → AST )和二进制编码( AST → .dtb)。下面拆每一步。

第一步:词法 + 语法——.dts 文本变成语法树

dtc 用手工写的 lexer (dtc-lexer.l)和 yacc/bison parser (dtc-parser.y)来解析 .dts。以这个最简单的设备树为例:

/dts-v1/;
/{
led{
compatible="gpio-leds";
        gpios =<&gpio0170>;
    };
};

Parser 产生的 AST 结构(简化):

root_node("/")
  └─ node("led")
       ├─ property("compatible") = "gpio-leds"
       └─ property("gpios") = <&gpio0 17 0>

&gpio0 是一个 phandle 引用——dtc 不会在这里解析它指向哪个节点。 phandle 解析发生在第二步的二进制编码阶段——这是设备树编译器和 C 编译器的关键区别: C 编译器在语法阶段就解析符号引用,而 dtc 把引用解析推迟到输出阶段。

为什么要推迟?因为 phandle 是编译时自动分配的——一个节点被引用时才被分配一个唯一的 32 位 ID 。语法树阶段还不知道哪些节点会被引用。

第二步:二进制编码——AST 变成扁平 DTB

DTB 格式是无指针的二进制结构——没有 void*、没有偏移量计算、没有内存分配。整个文件是一个连续的字节流,靠固定的 header 结构来定位数据块。

dtb 的内存布局:

+------------------+
| fdt_header       |  固定 40 字节的头部
+------------------+
| mem reserve map  |  保留内存区域(告诉内核哪些物理地址不能用)
+------------------+
| device-tree      |  节点和属性的扁平化结构
|   structure      |
+------------------+
| device-tree      |  字符串块(所有属性名和 compatible 字符串存放处)
|   strings        |
+------------------+

头部结构(来自内核源码 include/linux/libfdt.h):

structfdt_header {
uint32_t magic;             // 0xd00dfeed——魔数,内核以此识别 DTB
uint32_t totalsize;         // 整个 DTB 文件的大小
uint32_t off_dt_struct;     // 结构块的偏移
uint32_t off_dt_strings;    // 字符串块的偏移
uint32_t off_mem_rsvmap;    // 内存保留区的偏移
uint32_t version;           // 版本号——当前为 17
uint32_t last_comp_version; // 最低兼容版本——16
uint32_t boot_cpuid_phys;   // 启动 CPU 的物理 ID
uint32_t size_dt_strings;   // 字符串块的大小
uint32_t size_dt_struct;    // 结构块的大小
};

总共 10 个 32 位字段 = 40 字节。内核启动时 Bootloader 把 DTB 地址传给内核,内核读 magic 确认是有效 DTB ,然后按 off_dt_struct 和 off_dt_strings 的偏移定位到数据区。

第三步: phandle 决议——&gpio0 变成数字

这是 dtc 编译过程中最关键的一步。.dts 里的 &gpio0 是一个标签引用——指向设备树中 gpio0: gpio@xxx 标签标记的节点。

DTC 的 phandle 分配算法:

1.遍历 AST ,收集所有被引用的节点标签
2.为每个被引用的节点分配一个唯一的 phandle ID (从 1 开始递增)
3.把 &gpio0 替换为实际 phandle 数值
4.如果同一个节点被多次引用——所有引用指向同一个 phandle
// dtc 源码中的 phandle 分配逻辑(简化,来自 livetree.c)
cell_t get_node_phandle(structnode*node) {
if (!node->phandle)
        node->phandle =++phandle_counter;  // 惰性分配
return node->phandle;
}

一个常见问题:同一个 .dts 中同一个节点标签在 overlay 和 base 里定义不一致。 dtc 默认不会报错——它只负责编译,不负责验证语义。验证交给 dt-validate(用 YAML binding schema 检查)。

第四步: overlay 编译——-@ 参数做了什么

dtc -@ 生成的不只是 DTB——它额外生成了 __symbols__ 节点和 phandle map :

dtc-@-Idts-Odtb-ooverlay.dtbooverlay.dts

-@ 的作用:
1. 为所有带标签的节点自动生成 phandle (不只是被引用的节点)
2. 生成 __symbols__ 节点——标签名到 phandle 的映射表
3. 生成 __fixups__ 节点——未解析的外部引用列表

当 overlay 被应用到 base DTB 时,内核的 overlay 管理器通过 __fixups__ 找到需要连接的 phandle ,通过 __symbols__ 找到 base DTB 中的目标节点。


dtc 的 3000 行 C 代码做完这三件事:解析( dts→AST )、编码( AST→DTB )、phandle 决议(标签→数字)。下次你的 overlay 没生效时——不要只看驱动代码。用 dtc -I dtb -O dts your.dtbo 把 dtbo 反编译回文本——看看 phandle 是不是对不上。

dtc 源码( Linux 内核树内)[2]
设备树规范( devicetree.org )[3]


参考链接

[1] dtc: https://git.kernel.org/pub/scm/utils/dtc/dtc.git/

[2] dtc 源码( Linux 内核树内): https://git.kernel.org/pub/scm/utils/dtc/dtc.git/

[3] 设备树规范( devicetree.org ): https://www.devicetree.org/specifications/

最新文章

随机文章