每个嵌入式 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 分配算法:
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/