第三节: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@1fragment@2…… 想写几个写几个。每个 fragment 里必须包含两样东西:target__overlay__

核心要点: fragment 的编号(@0、@1)只是标签,不影响加载顺序。内核是按 fragment 在文件中的先后顺序依次应用的。

三、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 语法的核心逻辑串起来了:

DT Overlay 语法核心结构 / (根节点) fragment@0 fragment@1 target target __overlay__ __overlay__ 属性/子节点 (要叠加的内容) 属性/子节点 (要叠加的内容) 每个 fragment 包含一个 target(目标地址)和一个 __overlay__(叠加内容)

七、几个容易踩的坑

最后,我把自己这些年踩过的坑总结一下:

  • 标签冲突:Overlay 里定义的标签(比如 temp_sensor)不能和 Base DT 里的标签重名,否则加载会失败。我习惯在 Overlay 的标签前加个模块名前缀,比如 my_temp_sensor
  • 路径语法别写错:用 target = <{/path/to/node}> 这种路径写法时,花括号和斜杠一个都不能少。少一个花括号,编译器直接报语法错误。
  • 别忘了 -@ 选项:编译 Base DTB 时也要加 -@,否则 Base DT 里没有符号信息,Overlay 的 target = &xxx 就找不到目标了。
  • fragment 编号不连续没关系:你可以写 fragment@0fragment@5fragment@100,内核不关心编号是否连续,只按文件顺序处理。

嗯,Overlay 的语法基础就这些。说白了就是四个关键词:/plugin/ 声明身份,fragment 打包内容,target 指定位置,__overlay__ 描述改动。把这四个东西搞明白,Overlay 就算入门了。