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 编译流程:为什么不能增量? .dts 源文件 .dtsi 头文件 .dts 宏定义 CPP 预处理器 展开宏 + 合并头文件 展开后的完整设备树 (单一大文件) 所有节点、属性已合并 DTC 核心编译器 语法检查 + 结构验证 .dtb 二进制输出 关键问题 DTC 没有“中间文件”概念 输入是完整树 → 输出是完整dtb

看到了吗?
DTC的输入,是经过CPP预处理后的单一、完整的设备树文本。
它根本不知道你改了哪个文件,它只知道:给我一棵树,我还你一个.dtb。

3.3 那为什么C语言可以增量编译?

我们来对比一下:

特性 C语言编译 设备树编译
编译单元 单个.c文件 整个.dts + 所有.dtsi
中间产物 .o 目标文件 无(直接生成.dtb)
链接阶段 链接器合并.o 无(DTC一次性完成)
增量依据 文件时间戳 + 头文件依赖 无(每次全量)
修改影响范围 仅重新编译改动的.c 整个.dtb重新生成

你想想看,C语言之所以能增量,是因为它把编译拆成了两步:
编译(.c → .o) 和 链接(.o → 可执行文件)。

设备树没有这个拆分。DTC把“编译”和“链接”合二为一了。

3.4 有没有办法“伪增量”?

嗯,这里有个小技巧。

虽然DTC不支持真正的增量编译,但我们可以通过分离设备树文件来减少全量编译的“痛感”。

我个人习惯的做法:

  1. 把不变的部分做成.dtsi —— 比如SoC核心定义、时钟树、中断控制器。
  2. 把经常变的部分单独放.dts —— 比如板级外设、引脚复用、设备使能。
  3. 用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

所以,如果你真的受够了全量编译,可以考虑:

  1. 把经常改的部分做成overlay
  2. 基板设备树保持不变
  3. 每次只编译overlay的.dts

但要注意:overlay有运行时开销,而且不是所有bootloader都支持。

3.7 总结一下

回到最初的问题:只改一个节点,为何要全量重编?

答案其实很简单:

  • DTC没有中间表示 —— 它不生成.o文件,没法做增量
  • 输入是完整的树 —— 预处理后所有文件合并成一个,DTC分不清谁是谁
  • 设计哲学不同 —— DTC追求简单可靠,不是编译速度

我个人觉得,与其抱怨DTC“笨”,不如接受这个现实。
设备树编译本身很快(大型项目也就几秒),真正的瓶颈往往在内核编译和根文件系统打包上。