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,地址部分通常对应片选或基地址,方便阅读。

我的习惯:写 DTS 时,我会先画一张硬件框图,把 CPU、内存、外设的地址范围标清楚。这样写 reg 属性时不容易出错。

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,你可以对照着梳理自己的理解:

设备树核心知识体系 设备树 (Device Tree) DTS 语法与结构 节点与属性 设备树覆盖 节点命名 / 标签 / 引用 compatible / model / status #address-cells / #size-cells reg:地址与空间描述 interrupts:中断描述 gpios / pinctrl:GPIO 描述 fragment / target / __overlay__ 编译:dtc -@ 加载:configfs / 启动参数 核心原则:设备树只描述硬件,不描述行为 驱动通过 compatible 匹配,通过 reg/interrupts/gpios 获取资源

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。这样既不影响主线,又能快速迭代。