第三节:DT Overlay 语法基础:/plugin/、target、__overlay__、fragment
好,咱们今天来啃硬骨头了。
Overlay 的语法,说白了就是一套「补丁」规则。你想想看,主设备树(Base DT)是一块完整的电路板,Overlay 就是一块块小模块,要往这块大板上插。怎么插?插在哪?插什么?这就是 /plugin/、target、__overlay__ 和 fragment 要解决的问题。
我个人习惯把 Overlay 文件比作「快递包裹」——fragment 是包裹里的东西,target 是收货地址,__overlay__ 是拆包裹的动作,而 /plugin/ 是整个包裹的快递单号。这么一想,是不是好理解多了?
一、/plugin/:声明身份
每个 Overlay 文件的第一行,必须是:
/dts-v1/;
/plugin/;
这俩缺一不可。/dts-v1/ 告诉编译器这是 DTS 版本 1 的语法,/plugin/ 则声明:「我是一个补丁文件,不是完整设备树」。
我曾经见过同事把 /plugin/ 漏掉,结果编译出来的 .dtbo 文件加载时直接报错,内核日志里一堆「invalid overlay」的提示。排查了半天,最后发现就是少了这行声明。嗯,这种坑踩过一次就记住了。
/plugin/ 的 DTS 文件,即使你写了 fragment 和 target,编译器也会把它当成普通设备树来解析。加载时内核会拒绝,因为格式不对。
二、fragment:包裹里的内容
Overlay 的核心结构是 fragment。每个 fragment 就是一个独立的修改单元。语法长这样:
/ {
fragment@0 {
target = &i2c1;
__overlay__ {
status = "okay";
my_device: my_device@50 {
compatible = "my,device";
reg = <0x50>;
};
};
};
};
这里 fragment@0 是第一个补丁块。你可以写 fragment@1、fragment@2…… 想写几个写几个。每个 fragment 里必须包含两样东西:target 和 __overlay__。
三、target:收货地址
target 指定你要修改哪个节点。有三种写法:
| 写法 | 示例 | 说明 |
|---|---|---|
| 引用标签 | target = &i2c1; |
最常用,直接引用设备树中的标签 |
| 路径字符串 | target-path = "/soc/i2c@7e804000"; |
用绝对路径,适合没有标签的节点 |
| phandle 数字 | target = <0x1234>; |
直接写节点句柄,不推荐,可读性差 |
我个人最推荐用第一种——引用标签。为什么?因为可读性强,而且编译器会帮你检查标签是否存在。我遇到过用路径字符串写错一个字母,结果 Overlay 加载时静默失败,查了半天才发现是路径拼错了。用标签的话,编译阶段就能发现错误。
dtc -I dtb -O dts 反编译你的 Base DTB,看看节点前面有没有带 : 的标签名。
四、__overlay__:拆包裹的动作
__overlay__ 里面放的是你要「叠加」进去的内容。说白了,就是你想在目标节点里新增或修改的属性、子节点。
举个例子:
fragment@0 {
target = &uart0;
__overlay__ {
// 这里的内容会合并到 uart0 节点中
status = "okay";
clock-frequency = <115200>;
};
};
内核加载 Overlay 时,会把 __overlay__ 里的内容,按照设备树合并规则,覆盖或追加到目标节点上。如果目标节点已经有 status 属性,就覆盖;如果没有,就新增。
这里有个细节要注意:合并是递归的。你在 __overlay__ 里写的子节点,也会递归合并到目标节点的子节点中。但如果你写了一个同名属性,新值会完全覆盖旧值,不会做部分更新。
__overlay__ 里写了一个 pinctrl-0 属性,想追加一个引脚配置。结果发现原来的引脚配置全没了。后来才明白,__overlay__ 的合并是「替换」不是「追加」。要追加的话,得用 pinctrl-0 = <&state0 &state1>; 这种写法,把新旧状态都列出来。
五、完整示例:一个真实的 Overlay
咱们来看一个完整的例子。假设你的主板上有一个 SPI 控制器,你想通过 Overlay 挂载一个温度传感器:
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = &spi0;
__overlay__ {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
temp_sensor: temp@0 {
compatible = "my,temperature-sensor";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
};
fragment@1 {
target = <{/soc/gpio@7e200000}>;
__overlay__ {
temp_intr_pin: temp_intr_pin@4 {
brcm,pins = <4>;
brcm,function = <0>;
};
};
};
};
这个例子展示了两个 fragment:
- fragment@0:往 SPI0 控制器里添加一个温度传感器子节点,并启用 SPI0
- fragment@1:用路径语法,在 GPIO 控制器里添加一个中断引脚配置
编译命令:
dtc -@ -I dts -O dtb -o my_overlay.dtbo my_overlay.dts
注意那个 -@ 选项,它告诉编译器保留符号信息,这样 Overlay 才能通过标签找到目标节点。没有 -@,编译出来的 .dtbo 加载时会找不到 &spi0 对应的节点。
六、知识体系图
下面这张图,把 Overlay 语法的核心逻辑串起来了:
七、几个容易踩的坑
最后,我把自己这些年踩过的坑总结一下:
- 标签冲突:Overlay 里定义的标签(比如
temp_sensor)不能和 Base DT 里的标签重名,否则加载会失败。我习惯在 Overlay 的标签前加个模块名前缀,比如my_temp_sensor。 - 路径语法别写错:用
target = <{/path/to/node}>这种路径写法时,花括号和斜杠一个都不能少。少一个花括号,编译器直接报语法错误。 - 别忘了 -@ 选项:编译 Base DTB 时也要加
-@,否则 Base DT 里没有符号信息,Overlay 的target = &xxx就找不到目标了。 - fragment 编号不连续没关系:你可以写
fragment@0、fragment@5、fragment@100,内核不关心编号是否连续,只按文件顺序处理。
嗯,Overlay 的语法基础就这些。说白了就是四个关键词:/plugin/ 声明身份,fragment 打包内容,target 指定位置,__overlay__ 描述改动。把这四个东西搞明白,Overlay 就算入门了。