第四章 CPE平台设备树框架:SoC级dtsi、板级dts、overlay机制

好,咱们今天聊聊CPE设备树的整体框架。说实话,很多刚入行的朋友拿到一个CPE方案,看到那一堆.dtsi和.dts文件就头大。我刚开始也是这样,总觉得这玩意儿怎么这么绕?后来做多了才发现,这套分层设计其实非常巧妙。

4.1 为什么要分层?

你想想看,一个CPE平台,SoC可能是联发科MT7981或者瑞昱RTL9607,但板级设计千差万别——有的带Wi-Fi 6,有的带GPON,有的带4G模块。如果每个板子都从头写一份完整的设备树,那维护成本就太高了。

所以设备树引入了分层机制:

  • SoC级dtsi:描述芯片内部所有硬件资源
  • 板级dts:描述具体板卡的外设和差异
  • overlay:运行时动态修改设备树

说白了,就是「公共部分抽出来,差异部分单独写」。我在项目中遇到过好几次,因为没搞清楚分层逻辑,导致同一个外设在两个文件里重复定义,编译直接报错。嗯,这个坑咱们后面细说。

4.2 SoC级dtsi:芯片的「户口本」

SoC级dtsi文件,一般由芯片原厂提供。它定义了这颗芯片的所有硬件资源,包括:

  • CPU核心数量和类型
  • 内存控制器和地址映射
  • 各类总线(AXI、AHB、APB)
  • 内置外设(UART、I2C、SPI、GMAC等)
  • 中断控制器和时钟控制器

举个例子,MT7981的dtsi大概长这样:

// mt7981.dtsi
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/clock/mt7981-clk.h>

/ {
    compatible = "mediatek,mt7981";
    interrupt-parent = <&gic>;
    #address-cells = <2>;
    #size-cells = <2>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a53";
            reg = <0x0>;
            enable-method = "psci";
        };
    };

    uart0: serial@11002000 {
        compatible = "mediatek,mt7981-uart",
                     "mediatek,mt6577-uart";
        reg = <0 0x11002000 0 0x400>;
        interrupts = <GIC_SPI 52 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&topckgen CLK_TOP_UART0_SEL>;
        status = "disabled";
    };
};

注意看,这里uart0的status默认是"disabled"。为什么?因为芯片内部有多个UART,但具体哪个被引出到板子上,得看板级设计。这个设计哲学很有意思——SoC级只提供可能性,板级决定哪些被启用

重要原则:SoC级dtsi中,所有外设的status默认设为"disabled"。板级dts中再根据需要改为"okay"。

4.3 板级dts:具体板卡的「配置单」

板级dts文件,就是咱们驱动工程师最常打交道的地方。它通过#include包含SoC级dtsi,然后做三件事:

  1. 启用外设:把status从"disabled"改成"okay"
  2. 配置管脚:通过pinctrl设置复用功能
  3. 添加板级特有设备:比如GPON模块、Wi-Fi芯片、LED指示灯

来看一个实际的板级dts:

// mt7981-cpe-v1.dts
/dts-v1/;
#include "mt7981.dtsi"

/ {
    model = "MediaTek MT7981 CPE Board v1";
    compatible = "mediatek,mt7981-cpe-v1", "mediatek,mt7981";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

    memory {
        device_type = "memory";
        reg = <0 0x40000000 0 0x20000000>;  // 512MB
    };

    gpio-leds {
        compatible = "gpio-leds";
        led-power {
            label = "power";
            gpios = <&pio 8 GPIO_ACTIVE_HIGH>;
            default-state = "on";
        };
    };
};

&uart0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
};

&uart1 {
    status = "okay";  // 接蓝牙模块
};

