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>;
...
};
};
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
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 |
4.3 pinctrl 配置:管脚复用
RK 芯片的 GPIO 是「多功能」的——同一个引脚可以当 I2C 用、可以当 UART 用、也可以当普通 GPIO。pinctrl 就是用来告诉芯片:这个脚现在要干哪份活。
pinctrl 的配置分两步:
- 在 dtsi 里定义「功能组」:比如 i2c8 有两组引脚可选,i2c8m0_xfer 和 i2c8m2_xfer。
- 在设备节点里引用:通过 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 |
&pcfg_pull_up 后,通信就稳了。记住:I2C 的 SDA 和 SCL 必须上拉。
4.4 知识结构总览
下面这张图把设备树的核心逻辑串起来了,你可以对照着看:
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 要求的完全一致,差一点都可能不出图。
make dtbs 编译,然后用 fdtdump 或 dtc -I dtb -O dts 反编译,确认改动生效了。别直接烧录,否则出了问题你都不知道是 dts 没改对还是驱动有 bug。