4. 设备树基础:DTS语法、节点与属性、reg/irq/gpio等常用属性、设备树覆盖(overlay)
设备树(Device Tree)这东西,说白了就是一块“硬件描述板”。你想想看,Linux 内核要跑在千奇百怪的硬件上,总不能每换一块板子就重新编译一次内核吧?设备树就是用来解决这个问题的——它把硬件信息从内核代码里抽出来,变成一份独立的数据结构。
我在 Rockchip 平台上最早接触设备树时,其实挺懵的。那时候 RK3288 刚出来,文档也不全,全靠自己对着内核源码和原理图一点点啃。后来摸透了,才发现这东西的套路其实很固定。
4.1 DTS 语法基础
设备树源文件(.dts)的语法,跟 C 语言的结构体有点像,但更简洁。一个最基本的节点长这样:
/dts-v1/;
/ {
compatible = "rockchip,rk3568-evb";
model = "Rockchip RK3568 EVB Board";
chosen {
stdout-path = &uart2;
};
memory@200000 {
device_type = "memory";
reg = <0x00200000 0x3fe00000>;
};
};
这里有几个关键点:
- /dts-v1/:版本声明,必须放在文件开头
- /:根节点,整棵树从这里开始
- compatible:兼容性字符串,内核靠它匹配驱动
- model:板子名称,调试时很有用
节点命名有个约定:名称@地址。比如 memory@200000,地址部分通常对应片选或基地址,方便阅读。
4.2 节点与属性详解
每个节点都可以包含属性和子节点。属性就是键值对,值可以是字符串、整数数组、二进制数据等。
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "rockchip,rk3568-uart"; |
驱动匹配的关键 |
| 32位整数 | reg = <0xff030000 0x1000>; |
地址+长度 |
| 64位整数 | reg = <0x0 0xfe000000 0x0 0x100000>; |
高32位+低32位 |
| 布尔值 | status = "okay"; |
控制设备使能 |
| 引用 | &uart2 { ... }; |
通过标签引用节点 |
节点引用是我特别喜欢的一个特性。比如你在根 dts 里定义了 uart2: serial@ff650000,然后在板级 dts 里用 &uart2 就能直接追加属性,不用重新写一遍。
避坑指南:我曾经在 RK3399 项目上,因为忘记在根节点里声明 #address-cells = <2>,导致 64 位地址被截断,内存死活只认到 4GB。查了两天才发现是 cells 没配对。
4.3 reg 属性:地址与空间描述
reg 属性是设备树里最常用的属性之一。它描述设备的地址空间,格式是 <地址1 长度1 地址2 长度2 ...>。
地址和长度的位数由 #address-cells 和 #size-cells 决定。这两个属性在父节点里定义,子节点继承。
// 父节点定义 cells
soc {
#address-cells = <2>; // 地址用 2 个 cell(64位)
#size-cells = <2>; // 长度用 2 个 cell(64位)
// 子节点使用 reg
gpu@fde60000 {
compatible = "rockchip,rk3568-mali";
reg = <0x0 0xfde60000 0x0 0x40000>;
// 地址: 0xfde60000, 长度: 0x40000
};
};
为什么会有 64 位地址?因为有些 SoC 的地址空间很大,比如 RK3588 的 PCIe 控制器映射到 64 位地址空间。这时候如果 #address-cells 设成 1,地址就丢了。
注意:reg 里的地址必须是物理地址,不是虚拟地址。内核驱动通过 of_iomap() 或 platform_get_resource() 来获取这些地址并映射。
4.4 interrupts 属性:中断描述
中断属性描述设备如何向 CPU 发中断。格式跟 reg 类似,但内容不同。
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0xfd400000 0x0 0x10000>;
};
uart2: serial@fe660000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe660000 0x0 0x100>;
interrupts = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
};
这里 GIC_SPI 是共享外设中断,115 是中断号,IRQ_TYPE_LEVEL_HIGH 是触发类型。这三个值对应 #interrupt-cells = <3>。
中断号怎么查?我一般看芯片的 TRM(技术参考手册),里面有个中断向量表。比如 RK3568 的 UART2 中断号就是 115。
我的经验:调试中断时,先看 /proc/interrupts 确认中断有没有注册上。如果中断号对但没触发,八成是中断控制器节点没配好,或者 interrupt-parent 没指定。
4.5 gpio 属性:GPIO 描述
GPIO 在设备树里通常用 gpios 或 <name>-gpios 来描述。Rockchip 平台用的是 gpio-ranges 和 pinctrl 配合。
leds {
compatible = "gpio-leds";
power_led: led-0 {
label = "power";
gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
default-state = "on";
};
};
// 在驱动里获取 GPIO
// gpiod_get(&pdev->dev, NULL, GPIOD_OUT_HIGH);
RK_PA5 是 Rockchip 自定义的宏,展开后是 (0 << 8) | 5,表示 GPIO0 组的第 5 脚。这种写法比直接写数字可读性强多了。
GPIO 的复用功能由 pinctrl 子系统管理。比如你想把某个脚配成 UART 功能,需要在 pinctrl 节点里定义:
&pinctrl {
uart2 {
uart2m0_xfer: uart2m0-xfer {
rockchip,pins =
<0 RK_PB4 2 &pcfg_pull_up>,
<0 RK_PB5 2 &pcfg_pull_up>;
};
};
};
这里 2 是功能号,对应 TRM 里的第 2 个复用功能。配错了的话,GPIO 要么没反应,要么乱跳。
避坑指南:我曾经在 RK3568 上调试 SPI 时,发现 MISO 脚一直没数据。查了半天,原来是 pinctrl 里配的功能号写成了 1,实际应该是 3。所以配 GPIO 复用一定要对着原理图和 TRM 反复确认。
4.6 设备树覆盖(Overlay)
设备树覆盖,说白了就是“打补丁”。你有一个基础 dts,然后通过 overlay 追加或修改节点,不用动原始文件。
Overlay 的语法跟普通 dts 几乎一样,但多了 fragment 和 target:
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = &i2c2;
__overlay__ {
status = "okay";
clock-frequency = <400000>;
mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PA0 IRQ_TYPE_EDGE_RISING>;
};
};
};
};
编译 overlay 用 dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts。加载方式有两种:
- 内核启动时:通过
overlay参数指定 dtbo 文件 - 运行时:通过
configfs接口动态加载
运行时加载的步骤:
# 挂载 configfs
mount -t configfs configfs /sys/kernel/config
# 创建 overlay 目录
mkdir /sys/kernel/config/device-tree/overlays/my-overlay
# 写入 dtbo 文件
cat my-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-overlay/dtbo
Overlay 在 Rockchip 平台上特别有用。比如你要接一个外设模块,不想改内核源码,直接写个 overlay 加载就行。我在 RK3588 上调试 MIPI 摄像头时,就是靠 overlay 快速迭代的——改一下配线,重新加载,不用重启。
注意:Overlay 的 target 必须存在,否则加载会失败。另外,overlay 不能删除节点,只能修改或追加。想删除节点?那得改基础 dts。
4.7 知识结构图
下面这张图总结了设备树的核心知识体系,从语法到属性再到 overlay,你可以对照着梳理自己的理解:
4.8 实战建议
学设备树,光看语法是不够的。我建议你拿一块 Rockchip 开发板,对着原理图和 TRM,自己写一份最小系统的 dts。从 chosen 节点开始,加上内存、串口、GPIO LED,一步步来。
调试时多用 dtc -I dtb -O dts 反编译,看看内核实际用的是不是你的配置。还有 /sys/firmware/devicetree/base 这个目录,能直接看到设备树展开后的内容,非常方便。
我的习惯:每次改 dts 后,我都会用 dtc -@ -I dts -O dtb -o test.dtb test.dts 先编译检查语法。如果报错,八成是括号没配对或者 cells 数不对。
设备树覆盖是调试利器。我经常写一个 overlay 来测试新外设,验证通过后再合入正式 dts。这样既不影响主线,又能快速迭代。