3. 增量编译:只改一个节点,为何要全量重编?
这个问题,我几乎每次培训都会被问到。
“我就改了一个GPIO的pinmux,为啥整个设备树都要重新编译?”
“增量编译不是应该只编译我改的那个文件吗?”
嗯,听起来很有道理。但实际做起来,完全不是这么回事。
今天我们就来扒一扒,设备树编译(DTC)到底干了什么,为什么它“不聪明”。
3.1 先看一个“血泪”案例
我之前带过一个项目,硬件改了一版,把UART3的TX/RX引脚从PB10/PB11换到了PC4/PC5。
软件这边,我就改了一行.dtsi:
// 原配置
uart3_pins_a: uart3-pins-a {
pins = "PB10", "PB11";
function = "uart3";
};
// 改后配置
uart3_pins_a: uart3-pins-a {
pins = "PC4", "PC5";
function = "uart3";
};
改完我心想:就一个节点,make一下,几秒钟搞定。
结果呢?
整个.dtb重新生成了。所有外设的配置、时钟、中断,全部重来一遍。
我当时就纳闷了:DTC这么“笨”的吗?
3.2 DTC为什么不支持增量编译?
说白了,设备树编译器(DTC)的设计哲学就是:输入完整,输出完整。
它不像C语言的编译器,有.o文件做中间产物,可以按文件粒度增量。
DTC的输入是一个巨大的、展开后的树状结构。
我画个图你就明白了:
看到了吗?
DTC的输入,是经过CPP预处理后的单一、完整的设备树文本。
它根本不知道你改了哪个文件,它只知道:给我一棵树,我还你一个.dtb。
3.3 那为什么C语言可以增量编译?
我们来对比一下:
| 特性 | C语言编译 | 设备树编译 |
|---|---|---|
| 编译单元 | 单个.c文件 | 整个.dts + 所有.dtsi |
| 中间产物 | .o 目标文件 | 无(直接生成.dtb) |
| 链接阶段 | 链接器合并.o | 无(DTC一次性完成) |
| 增量依据 | 文件时间戳 + 头文件依赖 | 无(每次全量) |
| 修改影响范围 | 仅重新编译改动的.c | 整个.dtb重新生成 |
你想想看,C语言之所以能增量,是因为它把编译拆成了两步:
编译(.c → .o) 和 链接(.o → 可执行文件)。
设备树没有这个拆分。DTC把“编译”和“链接”合二为一了。
3.4 有没有办法“伪增量”?
嗯,这里有个小技巧。
虽然DTC不支持真正的增量编译,但我们可以通过分离设备树文件来减少全量编译的“痛感”。
我个人习惯的做法:
- 把不变的部分做成.dtsi —— 比如SoC核心定义、时钟树、中断控制器。
- 把经常变的部分单独放.dts —— 比如板级外设、引脚复用、设备使能。
- 用Makefile的依赖追踪 —— 虽然DTC全量编译,但Make可以判断是否需要重新调用DTC。
举个例子:
# 在Makefile中
board.dtb: board.dts soc.dtsi memory.dtsi
dtc -I dts -O dtb -o $@ $<
# 如果只改了board.dts,soc.dtsi没变
# Make会检测到依赖变化,重新生成board.dtb
# 但DTC内部仍然是全量编译
说白了,这是Make层面的增量,不是DTC层面的增量。
别纠结DTC为什么不支持增量编译。它的设计就是简单粗暴——输入完整树,输出完整dtb。
与其想办法“骗”DTC做增量,不如把精力放在优化.dts文件结构上,让每次全量编译的代价降到最低。
3.5 一个常见的误解
我曾经遇到一个同事,他坚持认为:
“我把设备树拆成多个.dtsi,然后只编译我改的那个.dtsi,不就能增量了吗?”
嗯,想法很好,但行不通。
因为.dtsi不是独立的编译单元。它必须被某个.dts通过#include包含进去,才能参与编译。
你单独编译一个.dtsi,DTC会报错:
Error: soc.dtsi:1: syntax error
FATAL ERROR: Cannot parse the input tree
为什么?
因为.dtsi里只有节点片段,没有根节点/dts-v1/;,没有完整的树结构。
DTC拿到一个“半棵树”,直接懵了。
永远不要尝试单独编译.dtsi文件。它必须作为.dts的一部分被包含。
如果你真的想“只编译改动的部分”,可以考虑overlay设备树(.dtbo),那是另一套机制。
3.6 那overlay能解决增量问题吗?
好问题。
设备树overlay(.dtbo)确实允许你在运行时动态加载/卸载设备节点。
但它的目的不是加速编译,而是动态配置。
从编译角度看:
- overlay本身也是全量编译的(.dts → .dtbo)
- 但overlay的.dts通常很小,编译很快
- 如果你只改overlay,确实只需要重新编译那个小的.dts
所以,如果你真的受够了全量编译,可以考虑:
- 把经常改的部分做成overlay
- 基板设备树保持不变
- 每次只编译overlay的.dts
但要注意:overlay有运行时开销,而且不是所有bootloader都支持。
3.7 总结一下
回到最初的问题:只改一个节点,为何要全量重编?
答案其实很简单:
- DTC没有中间表示 —— 它不生成.o文件,没法做增量
- 输入是完整的树 —— 预处理后所有文件合并成一个,DTC分不清谁是谁
- 设计哲学不同 —— DTC追求简单可靠,不是编译速度
我个人觉得,与其抱怨DTC“笨”,不如接受这个现实。
设备树编译本身很快(大型项目也就几秒),真正的瓶颈往往在内核编译和根文件系统打包上。