3. 设备树基础:DTS语法、Rockchip平台设备树结构、GPIO与PinCtrl配置
设备树(Device Tree)是什么?说白了,它就是一块「硬件描述板」。以前内核里塞满了各种板级文件,换个引脚就要重新编译内核,太折腾了。Rockchip 平台更是如此——芯片型号多、引脚复用复杂,没有设备树,你连灯都点不亮。
我个人习惯把设备树看作「硬件与驱动的契约」。驱动工程师写代码,硬件工程师画原理图,设备树就是中间那个翻译官。今天我们就把它彻底讲透。
3.1 DTS 语法速通
先看一个最简单的节点:
/dts-v1/;
/ {
model = "Rockchip RK3568 EVB";
compatible = "rockchip,rk3568-evb", "rockchip,rk3568";
chosen {
stdout-path = &uart2;
};
};
这里有几个关键点:
- /dts-v1/:版本声明,必须写在第一行。
- /:根节点,整棵树从这里开始。
- model:板子名字,给人看的。
- compatible:匹配字符串,内核靠它找到对应驱动。顺序很重要——越具体越靠前。
节点里可以放属性,属性值可以是字符串、数字、数组,甚至二进制数据。举个例子:
memory@200000 {
device_type = "memory";
reg = <0x00200000 0x3fe00000>;
};
reg 表示地址和长度,格式是 <address length>。多段内存就写多组:
reg = <0x00200000 0x3fe00000>,
<0x40000000 0x40000000>;
小技巧:我刚开始写 DTS 时经常搞混 reg 的顺序。记住一个口诀——「地址在前,长度在后,单位是字节」。
引用节点用 &label 语法。比如:
&uart2 {
status = "okay";
};
这比重新写一遍节点优雅多了。你想想看,如果每个板子都复制粘贴整个 uart2 节点,那设备树得膨胀成什么样。
3.2 Rockchip 平台设备树结构
Rockchip 的设备树分三层:芯片级 .dtsi → 核心板级 .dtsi → 底板级 .dts。我画了张图,你看一眼就明白了:
这种分层设计的好处很明显:芯片厂商维护 .dtsi,核心板厂商维护另一个 .dtsi,底板厂商只写自己的 .dts。各司其职,互不干扰。
我记得第一次接触 RK3399 时,看到 #include 链差点晕了。后来发现,只要顺着 #include 往上找,总能找到源头。内核编译时会把所有 #include 展开成一个完整的 .dts,然后编译成 .dtb。
3.3 GPIO 与 PinCtrl 配置
这部分是实战中的重灾区。很多新手点不亮 LED,十有八九是 PinCtrl 没配对。
3.3.1 GPIO 基础属性
一个典型的 GPIO 节点长这样:
&gpio0 {
led-gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
};
这里 RK_PA5 是 Rockchip 自定义的宏,展开后就是 5。GPIO_ACTIVE_HIGH 表示高电平有效。如果你想要低电平有效,就改成 GPIO_ACTIVE_LOW。
注意:我曾经遇到一个坑——GPIO 编号搞错了。RK3568 的 GPIO0 有 32 个引脚,PA0 对应 0,PA5 对应 5。但有些芯片的 GPIO 编号不是连续的,一定要查 datasheet 确认。
3.3.2 PinCtrl 配置
PinCtrl 负责引脚复用和电气属性配置。看这个例子:
&pinctrl {
led {
led_pin: led-pin {
rockchip,pins =
<0 RK_PA5 1 &pcfg_pull_none>;
};
};
};
每个 rockchip,pins 条目包含四个字段:
| 字段 | 含义 | 示例值 |
|---|---|---|
| bank | GPIO 组号 | 0(GPIO0) |
| pin | 引脚编号 | RK_PA5(5) |
| func | 功能编号 | 1(GPIO 功能) |
| config | 电气配置 | &pcfg_pull_none |
func 编号是关键。0 通常是保留,1 是 GPIO,2 以上是复用功能。比如 UART2 的 TX 可能是 func 2,RX 可能是 func 3。具体值要看 datasheet 里的 Pin Mux Table。
个人经验:我习惯把 PinCtrl 节点按功能分组,比如 led、uart2、i2c1。这样后期维护时一目了然。别把所有引脚塞到一个节点里,那会把自己逼疯。
3.3.3 完整示例:点亮一个 LED
把上面两部分串起来,就是一个完整的 LED 节点:
// 在根节点下添加
leds {
compatible = "gpio-leds";
status = "okay";
work_led: led-0 {
label = "work";
gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
pinctrl-names = "default";
pinctrl-0 = <&led_pin>;
};
};
// 在 pinctrl 节点下添加
&pinctrl {
led {
led_pin: led-pin {
rockchip,pins =
<0 RK_PA5 1 &pcfg_pull_none>;
};
};
};
这里 pinctrl-names 和 pinctrl-0 把 LED 节点和 PinCtrl 配置关联起来。内核在 probe 时会自动应用这个配置。
为什么需要 pinctrl-names?因为一个设备可能有多种状态,比如 default、sleep、active。驱动可以根据电源状态切换引脚配置。不过大部分场景只用 default。
3.4 避坑指南
这些年我在 Rockchip 平台上踩过的坑,列几个典型的:
- 引脚冲突:两个节点用了同一个引脚,但 func 不同。编译不会报错,运行时只有一个功能生效。我建议每次改完 DTS 都 grep 一下引脚号,确认没有被其他地方占用。
- 漏配电源域:有些 GPIO 需要先打开对应的电源域(比如
vccio1),否则电平不对。我曾经因为漏配vcc3v3_sys,导致 SD 卡检测脚一直低电平,折腾了两天。 - status 忘写 "okay":默认是 "disabled",不写就等于没配。这个低级错误我犯过不止一次。
- 中断号搞错:GPIO 中断号不是随便写的。RK3568 的 GPIO0 中断号是 32,GPIO1 是 33,以此类推。具体数值看
interrupt-cells定义。
核心原则:设备树不是写一次就完事的。每次硬件改版,都要同步更新 DTS。我见过太多「硬件改了,DTS 没改」的案例,结果点不亮、跑不稳,最后查出来是引脚没对上。
3.5 调试方法
配完 DTS 怎么验证?我一般用这几招:
- 查看 /sys/kernel/debug/gpio:确认 GPIO 状态、方向、电平。
- 查看 /sys/kernel/debug/pinctrl/pinctrl-handles:确认 PinCtrl 是否被正确应用。
- 用 devmem2 直接读写寄存器:绕过驱动,直接操作 GPIO 寄存器,确认硬件没问题。
- 编译时加 -@ 选项:生成符号表,方便用 dtc 反编译后对比。
举个例子,查看 GPIO0_PA5 的状态:
cat /sys/kernel/debug/gpio | grep "PA5"
如果输出显示 gpio-5 且方向是 out,说明配置正确。如果没出现,那就是 PinCtrl 没生效。
嗯,设备树这部分内容不少,但核心就三点:语法要熟、层级要清、GPIO/PinCtrl 要准。把这三点拿捏住,Rockchip 平台上的大部分硬件适配问题都能迎刃而解。