3. 设备树基础(DTS):语法、节点与关键属性详解

设备树,说白了就是一块“硬件描述板”。

你写驱动,总得知道CPU接了什么外设、中断挂在哪条线上、寄存器地址是多少。以前这些信息散落在C代码里,改个引脚就要重新编译内核。后来社区搞出了设备树,把硬件描述和驱动代码彻底解耦。我个人觉得,这是嵌入式Linux发展史上最聪明的设计之一。

核心思想:设备树是一个树形数据结构,用文本文件(.dts)描述硬件,编译成二进制(.dtb)后由Bootloader传递给内核。驱动通过解析设备树来获取硬件资源,不再硬编码。

3.1 设备树语法——其实没那么玄乎

先看一个最简单的例子:

/dts-v1/;

/ {
    model = "Rockchip RK3566 EVB";
    compatible = "rockchip,rk3566-evb", "rockchip,rk3566";

    chosen {
        stdout-path = &uart2;
    };

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

嗯,结构很清晰:

  • /dts-v1/:版本声明,必须写在第一行。
  • /:根节点,整棵树的起点。
  • uart2: serial@fe660000:一个子节点,uart2是标签(label),serial@fe660000是节点名+地址。
  • 属性:compatible、reg、interrupts这些就是节点的“身份证”。

我的习惯:写节点名时,尽量用功能名@基地址的格式,比如i2c@ff3f0000。这样一看就知道是什么外设、地址在哪,调试时省不少事。

3.2 节点与属性——树是怎么长出来的

设备树的节点可以嵌套,就像文件夹套文件夹。每个节点可以有多个属性,属性值可以是字符串、整数、数组、二进制数据等。

属性类型 写法示例 说明
字符串 compatible = "rockchip,rk3566-uart"; 驱动匹配的关键
32位整数 reg = <0xfe660000 0x100>; 地址+长度
64位整数 reg = <0x0 0xfe660000 0x0 0x100>; 高32位+低32位
布尔值 status = "disabled"; 存在即true,不存在即false
数组 interrupts = <0 116 4>; 中断类型+编号+触发方式
引用 &uart2 通过标签引用其他节点

这里有个容易踩的坑——地址位数。我在项目中遇到过,有人把64位地址写成了32位格式,结果驱动死活读不到正确的寄存器基址。你想想看,reg = <0xfe660000 0x100>在32位系统里没问题,但在64位系统里,内核会认为第一个0xfe660000是地址,第二个0x100是长度。如果地址超过32位,必须写成<0x0 0xfe660000 0x0 0x100>,两个cell一组。

3.3 关键属性详解——compatible、reg、interrupts

3.3.1 compatible:驱动匹配的“钥匙”

这是最重要的属性,没有之一。内核通过compatible字符串找到对应的驱动。写法有讲究:

compatible = "rockchip,rk3566-uart", "snps,dw-apb-uart";

第一个字符串是最具体的,第二个是通用的。内核匹配时,先找第一个,找不到再找第二个。这样做的好处是:如果Rockchip的UART有特殊寄存器,可以用第一个字符串绑定专用驱动;如果没有,就退回到通用的DesignWare驱动。

我曾经见过有人把顺序写反了,结果内核总是匹配到通用驱动,专用驱动的优化功能没生效。排查了半天才发现是compatible顺序的问题。记住:越具体越靠前。

3.3.2 reg:告诉驱动“我在哪”

reg描述设备的地址空间。格式是reg = <地址1 长度1 地址2 长度2 ...>。每个地址+长度称为一个reg区域(reg region)。

reg = <0x0 0xfe660000 0x0 0x100>;  // 64位系统:地址0xfe660000,长度0x100

驱动里通过platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到第一个reg区域。如果有多个区域,索引依次递增。

我个人的习惯是:每个外设只用一个reg区域,除非硬件设计上确实有多个不连续的地址块。这样驱动代码更简洁,不容易出错。

3.3.3 interrupts:中断怎么配

中断属性的格式取决于中断控制器。在Rockchip平台上,GIC(通用中断控制器)的格式通常是:

interrupts = <中断类型 中断号 触发方式>;
  • 中断类型:0=SPI(共享外设中断),1=PPI(私有外设中断)。
  • 中断号:从32开始算。比如GIC里的中断号116,实际硬件中断线是116+16=132(因为前16个是CPU内部中断)。
  • 触发方式:4=高电平触发,1=上升沿触发,2=下降沿触发,8=低电平触发。

举个例子:

interrupts = <0 116 4>;  // SPI中断,硬件中断号132,高电平触发

避坑指南:不同厂商的中断号定义可能不一样。Rockchip的TRM里写的中断号通常是硬件中断号(从0开始),而设备树里用的是GIC中断号(从32开始)。我曾经因为没搞清楚这个偏移,配出来的中断一直不触发。后来对着TRM和内核代码逐行比对才找到问题。

3.4 知识体系总览

下面这张图概括了设备树的核心结构,我建议你把它存下来,写设备树时对照着看:

设备树核心知识体系 根节点 / cpus { } memory { } chosen { } uart2: serial@... i2c0: i2c@... gpio0: gpio@... compatible 驱动匹配字符串 reg 地址+长度 interrupts 中断配置 clocks / clock-names pinctrl-0 / pinctrl-names status 每个节点通过 compatible 匹配驱动,通过 reg 获取地址,通过 interrupts 绑定中断 其他属性(clocks、pinctrl、status)提供更细粒度的硬件配置

3.5 实战建议——从看懂到会写

刚开始接触设备树,别急着自己从头写。我的做法是:

  1. 找一块开发板的完整dts,比如Rockchip官方SDK里的rk3566-evb.dts。
  2. 对照原理图,一个一个节点看过去。看它的compatible是什么、reg地址对不对、中断号是否匹配。
  3. 改一个节点试试,比如把UART的status从"disabled"改成"okay",重新编译dtb,看内核能不能识别。
  4. 用dtc工具反编译:dtc -I dtb -O dts -o output.dts input.dtb,看看实际生效的设备树长什么样。

一个小技巧:在/sys/firmware/devicetree/base/目录下,你可以直接看到内核解析后的设备树结构。每个属性都是一个文件,用cat就能读出原始二进制数据。调试时特别有用。