4、Rockchip设备树架构:rk3588.dtsi、rk3588-evb.dts等文件结构、include与覆盖机制

设备树这东西,说白了就是一块「硬件描述画板」。Rockchip 的玩法,是把公用的、芯片级的东西放在 .dtsi 里,把板级差异扔进 .dts。我刚开始接触 RK3588 时,面对那一堆 #include 和 & 符号,说实话有点懵。后来拆了几个官方 evb 的 dts,才摸清套路。

今天我们就来拆解一下:rk3588.dtsi 里到底藏了什么?rk3588-evb.dts 又是怎么「叠加上去」的?

4.1 文件结构:从芯片到板子

Rockchip 的设备树文件,遵循一套清晰的层级。我习惯把它想象成「俄罗斯套娃」——最里面是芯片本身,外面一层层套上 soc、板级、产品级。

文件 层级 描述
rk3588.dtsi 芯片级 RK3588 所有内置 IP 的默认配置,比如 CPU 核心、GPU、NPU、DDR 控制器、中断控制器等。这是最底层的「硬件清单」。
rk3588-evb.dtsi 板级公共 EVB 开发板的公共配置,比如 eMMC、SD 卡、以太网、USB 等外设的引脚复用和电源。
rk3588-evb1-lp4.dts 具体产品 最终编译的设备树源文件,通过 #include 把上面两个文件拉进来,再覆盖或新增自己特有的配置。

嗯,这里要注意:.dtsi 是 不能单独编译 的,它必须被 .dts 包含。你可以把 .dtsi 理解成 C 语言里的头文件,只是它描述的是硬件。

4.2 include 机制:不只是简单的文本粘贴

设备树的 #include 和 C 语言的 #include 很像,但有个关键区别——它支持 覆盖(overlay)。我在项目中遇到过好几次,明明在 .dtsi 里配好了某个外设,结果在 .dts 里重新赋值后,之前的配置就全丢了。

为什么会这样?因为设备树的解析规则是:后出现的属性覆盖先出现的。如果你在 .dtsi 里写了:

&uart2 {
    status = "disabled";
    pinctrl-0 = &uart2m0_xfer;
};

然后在 .dts 里写:

&uart2 {
    status = "okay";
};

最终 uart2 的 status 是 "okay",但 pinctrl-0 呢?没了。因为你在 .dts 里重新引用了 &uart2 节点,相当于重新打开了一个作用域,之前的子属性不会自动继承。

避坑指南: 我曾经在调试 RK3588 的 SPI 接口时,花了整整一天才发现,原来我在 .dtsi 里配好了 SPI 的引脚和时钟,结果在 .dts 里只改了个 status,其他配置全丢了。正确的做法是:要么在 .dts 里把需要的属性全部重写一遍,要么用 /delete-property/ 先删掉再重新赋值。

4.3 覆盖机制:& 符号的妙用

设备树的覆盖,核心就是 & 符号。它用来引用一个已经存在的节点,然后往里面添加或修改属性。我个人的习惯是:能用 & 就别重新定义整个节点,这样代码更清晰,也更容易维护。

举个例子,在 rk3588.dtsi 里,i2c2 节点可能是这样的:

i2c2: i2c@feac0000 {
    compatible = "rockchip,rk3588-i2c";
    reg = <0x0 0xfeac0000 0x0 0x1000>;
    interrupts = <GIC_SPI 80 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru CLK_I2C2>, <&cru PCLK_I2C2>;
    clock-names = "i2c", "pclk";
    pinctrl-0 = <&i2c2m0_xfer>;
    status = "disabled";
};

然后在 rk3588-evb.dtsi 里,你想挂一个触摸屏:

&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    touch@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <RK_PB5 IRQ_TYPE_EDGE_FALLING>;
    };
};

你看,&i2c2 引用了之前定义的节点,然后添加了 clock-frequency 和一个子节点 touch@38。原来的 compatible、reg、interrupts 这些属性,都还在,不会被覆盖。

个人技巧: 我习惯在 .dtsi 里把所有外设的 status 都设为 "disabled",然后在 .dts 里按需打开。这样既安全又灵活,不会因为忘记关某个外设导致引脚冲突。

4.4 核心结构图:设备树文件关系

下面这张图,是我自己画的一个文件关系图。你看一眼,就能明白 .dtsi 和 .dts 是怎么「拼」在一起的。

Rockchip 设备树文件关系图 Rockchip 设备树文件关系与覆盖机制 rk3588.dtsi 芯片级:CPU/GPU/NPU/DDR rk3588-evb.dtsi 板级:eMMC/SD/以太网 rk3588-evb1-lp4.dts 产品级:最终编译源文件 覆盖机制说明 • 使用 & 符号引用已有节点,添加或修改属性 • 后出现的属性覆盖先出现的(注意:整个节点重新赋值会丢失子属性) • 建议:.dtsi 中 status 默认 disabled,.dts 中按需打开 编译流程:.dts → dtc 编译器 → .dtb(二进制设备树)

4.5 实战:如何正确覆盖一个节点

我记得有一次,客户要求在 RK3588 上同时启用两个 SPI 控制器,但其中一个要改引脚。我当时的做法是:

  1. 在 rk3588.dtsi 里,两个 SPI 的 status 都是 "disabled"。
  2. 在 rk3588-evb.dtsi 里,用 &spi0 和 &spi1 分别打开,并配置各自的引脚。
  3. 在最终的产品 .dts 里,只覆盖需要改动的属性,比如 max-freq 或 cs-gpios。

这样做的最大好处是:改动点集中,排查问题快。如果哪天 SPI0 不工作了,我只需要看 rk3588-evb.dtsi 里 &spi0 那块代码,不用去翻几千行的 rk3588.dtsi。

核心原则: 设备树覆盖的本质是「增量修改」。尽量在 .dtsi 里定义完整的基础配置,在 .dts 里只做最小化的覆盖。这样既保持了芯片级配置的通用性,又保证了板级配置的灵活性。

4.6 常见陷阱与避坑

  • 陷阱一: 在 .dts 里重新定义了整个节点,导致 .dtsi 里的所有子属性丢失。我曾经因为这个,把 I2C 的时钟频率配错了,调试了两天。
  • 陷阱二: 多个 .dtsi 文件同时覆盖同一个节点,后 include 的覆盖先 include 的。我建议用 /include/ 时注意顺序。
  • 陷阱三: 忘记在 .dts 里 #include 必要的 .dtsi,编译直接报错。嗯,这个低级错误我犯过不止一次。

好了,关于 Rockchip 设备树的文件结构和覆盖机制,就聊到这里。你只要记住:.dtsi 是芯片的「底牌」,.dts 是板子的「定制」,& 符号是你修改的「手术刀」。下次看到那些层层嵌套的 #include,你就知道它们是怎么拼起来的了。