&spi0 {
    status = "okay";
    spi-nor@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

这里有个细节我特别想强调:&uart0这种语法叫「引用节点」。它不是在创建新节点,而是在修改SoC级dtsi中已经定义好的节点。我曾经见过有人直接在板级dts里重新写一遍uart0,结果编译报duplicate node name——这就是没搞懂引用的用法。

我的习惯:在板级dts中,我一般先写/ {}根节点下的板级特有内容(LED、按键、GPIO扩展等),然后用&引用修改SoC级外设。这样结构清晰,不容易乱。

4.4 Overlay机制:动态修改的「补丁」

Overlay,说白了就是设备树的「热补丁」。它允许系统在运行时动态加载或卸载设备树片段。这个机制在CPE设备上特别有用,因为:

  • 同一个硬件平台可能支持不同的扩展模块
  • USB外设热插拔需要动态添加节点
  • 某些功能可以通过软件许可证动态启用

Overlay的语法和普通dts几乎一样,但有几个关键区别:

// mt7981-wifi-overlay.dts
/dts-v1/;
/plugin/;

&wifi {
    status = "okay";
    mediatek,eeprom = <&spi_nor 0x100000 0x4000>;
};

&spi0 {
    spi-nor@0 {
        partitions {
            wifi-calibration@100000 {
                label = "wifi-cal";
                reg = <0x100000 0x4000>;
            };
        };
    };
};

注意第一行:/plugin/; 这个声明告诉编译器这是一个overlay,不是完整的设备树。编译方式也不同:

# 编译普通dts
dtc -I dts -O dtb -o mt7981-cpe-v1.dtb mt7981-cpe-v1.dts

# 编译overlay
dtc -I dts -O dtb -o mt7981-wifi-overlay.dtbo mt7981-wifi-overlay.dts

加载overlay的方式也很有意思。在Linux内核中,可以通过configfs接口操作:

# 加载overlay
mkdir /sys/kernel/config/device-tree/overlays/wifi
cat /lib/firmware/mt7981-wifi-overlay.dtbo > /sys/kernel/config/device-tree/overlays/wifi/dtbo

# 卸载overlay
rmdir /sys/kernel/config/device-tree/overlays/wifi

我曾经踩过的坑:Overlay中引用的节点,必须在基础设备树中已经存在。如果你在overlay里引用了一个不存在的节点,加载时会直接报错,而且错误信息非常隐晦——就一个「Invalid argument」。排查起来特别痛苦。所以我的建议是:先确认基础dtb中所有被引用的节点都存在,再写overlay

4.5 三者的协作关系

咱们用一张表格总结一下这三者的分工:

层级 文件后缀 谁维护 修改频率 典型内容
SoC级dtsi .dtsi 芯片原厂 极低(芯片版本更新) CPU、总线、内置外设定义
板级dts .dts 设备厂商 中等(硬件改版) 外设启用、管脚配置、板级设备
Overlay .dtso 驱动/系统工程师 较高(功能扩展) 动态加载的外设、分区、校准数据

实际项目中,我一般这样组织:

  1. SoC级dtsi直接从芯片厂商的SDK里拿,基本不改
  2. 板级dts作为主设备树,编译成dtb烧入固件
  3. Overlay放在文件系统中,按需加载

这样做的好处是:基础系统不变,功能扩展灵活。比如同一个CPE设备,出货到不同运营商,可能有的需要Wi-Fi 6,有的只需要Wi-Fi 5。通过不同的overlay组合,一套固件就能覆盖多个SKU。

4.6 实际项目中的避坑指南

最后分享几个我实际项目中遇到的坑:

  • 地址冲突:有一次我在板级dts里给一个SPI flash分配了地址,结果和SoC级dtsi里的某个外设地址重叠了。系统启动时直接挂掉。排查了一天才发现是地址空间冲突。所以我的习惯是:每次修改地址相关的内容,都拿地址映射表核对一遍
  • 中断号不对:CPE设备的中断控制器可能是GIC v2或者v3,中断号的编码方式不同。我曾经把一个SPI中断号写成了PPI,结果中断一直触发不了。嗯,这个细节很容易被忽略。
  • Overlay加载顺序:如果有多个overlay依赖同一个资源,加载顺序很重要。比如Wi-Fi overlay依赖SPI-NOR分区,那就必须先加载分区overlay,再加载Wi-Fi overlay。我建议在启动脚本里明确指定加载顺序。

好了,这一章的内容就到这里。设备树的分层设计,说白了就是「分而治之」的思想在嵌入式领域的体现。理解了SoC级dtsi、板级dts和overlay这三者的关系,你就能更好地驾驭CPE平台的设备树配置。