4. 设备树基础:dts/dtsi 文件结构,reg、interrupts、clocks 等属性含义,pinctrl 配置

设备树,说白了就是一块「硬件描述画板」。你想想看,Linux 内核要跑在千奇百怪的板子上,总不能每换一颗传感器就改一遍驱动代码吧?设备树就是用来解耦的——把硬件信息从代码里抽出来,放到一个文本文件里。内核启动时读这个文件,就知道「哦,这颗 CSI 摄像头挂在哪组 I2C 上、用哪条 MIPI 通道、中断脚是哪个」。

我在 RK 平台上调过不下二十款 sensor,每次拿到新模组,第一件事就是打开 dts 文件。嗯,这里面的门道,咱们一条一条捋清楚。

4.1 dts 与 dtsi:谁是谁的「爹」?

先看文件结构。RK 平台的设备树分两层:

  • .dtsi(SoC 级):描述芯片内部固定的资源,比如 CPU 核数、GIC 中断控制器、I2C 控制器、时钟树。这部分由 Rockchip 原厂维护,你一般不改。
  • .dts(板级):描述你这块板子上接了哪些外设,比如摄像头、显示屏、Wi-Fi 模块。这部分是你主要动刀的地方。

举个例子,RK3588 的 dtsi 里定义了 i2c8 这个节点:

i2c8: i2c@fead0000 {
    compatible = "rockchip,rk3588-i2c";
    reg = <0x0 0xfead0000 0x0 0x1000>;
    interrupts = <GIC_SPI 320 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru CLK_I2C8>, <&cru PCLK_I2C8>;
    clock-names = "i2c", "pclk";
    pinctrl-0 = <&i2c8m2_xfer>;
    status = "disabled";
};

然后在你的板级 dts 里,只需要打开它,再挂上 sensor 子节点:

&i2c8 {
    status = "okay";
    imx415: camera@1a {
        compatible = "sony,imx415";
        reg = <0x1a>;
        ...
    };
};
我的习惯: 尽量不改 dtsi,所有板级差异都放在 dts 里。这样换平台时,只需要换 dts,驱动和 dtsi 可以复用。

4.2 核心属性:reg、interrupts、clocks

这几个属性是设备树的「身份证」,内核靠它们找到外设、分配资源。我一个个讲。

4.2.1 reg —— 地址与大小

reg 描述外设的寄存器基地址和映射范围。格式是 <address length>,地址和长度可以有多组。

reg = <0x0 0xfead0000 0x0 0x1000>;  // 64位地址:基址 0xfead0000,大小 4KB

对于 I2C 设备,reg 就是从设备地址:

reg = <0x1a>;  // 7位 I2C 地址 0x1a
我曾经踩过的坑: 某次 sensor 的 I2C 地址是 0x30,但 datasheet 写的是 7 位地址 0x18。后来才发现,0x30 是 8 位左移后的值。设备树里必须填 7 位地址,否则内核 i2c 子系统根本找不到设备。

4.2.2 interrupts —— 中断线

interrupts 告诉内核:这个设备用哪条中断线、触发方式是什么。RK 平台通常用 GIC(通用中断控制器),格式如下:

interrupts = <GIC_SPI 320 IRQ_TYPE_LEVEL_HIGH>;

三个字段的含义:

字段 含义 常见值
GIC_SPI / GIC_PPI 中断类型:SPI(共享外设中断)或 PPI(私有外设中断) GIC_SPI 用于外设,GIC_PPI 用于 CPU 内部
中断号 硬件中断线编号 320(RK3588 的 I2C8 中断)
触发方式 电平触发或边沿触发 IRQ_TYPE_LEVEL_HIGH / IRQ_TYPE_EDGE_RISING

对于 sensor 的 GPIO 中断,写法稍有不同:

interrupt-parent = <&gpio1>;
interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>;

4.2.3 clocks —— 时钟

clocks 属性列出设备需要的时钟源,clock-names 给每个时钟起个名字,驱动里通过名字来获取。

clocks = <&cru CLK_I2C8>, <&cru PCLK_I2C8>;
clock-names = "i2c", "pclk";

MIPI CSI 接口涉及的时钟比较多,我列个表:

