4. 设备树基础(DTS):从语法到Rockchip实战

设备树,说白了就是描述硬件的一张“地图”。Linux内核通过它才知道你板子上接了哪些外设、内存多大、中断怎么连。我刚开始接触嵌入式Linux时,总觉得DTS就是个配置文件,后来踩了几次坑才明白——它其实是驱动和硬件之间的“翻译官”。

核心认知: 设备树不是C代码,也不是脚本,它是一种描述硬件拓扑结构的数据结构。内核在启动时解析它,然后动态创建platform_device等设备实例。

4.1 DTS语法入门:节点、属性、reg、interrupts

先看一个最简单的设备树节点长什么样:

/dts-v1/;

/ {
    model = "Rockchip RK3588 EVB Board";
    compatible = "rockchip,rk3588-evb", "rockchip,rk3588";

    chosen {
        stdout-path = &uart2;
    };

    memory@a00000 {
        device_type = "memory";
        reg = <0x0 0xa00000 0x0 0x80000000>;
    };

    uart2: serial@fe660000 {
        compatible = "rockchip,rk3588-uart", "snps,dw-apb-uart";
        reg = <0x0 0xfe660000 0x0 0x100>;
        interrupts = <0 252 4>;
        clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
        clock-names = "baudclk", "apb_pclk";
        status = "disabled";
    };
};

这里有几个关键概念,我一个个说。

4.1.1 节点(Node)

每个硬件设备或总线都是一个节点。节点用{ }包裹,可以嵌套。节点名格式是name@address,比如serial@fe660000。@后面的地址只是为了区分同名节点,不是必须的。

4.1.2 属性(Property)

属性就是键值对。值可以是字符串、32位整数数组、64位整数数组、字节数组等。常见的属性有:

  • compatible:驱动匹配的关键字。内核通过它找到对应的驱动。
  • status:设备状态,okay表示启用,disabled表示禁用。
  • model:板子型号描述。

4.1.3 reg属性——地址资源

reg描述设备的地址空间。格式是<地址 长度>,可以有多组。比如:

reg = <0x0 0xfe660000 0x0 0x100>;

这表示UART2的寄存器基地址是0xfe660000,映射范围0x100字节。为什么是两组<地址 长度>?因为RK3588是64位SoC,地址用两个32位单元表示高32位和低32位。

我的经验: 写reg时一定要看datasheet确认地址范围。我曾经把长度写小了,结果驱动访问寄存器时触发总线错误,查了两天才发现是reg长度不够。

4.1.4 interrupts属性——中断资源

interrupts描述设备使用的中断号。格式取决于中断控制器。在RK3588上,GIC(通用中断控制器)使用三个单元:

interrupts = <0 252 4>;
  • 第一个值:中断类型,0=SPI(共享外设中断),1=PPI(私有外设中断)
  • 第二个值:中断号,252是UART2的中断号
  • 第三个值:触发方式,4=高电平触发,1=上升沿触发
注意: 中断号是硬件相关的,不同SoC的中断号映射不同。RK3588的GIC有480个中断源,但设备树里只用到一部分。查中断号一定要看芯片手册的interrupt mapping表。

4.2 Rockchip平台DTS文件结构

Rockchip的DTS文件组织方式很有代表性。我拿RK3588举例,它的设备树文件分三层:

文件 作用 包含关系
rk3588.dtsi SoC级描述,定义所有内部外设、时钟、中断控制器、总线等 被所有RK3588板级dts包含
rk3588-evb.dts 板级描述,定义具体板子上的外设(如HDMI、USB、以太网PHY等) #include "rk3588.dtsi"
rk3588-evb.dtsi 板级公共部分,如果多个板型共用部分外设,可以抽出来 被rk3588-evb.dts包含

为什么要分层?说白了就是复用。SoC厂商(Rockchip)提供rk3588.dtsi,板卡厂商只需要写自己的板级dts,然后#include进去,再修改或添加板级特有的节点就行。

4.2.1 rk3588.dtsi 里有什么?

打开这个文件,你会看到几百个节点。我挑几个典型的:

  • cpus:CPU核心描述,4个Cortex-A76 + 4个Cortex-A55
  • gic:中断控制器,GIC-600
  • cru:时钟复位单元,管理所有时钟和复位信号
  • i2c0 ~ i2c8:9个I2C控制器
  • uart0 ~ uart9:10个UART控制器
  • pinctrl:引脚复用控制器

每个外设节点都定义了reg、interrupts、clocks等基础属性。板级dts只需要设置status = "okay",然后配置pinctrl引脚复用即可。

