第四章 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,然后做三件事:
- 启用外设:把status从"disabled"改成"okay"
- 配置管脚:通过pinctrl设置复用功能
- 添加板级特有设备:比如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 | 驱动/系统工程师 | 较高(功能扩展) | 动态加载的外设、分区、校准数据 |
实际项目中,我一般这样组织:
- SoC级dtsi直接从芯片厂商的SDK里拿,基本不改
- 板级dts作为主设备树,编译成dtb烧入固件
- 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平台的设备树配置。