复用策略核心:include 机制与 /delete-node/、/delete-property/ 用法
设备树的复用,说白了就是「搭积木」。
你想想看,一个 SoC 可能对应几十款开发板,每款板子外设不一样,但核心的 CPU、内存控制器、中断控制器这些又都一样。如果每块板子都从头写一个完整的 dts,那维护量就太大了。所以,设备树规范里给了我们两样法宝:include 机制和删除节点/属性的语法。
我个人习惯,把 include 比作「继承」,把 /delete-node/ 和 /delete-property/ 比作「裁剪」。两者配合,就能做到「一份公共 dtsi,多板差异化定制」。
1. include 机制:从「复制粘贴」到「引用」
设备树的 include 机制,和 C 语言的 #include 很像。它允许你在一个 .dts 文件里,把另一个 .dtsi 文件的内容原封不动地「拉进来」。
核心规则:
#include "xxx.dtsi"—— 引用同目录或搜索路径下的文件#include—— 引用系统预置路径下的文件(比如内核自带的)- 支持多层嵌套:a.dts include b.dtsi,b.dtsi 再 include c.dtsi
举个例子,我做过一个 i.MX8M 的项目。SoC 厂商提供了 imx8mm.dtsi,里面定义了 CPU 核心、GIC、UART、I2C 等所有内部外设。我的板子只需要写一个 myboard.dts:
// myboard.dts
#include "imx8mm.dtsi"
/ {
model = "My i.MX8M Mini Board";
compatible = "my,imx8mm-board", "fsl,imx8mm";
chosen {
stdout-path = &uart1;
};
};
&uart1 {
status = "okay";
pinctrl-0 = <&pinctrl_uart1>;
};
&uart2 {
status = "disabled"; // 我这板子不用 uart2
};
看到了吗?我只写了板子特有的部分。CPU、内存、时钟这些都在 imx8mm.dtsi 里定义好了。这就是 include 的威力——你不需要重复造轮子。
我的经验:写 dts 时,尽量把「SoC 级」的通用定义放在 .dtsi 里,「板级」的差异化放在 .dts 里。这样换板子时,只需要改 .dts 文件,.dtsi 基本不动。
2. /delete-node/ 和 /delete-property/:精准裁剪
include 进来之后,有时候你会发现:某个节点或属性,在公共 dtsi 里定义了,但你的板子不需要。怎么办?
直接改 dtsi?不行,那会影响其他板子。
在 dts 里重新定义?可以,但有些属性(比如 reg、interrupts)一旦定义就不能覆盖,只能追加。
这时候,/delete-node/ 和 /delete-property/ 就派上用场了。
2.1 /delete-node/:删除整个节点
语法很简单:
/delete-node/ &label;
// 或者
/delete-node/ 节点名@地址;
我在项目中遇到过这样一个场景:某款 SoC 的 dtsi 里定义了 8 个 SPI 控制器,但我的板子只用了 SPI1 和 SPI2。剩下的 6 个如果不处理,内核会为它们分配资源,虽然 status 可以设为 disabled,但设备树的大小和解析时间还是浪费了。
我的做法是:
// 在板级 dts 中
#include "soc.dtsi"
/delete-node/ &spi3;
/delete-node/ &spi4;
/delete-node/ &spi5;
/delete-node/ &spi6;
/delete-node/ &spi7;
/delete-node/ &spi8;
这样,spi3~spi8 在编译后的 dtb 里就彻底消失了。内核解析时根本看不到它们,干净利落。
注意:/delete-node/ 只能用在 dts 文件里,不能用在 dtsi 里。而且它删除的是「引用」对应的节点,不是删除标签本身。如果你删除了一个被其他节点引用的 label(比如某个时钟输出),那引用它的节点就会编译报错。
2.2 /delete-property/:删除单个属性
有时候你不想删整个节点,只想删掉某个属性。比如 dtsi 里给某个外设配了一个默认的时钟频率,但你的板子用了不同的晶振,这个频率就不对了。
/delete-property/ &uart1, clock-frequency;
我曾经踩过一个坑:某款网卡芯片的 dtsi 里定义了 max-speed = <1000>;,但我的板子因为布线问题,千兆跑不稳,只能跑百兆。如果直接改 dtsi,其他用千兆的板子就废了。所以我在板级 dts 里:
ð {
/delete-property/ max-speed;
max-speed = <100>;
};
先删掉原来的属性,再重新定义。注意顺序:先删后加,否则编译会报重复定义。
3. 实战中的组合拳
include 和 delete 配合起来,能玩出很多花样。我画了一张图,帮你理清它们的关系:
这张图展示了完整的流程:公共 dtsi 通过 include 进入板级 dts,板级 dts 再通过 /delete-node/ 和 /delete-property/ 进行裁剪,最终编译成 dtb。说白了,就是「先继承,再定制」。
4. 避坑指南
我这些年用设备树,踩过的坑不少。挑几个典型的说说:
- 删除顺序问题:如果你先删了一个节点,然后又想引用它的 label,编译会报「label 未定义」。所以,
/delete-node/最好放在文件末尾,或者至少在所有引用之后。 - 属性覆盖陷阱:有些属性(比如
reg、interrupts)在 dtsi 里定义后,你在 dts 里不能直接覆盖,只能用/delete-property/先删再重新定义。否则编译器会报「重复定义」。 - include 路径问题:不同构建系统(比如 Yocto、Buildroot)对 include 的搜索路径不一样。我遇到过在 A 系统上编译通过,换到 B 系统就找不到 dtsi 的情况。建议用绝对路径或相对路径,别依赖默认搜索路径。
- 不要滥用删除:能用
status = "disabled"解决的问题,就别用/delete-node/。删除是「物理消失」,禁用是「逻辑关闭」。有些驱动在 probe 时会检查节点是否存在,如果删了反而会出问题。
我的习惯:写板级 dts 时,我会先 include 所有需要的 dtsi,然后在文件末尾统一做删除操作。这样结构清晰,别人看代码时一眼就能知道「这个板子砍掉了哪些东西」。
5. 总结一下
include 机制和 delete 语法,是设备树复用的两大基石。include 让你「拿来」,delete 让你「去掉」。两者结合,就能用最少的代码,覆盖最多的板型。
记住一个原则:公共的放 dtsi,差异的放 dts,多余的用 delete 干掉。这样维护起来,你会感谢自己的。