时钟名 作用 典型频率
sensor 的 XVCLK sensor 主时钟 24MHz / 27MHz
MIPI DPHY 参考时钟 MIPI 物理层时钟 24MHz
CSI 主机时钟 CSI 控制器时钟 300MHz ~ 600MHz
PCLK APB 总线时钟 100MHz
关键点: sensor 的 XVCLK 必须在驱动里显式使能,否则 sensor 不工作。我见过有人配了 dts 但忘了在驱动里调用 clk_prepare_enable,结果 sensor 死活不出图。

4.3 pinctrl 配置:管脚复用

RK 芯片的 GPIO 是「多功能」的——同一个引脚可以当 I2C 用、可以当 UART 用、也可以当普通 GPIO。pinctrl 就是用来告诉芯片:这个脚现在要干哪份活。

pinctrl 的配置分两步:

  1. 在 dtsi 里定义「功能组」:比如 i2c8 有两组引脚可选,i2c8m0_xfer 和 i2c8m2_xfer。
  2. 在设备节点里引用:通过 pinctrl-0 / pinctrl-names 指定。

看个例子:

// 在 dtsi 中定义
pinctrl: pinctrl {
    i2c8 {
        i2c8m2_xfer: i2c8m2-xfer {
            rockchip,pins =
                <1 RK_PB4 RK_FUNC_2 &pcfg_pull_up>,
                <1 RK_PB5 RK_FUNC_2 &pcfg_pull_up>;
        };
    };
};

// 在设备节点中引用
&i2c8 {
    pinctrl-0 = <&i2c8m2_xfer>;
    pinctrl-names = "default";
};

每个引脚配置包含四个要素:

字段 含义 示例
GPIO 组 属于哪个 GPIO bank 1(GPIO1)
引脚号 bank 内的引脚索引 RK_PB4(GPIO1_B4)
功能码 复用功能编号 RK_FUNC_2(I2C 功能)
上下拉配置 内部上拉/下拉/浮空 &pcfg_pull_up
我曾经踩过的坑: 某次调试,sensor 的 I2C 通信时好时坏。查了半天,发现 pinctrl 里忘了配上下拉,I2C 引脚处于浮空状态。加上 &pcfg_pull_up 后,通信就稳了。记住:I2C 的 SDA 和 SCL 必须上拉。

4.4 知识结构总览

下面这张图把设备树的核心逻辑串起来了,你可以对照着看:

设备树核心结构(RK MIPI CSI 场景) .dtsi(SoC 级) CPU / GIC / CRU / I2C 控制器 .dts(板级) sensor / 电源 / 复位 / 引脚复用 设备节点核心属性 reg(地址/大小) | interrupts(中断号/触发方式) | clocks(时钟源) pinctrl(引脚复用) | status(启用/禁用) MIPI CSI 设备树配置实例 I2C 子节点 reg = <0x1a> 时钟配置 clocks = <&cru ...> 中断配置 interrupts = <...> pinctrl 引脚复用 rockchip,pins = <1 RK_PB4 RK_FUNC_2 &pcfg_pull_up> 说明:dtsi 定义 SoC 资源,dts 定义板级外设;核心属性将两者连接起来,最终驱动通过设备树获取所有硬件信息

4.5 避坑指南

最后,我把自己在 RK 平台上调设备树时踩过的坑总结一下:

  • reg 地址别写错:I2C 地址是 7 位,不是 8 位。datasheet 上如果写 0x30,实际要填 0x18。
  • 中断号要减 32:RK 的 GIC 中断号在 dtsi 里已经减过了,但如果你自己加中断,记得 SPI 中断号要减去 32(因为前 32 个是 PPI)。
  • pinctrl 的上下拉别漏:I2C 必须上拉,MIPI 的 CLK 和 DATA 线通常要配成高阻或特定偏置,看 sensor 手册。
  • status 默认是 disabled:很多节点在 dtsi 里是关着的,你必须在板级 dts 里显式写成 status = "okay"。
  • 时钟频率要匹配:sensor 的 XVCLK 频率必须和 sensor 要求的完全一致,差一点都可能不出图。
我的调试技巧: 每次改完 dts,先 make dtbs 编译,然后用 fdtdump 或 dtc -I dtb -O dts 反编译,确认改动生效了。别直接烧录,否则出了问题你都不知道是 dts 没改对还是驱动有 bug。