第二章 RK平台设备树(DTS)深度解析
设备树,圈里人常叫它 DTS。说白了,它就是一块「硬件描述板」。告诉内核:你有哪些外设、挂在哪条总线上、中断号是多少、GPIO 怎么配。没有它,Linux 内核就是个瞎子。
我在 RK 平台上栽过最大的跟头,十有八九都跟 DTS 写错了有关。有一次点不亮 MIPI 屏,查了三天,最后发现是 pinctrl 里一个引脚功能选错了。嗯,从那以后我再也不敢轻视这个 .dts 文件了。
2.1 DTS 语法与结构——先看懂骨架
一个 .dts 文件,本质上是一棵单根树。根节点叫 /,下面挂各种子节点。每个节点描述一个设备或一段硬件资源。
/dts-v1/;
#include "rk3588.dtsi"
/ {
model = "RK3588 EVB Board";
compatible = "rockchip,rk3588-evb", "rockchip,rk3588";
chosen {
stdout-path = &uart2;
};
memory@a00000 {
device_type = "memory";
reg = <0x0 0xa00000 0x0 0x80000000>;
};
};
结构其实不复杂。几个关键点我拎出来说说:
- 根节点:
/,整棵树的起点。model 和 compatible 是必填项,内核靠它匹配板级。 - 节点名:格式是
name@address。比如i2c@ff3f0000,address 是寄存器基址,用来区分同名节点。 - 属性:键值对。值可以是字符串、32 位整数数组、64 位整数数组、二进制数据。
- 标签:比如
&uart2,用来引用其他节点。我习惯把所有外设节点都打上标签,方便后面覆盖。
核心原则:.dtsi 是头文件,放 SoC 通用定义;.dts 是板级文件,放具体板子的差异。千万别把板级差异写进 .dtsi,否则换一块板子你就得改 SoC 文件,那场面……我经历过一次,很酸爽。
2.2 RK 专用绑定——rockchip,pinctrl 到底怎么用
RK 平台最绕不开的就是 rockchip,pinctrl。它负责引脚功能复用、上下拉、驱动能力。我见过太多人在这里翻车。
先看一个 I2C 节点的完整写法:
&i2c2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&i2c2m0_xfer>;
clock-frequency = <400000>;
gt9xx: touchscreen@14 {
compatible = "goodix,gt9271";
reg = <0x14>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
irq-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
};
};
注意 pinctrl-0 引用的 &i2c2m0_xfer。这个定义在 .dtsi 里长这样:
&i2c2 {
pinctrl-names = "default";
pinctrl-0 = <&i2c2m0_xfer>;
};
&pinctrl {
i2c2m0_xfer: i2c2m0-xfer {
rockchip,pins =
<2 RK_PB3 1 &pcfg_pull_none>,
<2 RK_PB4 1 &pcfg_pull_none>;
};
};
这里 rockchip,pins 的格式是:<bank pin func pull>。bank 是 GPIO 组,pin 是组内引脚,func 是功能号(0=GPIO, 1=功能1, 2=功能2...),pull 是上下拉配置。
| 字段 | 含义 | 常见值 |
|---|---|---|
| bank | GPIO 组编号 | 0~4(RK3588) |
| pin | 组内引脚号 | RK_PA0 ~ RK_PD7 |
| func | 功能复用选择 | 0=GPIO, 1~3=外设功能 |
| pull | 上下拉配置 | pcfg_pull_none / pcfg_pull_up / pcfg_pull_down |
我曾经踩过的坑:I2C 总线死活不通,示波器量 SCL 有波形,SDA 一直低。查了半天,发现 pinctrl 里把 SDA 配成了 pcfg_pull_down。I2C 总线要求 SDA 和 SCL 都是开漏加上拉,你拉低了,设备怎么拉得动?改成 pcfg_pull_none 就好了。
2.3 GPIO 与 I2C 节点实战——手写一个触摸屏节点
实战是最好的学习方式。我们从头写一个 GT9271 触摸屏的节点。
先看原理图:
- I2C 接 i2c2,地址 0x14
- 中断脚:GPIO1_B0
- 复位脚:GPIO1_B1
- 供电:3.3V(不需要 DTS 控制)
完整节点:
&i2c2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&i2c2m0_xfer>;
clock-frequency = <400000>;
gt9271: touchscreen@14 {
compatible = "goodix,gt9271";
reg = <0x14>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
irq-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
AVDD28-supply = <&vcc3v3_sys>;
VDDIO-supply = <&vcc3v3_sys>;
};
};
几个容易出错的地方:
interrupts和irq-gpios都要写,但用途不同。前者是内核中断子系统用的,后者是驱动里 gpio_to_irq() 用的。我建议两个都写上,兼容性最好。AVDD28-supply和VDDIO-supply是 regulator 绑定。如果供电没配,驱动 probe 时会返回 -EPROBE_DEFER,然后内核会重试。但如果你根本没定义这个 regulator,那就永远 probe 不成功。
调试技巧:如果触摸屏没反应,先看 /sys/kernel/debug/gpio 确认 GPIO 状态。再用 i2cdetect -y 2 看设备有没有在总线上。我一般三步走:先看 GPIO 电平,再看 I2C 通信,最后查中断。90% 的问题出在前两步。
2.4 DTS 编译与反编译——你总得会修砖
写好的 .dts 要编译成 .dtb,内核启动时加载。反过来,出问题时你得能从 .dtb 反编译回 .dts 看看实际生效的是什么。
编译命令:
# 编译单个 dts
dtc -I dts -O dtb -o rk3588-evb.dtb rk3588-evb.dts
# 反编译 dtb 到 dts
dtc -I dtb -O dts -o rk3588-evb.dts rk3588-evb.dtb
反编译出来的 .dts 会包含所有展开后的内容,包括 .dtsi 里 include 进来的。这是排查问题的利器。
举个例子:你怀疑 pinctrl 没生效,反编译 .dtb 看看:
i2c2: i2c@ff3f0000 {
pinctrl-0 = <0x12345678>;
status = "okay";
};
看到 0x12345678 这种数字了吗?那是 phandle,指向实际的 pinctrl 节点。你再往下翻,找到对应的 pinctrl 节点,看看 pins 配置对不对。
| 场景 | 命令 | 说明 |
|---|---|---|
| 编译 | dtc -I dts -O dtb |
dts → dtb |
| 反编译 | dtc -I dtb -O dts |
dtb → dts |
| 检查语法 | dtc -I dts -O dtb -o /dev/null |
只检查不输出 |
| 生成汇编 | dtc -I dts -O asm |
调试用 |
注意:反编译出来的 .dts 可读性不如原版。所有标签名会被展开成 phandle 数字,注释也没了。但它能告诉你「内核实际看到的是什么」。我习惯在改完 DTS 后,先编译再反编译,对比一下关键节点,确保没被覆盖或覆盖错了。
2.5 知识结构总览
下面这张图把本章的核心知识点串起来了。你可以把它当一张「DTS 调试地图」用。
这张图从左到右,从上到下,就是 DTS 调试的完整路径。先看懂语法结构,再掌握 RK 专用绑定,然后学会编译反编译,最后用实战案例串起来。我建议你把这图存下来,以后调 DTS 问题时对着看,思路会清晰很多。