4.2.2 rk3588-evb.dts 怎么用?

看一个实际例子:

// rk3588-evb.dts
/dts-v1/;
#include "rk3588.dtsi"

&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart2m0_xfer>;
};

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
    
    hym8563: rtc@51 {
        compatible = "haoyu,hym8563";
        reg = <0x51>;
        status = "okay";
    };
};

注意这里的&uart2语法——这是设备树的“引用”机制。它表示修改rk3588.dtsi中定义的uart2节点,添加或覆盖属性。这样就不用把整个节点复制一遍。

关键点: 设备树编译时会把所有&标签的修改合并到原节点上。如果板级dts和SoC dtsi定义了相同的属性,板级dts的优先级更高。

4.3 编译与反编译DTB

DTS是文本文件,内核不能直接解析。需要编译成二进制DTB文件。工具链是dtc(Device Tree Compiler)。

4.3.1 编译DTS到DTB

# 基本编译
dtc -I dts -O dtb -o rk3588-evb.dtb rk3588-evb.dts

# 带调试信息(方便定位错误)
dtc -I dts -O dtb -o rk3588-evb.dtb rk3588-evb.dts -@

# 检查语法错误
dtc -I dts -O dtb rk3588-evb.dts 2>&1 | head -20

如果编译报错,最常见的原因是:

  • 缺少#include的头文件(如.dtsi文件路径不对)
  • 引用了未定义的标签(比如&uart2但uart2没定义)
  • 属性值格式错误(比如reg少了一个<>)

4.3.2 反编译DTB到DTS

调试时经常需要看实际运行的设备树是什么样。反编译命令:

# 反编译DTB
dtc -I dtb -O dts -o rk3588-evb-decompiled.dts rk3588-evb.dtb

# 查看设备树结构(只显示节点名)
dtc -I dtb -O dts rk3588-evb.dtb | grep "^[a-zA-Z]" | head -30
我的习惯: 每次修改DTS后,我都会反编译一遍生成的DTB,确认修改生效了。因为有时候#include路径写错,编译没报错但实际没包含进去。反编译一看就知道。

4.3.3 在Linux内核中编译DTS

如果你在内核源码树里工作,更推荐用内核的编译系统:

# 只编译设备树
make ARCH=arm64 dtbs

# 编译指定板子的设备树
make ARCH=arm64 rk3588-evb.dtb

# 编译后输出在 arch/arm64/boot/dts/rockchip/ 下

内核编译系统会自动处理#include依赖和头文件路径,比手动调用dtc省心很多。

4.4 设备树知识体系总览

下面这张图总结了本章的核心内容,从DTS语法到编译部署的完整链路:

设备树知识体系与编译链路 DTS语法基础 节点(Node) 属性(Property) reg / interrupts Rockchip文件结构 rk3588.dtsi(SoC级) rk3588-evb.dts(板级) #include 与 &引用 编译与反编译 dtc 工具链 DTS → DTB → DTS 内核 dtbs 编译 部署到目标板 烧录DTB到boot分区 / 通过uboot加载 / 内核解析并创建设备 调试与验证 /proc/device-tree 查看运行时设备树 | 反编译对比 | dmesg 错误信息 💡 核心流程:编写DTS → dtc编译 → 部署DTB → 内核解析 → 驱动匹配 调试时通过反编译和 /proc/device-tree 验证设备树是否被正确解析

4.5 避坑指南

最后分享几个我实际踩过的坑:

  • 中断号写错:我曾经把SPI中断号写成了PPI,结果驱动注册中断失败,内核报irq: no irq handler。查了半天才发现是中断类型字段写错了。
  • reg地址不对齐:RK3588的某些外设要求寄存器地址按4KB对齐。如果reg的起始地址没对齐,驱动访问时可能触发对齐异常。
  • 忘记设置status:SoC dtsi里默认status = "disabled",板级dts必须显式设置status = "okay"。我刚开始经常忘记,结果设备树里明明有节点,内核就是不枚举。
  • pinctrl配置冲突:两个外设用了同一个引脚,编译不报错,但运行时只有一个能工作。这种问题最难查,建议用pinctrl的调试接口检查引脚复用状态。
重要提醒: 设备树编译错误有时不会直接报“语法错误”,而是报“找不到xxx节点”或“属性值类型不匹配”。这时候先检查#include路径和标签引用是否正确。我一般先用dtc -I dts -O dts做一次“格式化”,看看展开后的文件是不是预期的样子。