第3章 设备树基础:DTS语法详解与RK平台层级结构

设备树(Device Tree)这东西,说白了就是一块「硬件描述板」。Linux内核通过它来知道你的板子上接了哪些外设、用的哪个引脚、电压多少。我刚开始接触RK平台时,觉得DTS就是个配置文件,后来踩了几次坑才明白——它其实是驱动和硬件之间的桥梁,写错了,内核根本认不出你的设备。

3.1 DTS语法速览

DTS的语法并不复杂,核心就几个概念:节点、属性、引用。我们直接看一个例子:

/dts-v1/;

/ {
    model = "Rockchip RK3568 EVB";
    compatible = "rockchip,rk3568-evb", "rockchip,rk3568";

    chosen {
        stdout-path = &uart2;
    };

    memory {
        device_type = "memory";
        reg = <0x0 0x00200000 0x0 0x80000000>;
    };
};

这里有几个要点:

  • 根节点:/ 表示设备树的根,所有设备都挂在这个下面
  • 属性:compatible 是匹配驱动的关键字符串,内核通过它找到对应的驱动
  • reg:描述地址和大小,格式是 <地址 长度>
  • &uart2:这是引用语法,指向其他地方定义的节点
我的习惯:写DTS时,我总会在每个节点上加一个 status = "okay" 或 "disabled"。这样调试时一眼就能看出哪些设备被激活了,不用去翻原理图。

3.2 RK平台设备树层级结构

RK平台的设备树是分层的,一共三层。我画了张图,你看完就明白了:

RK平台设备树层级结构 soc.dtsi 芯片级:CPU、GIC、DMA、Timer rk3568.dtsi / rk3588.dtsi 系列级:I2C、SPI、UART、PWM、SDMMC pinctrl、时钟、复位、电源域 board.dts(如 rk3568-evb.dts) 板级:GPIO复用、外设使能、 regulators LCD、Camera、Ethernet、USB 具体配置 编译顺序:soc.dtsi → rkxxx.dtsi → board.dts 后层覆盖前层同名属性,实现「芯片→系列→板级」逐层定制 最终生成单一 dtb 文件供内核使用

这三层的关系,我总结一下:

层级 文件名 内容范围 修改频率
芯片级 soc.dtsi CPU、GIC、定时器、DMA等 几乎不动
系列级 rk3568.dtsi 外设控制器、pinctrl、时钟 SDK升级时更新
板级 board.dts GPIO分配、外设使能、电源 经常改
我曾经踩过的坑:有次我在 board.dts 里改了一个 GPIO 的 pinctrl 配置,但死活不生效。查了半天才发现,rkxxx.dtsi 里已经定义了这个引脚的功能,而且 status = "okay" 写在系列级文件里。board.dts 的覆盖没生效,因为编译顺序是反的。记住:后编译的覆盖先编译的,board.dts 最后编译,所以它的优先级最高。

3.3 pinctrl与GPIO配置

pinctrl 是RK平台里最容易出问题的地方。它的作用就是告诉芯片:这个引脚现在是 GPIO 还是 I2C 的 SCL?上拉还是下拉?驱动能力多大?

看一个典型的 pinctrl 配置:

&pinctrl {
    i2c1 {
        i2c1_xfer: i2c1-xfer {
            rockchip,pins =
                <2 RK_PB0 1 &pcfg_pull_up>,
                <2 RK_PB1 1 &pcfg_pull_up>;
        };
    };

    lcd_rst {
        lcd_rst_gpio: lcd-rst-gpio {
            rockchip,pins =
                <3 RK_PC5 0 &pcfg_pull_none>;
        };
    };
};

这里 rockchip,pins 的格式是:

  • bank号:GPIO组,如 2 表示 GPIO2
  • 引脚号:RK_PB0 表示 Bank B 的第 0 脚
  • 功能号:0 表示 GPIO,1 表示 I2C,2 表示 SPI 等
  • 配置:上拉、下拉、悬空等

关键点:功能号 0 永远是 GPIO 模式。如果你想让一个引脚做普通 GPIO,功能号写 0 就行。我见过有人把功能号写成 1,结果 GPIO 死活拉不高的案例。

3.4 在设备树中引用GPIO

外设驱动里怎么用这些引脚?通过 gpios 属性:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;

    touch@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
        interrupt-parent = <&gpio3>;
        interrupts = <RK_PC5 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>;
    };
};

注意这里的写法:

  • interrupt-parent 指定中断控制器是哪个 GPIO bank
  • interrupts 里写引脚号和触发方式
  • reset-gpios 是自定义属性,驱动里通过 of_get_named_gpio 来获取
我个人的调试技巧:当 GPIO 不工作时,先别急着改驱动。在 /sys/kernel/debug/gpio 里看一下引脚状态,确认内核确实把它设成了输出/输入。如果 debugfs 里显示的状态和预期不符,99% 是 pinctrl 配置错了。

3.5 常见编译与调试命令

最后,给你几个我常用的命令,省得每次去翻文档:

操作 命令 说明
编译设备树 make dtbs 编译所有设备树
编译单个 make rk3568-evb.dtb 只编译目标板
反编译 dtc -I dtb -O dts -o tmp.dts rk3568-evb.dtb 查看最终生成的设备树
检查语法 dtc -I dts -O dtb -o /dev/null board.dts 只检查不输出
注意:反编译出来的 dts 和源文件可能不完全一样,因为编译过程中有些属性会被展开或合并。但用来排查「某个属性到底有没有生效」非常有用。我每次改完设备树,都会反编译确认一下最终结果。