第3章 设备树基础:DTS语法与结构、RK平台编写规范、GPIO与PinCtrl配置
设备树(Device Tree)这东西,刚接触时觉得挺绕。说白了,它就是一块「硬件描述板」,告诉内核:你手上这块板子有哪些外设、地址在哪、中断怎么接、GPIO怎么用。没有它,Linux内核根本不知道该怎么跟你的硬件打交道。
我在RK3399项目上吃过一次亏——设备树里一个GPIO的pinmux配错了,结果屏幕死活点不亮。查了两天才发现,原来是某个引脚被复用了两遍。从那以后,我对设备树的每一个节点都格外小心。
3.1 DTS语法基础——你得像写配置文件一样写它
设备树源文件(.dts)的语法其实不复杂。它用树形结构描述硬件,每个节点代表一个设备或一段总线。我习惯把它看成「硬件版的JSON」。
核心规则:
- 每个节点用
node-name@unit-address命名 - 属性用
key = value;赋值 - 字符串用双引号,数值用十六进制或十进制
- 引用用
&label,包含用#include
// 一个简单的设备树节点示例
/dts-v1/;
/ {
model = "Rockchip RK3568 EVB";
compatible = "rockchip,rk3568-evb", "rockchip,rk3568";
chosen {
stdout-path = &uart2;
};
memory@a0000000 {
device_type = "memory";
reg = <0x0 0xa0000000 0x0 0x80000000>;
};
uart2: serial@fe660000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe660000 0x0 0x100>;
interrupts = ;
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
clock-names = "baudclk", "apb_pclk";
status = "disabled";
};
};
看到没?uart2 这个节点里,reg 描述了寄存器基地址和范围,interrupts 告诉内核中断号,clocks 指定了时钟源。这些信息,内核驱动通过 of_match_table 和 platform_driver 就能自动拿到。
我的习惯:写设备树时,先把 compatible 字符串跟驱动里的 of_device_id 对齐。名字写错了,驱动根本不会 probe。
3.2 RK平台设备树编写规范——别自己造轮子
Rockchip 平台有自己的一套设备树组织方式。你打开内核源码的 arch/arm64/boot/dts/rockchip/ 目录,会发现一堆 .dtsi 文件。这些是 SoC 级描述文件,厂家已经帮你写好了大部分外设节点。
我们做板级适配时,只需要做三件事:
- 包含正确的
.dtsi——比如 RK3568 就包含rk3568.dtsi - 使能需要的节点——把
status = "disabled"改成"okay" - 覆盖或补充属性——比如修改
reg、pinctrl、gpio等
// rk3568-evb.dts 示例
#include "rk3568.dtsi"
/ {
model = "Rockchip RK3568 EVB";
compatible = "rockchip,rk3568-evb", "rockchip,rk3568";
// 板级LED
leds {
compatible = "gpio-leds";
work_led: led-0 {
gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
// 使能UART2
&uart2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart2m0_xfer>;
};
注意:千万不要直接修改 .dtsi 文件。那是 SoC 级描述,改了会影响所有板子。正确的做法是在你的 .dts 里用 &label 引用并覆盖。
RK平台设备树还有一个特点:pinctrl 节点是自动生成的。你只需要在 .dtsi 里找到对应的 pinmux 组,比如 uart2m0_xfer,然后在板级文件里引用就行。我刚开始不知道这个,自己手写 pinctrl 节点,结果编译报错——白费了半天功夫。
3.3 GPIO与PinCtrl配置——别让引脚打架
GPIO 和 PinCtrl 是设备树里最容易出问题的地方。说白了,一个物理引脚只能干一件事。你把它配成 UART 的 TX,就不能同时当 GPIO 用。这就是「引脚复用」的概念。
在 RK 平台上,PinCtrl 配置通常写在 .dtsi 的 pinctrl 节点里。每个功能对应一个子节点,里面指定了哪些引脚、什么功能。
// pinctrl 节点示例(来自 rk3568.dtsi)
pinctrl: pinctrl {
compatible = "rockchip,rk3568-pinctrl";
rockchip,grf = <&grf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
uart2 {
uart2m0_xfer: uart2m0-xfer {
rockchip,pins =
<0 RK_PB1 2 &pcfg_pull_up>, // TX
<0 RK_PB2 2 &pcfg_pull_up>; // RX
};
};
i2c1 {
i2c1m0_xfer: i2c1m0-xfer {
rockchip,pins =
<0 RK_PB3 1 &pcfg_pull_none>, // SCL
<0 RK_PB4 1 &pcfg_pull_none>; // SDA
};
};
};
每个 rockchip,pins 条目包含四个字段:bank、pin、function、pull配置。比如 <0 RK_PB1 2 &pcfg_pull_up> 表示 GPIO0_B1,功能2(UART TX),上拉。
避坑指南:我曾经在 RK3588 项目上,把 I2C 的 SCL 和某个 GPIO 输出配到了同一个引脚上。结果 I2C 总线一直报错,设备时灵时不灵。查了半天才发现是 pinmux 冲突。所以我的建议是——画一张引脚分配表,每个引脚只配一次。
3.4 GPIO 在设备树中的描述方式
GPIO 在设备树里通常用 gpios 或 xxx-gpios 属性来描述。格式是:<gpio-controller bank pin flags>。
| 属性名 | 含义 | 示例 |
|---|---|---|
gpios |
通用 GPIO 引用 | gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>; |
reset-gpios |
复位引脚 | reset-gpios = <&gpio1 RK_PC0 GPIO_ACTIVE_LOW>; |
irq-gpios |
中断引脚 | irq-gpios = <&gpio2 RK_PD3 GPIO_ACTIVE_LOW>; |
GPIO_ACTIVE_HIGH 和 GPIO_ACTIVE_LOW 是标志位,告诉内核这个 GPIO 是「高电平有效」还是「低电平有效」。驱动里用 gpiod_set_value() 时,内核会自动帮你做电平反转。
小技巧:调试 GPIO 时,我经常在 /sys/kernel/debug/gpio 里看引脚状态。如果发现某个引脚方向或电平不对,先查设备树里的 gpios 属性和 pinctrl 配置是否一致。
3.5 知识结构总览
下面这张图,是我自己总结的设备树与硬件适配的核心流程。每次写新板子,我都会对着它过一遍。
嗯,这张图基本概括了设备树适配的全流程。从硬件手册出发,到设备树编写,再到编译加载,每一步都可能踩坑。但只要你把 PinCtrl 和 GPIO 这两块理清楚,剩下的就是体力活了。
最后提醒一句:设备树写完后,一定要用 dtc -I dts -O dtb -o test.dtb test.dts 编译检查。我见过太多人把语法写错了,编译报错还一脸懵。另外,make dtbs 可以批量编译,省事不少。