4、设备树基础:设备树语法、节点属性、compatible匹配、在RK3588上的应用

设备树(Device Tree)是嵌入式Linux系统中描述硬件资源的核心机制,它以一种树形结构将CPU、内存、外设控制器、引脚复用、时钟、电源等信息传递给内核。对于RK3588这样一颗拥有四核Cortex-A76与四核Cortex-A55、集成Mali-G610 GPU、支持多路MIPI/PCIe/USB3.1等复杂外设的SoC而言,设备树不仅是驱动与硬件之间的“桥梁”,更是产品级BSP开发中必须掌握的第一项硬技能。本章将从设备树语法出发,深入解析节点与属性的编写规范,重点讲解compatible匹配机制在内核驱动模型中的运作原理,并结合RK3588的典型外设(如I2C、UART、GPIO)给出可直接落地的设备树编写与调试方法。

/ (根节点) cpus #address-cells = <1> memory@0 device_type = "memory" soc compatible = "simple-bus" i2c0: i2c@ff880000 compatible = "rockchip,rk3588-i2c" reg = <0xff880000 0x1000> uart2: serial@feb50000 compatible = "rockchip,rk3588-uart" pinctrl-names = "default" gpio0: gpio@fd8d0000 compatible = "rockchip,gpio-bank" gpio-controller of_match_table[] .compatible = "rockchip,rk3588-i2c" .compatible = "rockchip,rk3568-i2c" compatible匹配 platform_driver probe() 函数被调用 devm_platform_ioremap_resource() I2C外设 UART外设 GPIO控制器 驱动匹配层

上图展示了RK3588设备树的核心结构:根节点下挂载cpus、memory和soc三个顶层节点,其中soc节点作为simple-bus,其子节点对应芯片内部的各种外设控制器。每个外设节点通过compatible属性与内核中的platform_driver进行匹配,匹配成功后驱动框架自动调用probe函数完成设备初始化。这张图是理解设备树与驱动关系的总纲,后续所有内容都将围绕这一结构展开。

4.1 设备树语法与节点属性

4.1.1 核心原理:设备树是“硬件描述语言”

设备树本质上是一种树形数据结构,每个节点代表一个硬件设备或总线,节点通过属性(property)描述硬件的具体特征。在RK3588的BSP中,设备树源文件(.dts)经过设备树编译器(dtc)编译为二进制设备树(.dtb),由U-Boot在启动时传递给内核。内核通过解析.dtb,动态构建platform_device、i2c_client、spi_device等设备实例,进而触发对应驱动的probe。

设备树的基本语法遵循“节点名@单元地址”的约定,例如i2c0: i2c@ff880000中,i2c0是label(标签),i2c@ff880000是节点全名,其中ff880000是该设备在SoC总线上的基地址。节点内部通过属性 = 值;的形式定义硬件参数,常见属性包括:

  • compatible:字符串列表,用于驱动匹配,格式为“厂商,型号”,如"rockchip,rk3588-i2c"。
  • reg:描述设备寄存器地址范围,格式为<地址 长度>,地址和长度的位数由父节点的#address-cells和#size-cells决定。
  • interrupts:描述中断号、触发类型,通常与interrupt-parent配合使用。
  • clocks:引用时钟控制器中的时钟句柄,用于驱动使能外设时钟。
  • pinctrl-0:引脚复用配置,引用pinctrl节点中的引脚状态。

4.1.2 判断方法:如何读懂RK3588的.dtsi文件

RK3588的SDK中,设备树文件分为多个层次:rk3588.dtsi(SoC级描述)、rk3588-evb.dts(板级描述)、rk3588-evb1-ddr4x.dts(具体配置变体)。阅读时建议遵循以下流程:

  1. 先打开rk3588.dtsi,找到soc节点,查看#address-cells = <2>和#size-cells = <2>,这意味着reg属性中的地址和长度均为64位。
  2. 定位目标外设节点,例如i2c0,查看其compatible、reg、interrupts、clocks等基础属性。
  3. 切换到板级.dts文件,查看该节点是否被&i2c0方式引用并追加属性(如status = "okay"、clock-frequency、pinctrl-0)。
  4. 使用dtc -I dtb -O dts -o output.dts rk3588-evb.dtb反编译实际运行的.dtb,确认最终生效的设备树内容。
经验提醒:RK3588的i2c节点在.dtsi中默认status = "disabled",板级文件中必须显式改为"okay"才能被内核枚举。这是新手最容易遗漏的一步。

4.1.3 案例拆解:在RK3588上添加一个I2C触摸屏节点

假设硬件上在I2C0上连接了一颗GSL3673触摸控制器,中断引脚为GPIO4_B5,复位引脚为GPIO4_B6。设备树配置如下:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;  // 400kHz快速模式
    pinctrl-names = "default";
    pinctrl-0 = <&i2c0_xfer>;   // 引脚复用为I2C功能

    gsl3673@40 {
        compatible = "silead,gsl3673";
        reg = <0x40>;              // 7位从机地址
        interrupt-parent = <&pinctrl>;
        interrupts = <RK_GPIO4 RK_PB5 IRQ_TYPE_LEVEL_LOW>;
        reset-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <800>;
        touchscreen-size-y = <1280>;
    };
};

这段配置中,gsl3673@40是子节点,reg = <0x40>表示从机地址;interrupts属性描述了中断引脚和触发条件;reset-gpios引用了GPIO4控制器。驱动侧通过of_get_named_gpio()和gpiod_get()获取这些资源。需要注意的是,RK_GPIO4、RK_PB5这些宏定义在include/dt-bindings/pinctrl/rockchip.h中,编译设备树时必须包含该头文件。

4.1.4 常见误区与总结

  • 误区一:忘记设置status = "okay",导致设备未被枚举。排查方法:ls /dev/i2c-*看不到对应总线。
  • 误区二:reg地址与原理图不一致。RK3588的I2C0基地址是0xff880000,I2C1是0xff881000,写错后驱动ioremap失败。
  • 误区三:中断号使用错误。RK3588的GPIO中断号需要加上GPIO0的基准中断号(32),例如GPIO4_B5对应的SPI中断号是32 + 4*32 + 5 = 165,但设备树中通常直接使用<&gpio4 RK_PB5 IRQ_TYPE_LEVEL_LOW>,由pinctrl子系统自动转换。

4.2 compatible匹配机制

4.2.1 核心原理:从设备树到驱动的“钥匙”

compatible属性是设备树与驱动之间最核心的关联纽带。内核的platform总线在注册platform_device时,会将该节点的compatible字符串列表与驱动中of_match_table数组中的每个条目逐一比较。匹配顺序遵循“最具体优先”原则:如果设备树中写了"rockchip,rk3588-i2c", "rockchip,rk3568-i2c",而驱动表中同时存在这两个条目,则优先匹配第一个。匹配成功后,驱动的probe()函数被调用,并传入struct platform_device指针,驱动通过该指针获取设备树中的资源。

RK3588的I2C控制器驱动(drivers/i2c/busses/i2c-rk3x.c)中的匹配表如下:

static const struct of_device_id rk3x_i2c_match[] = {
    { .compatible = "rockchip,rk3588-i2c", .data = &rk3588_i2c_data },
    { .compatible = "rockchip,rk3568-i2c", .data = &rk3568_i2c_data },
    { .compatible = "rockchip,rk3399-i2c", .data = &rk3399_i2c_data },
    { /* sentinel */ }
};

当设备树节点compatible为"rockchip,rk3588-i2c"时,内核选择rk3588_i2c_data作为该设备的私有数据,其中包含了时钟频率范围、FIFO深度等硬件差异参数。

4.2.2 判断方法:如何确认驱动是否匹配成功

在RK3588开发板上,可以通过以下方法验证匹配结果:

  1. 查看内核日志:dmesg | grep i2c,如果看到i2c 0: Added multiplexed i2c bus或rk3x-i2c ff880000.i2c: Initialized RK3xxx I2C bus at 0xff880000,说明匹配成功。
  2. 查看sysfs:ls /sys/bus/platform/devices/ff880000.i2c/,如果目录存在且包含driver符号链接,说明驱动已绑定。
  3. 使用cat /sys/bus/platform/devices/ff880000.i2c/uevent,查看OF_COMPATIBLE_1=rockchip,rk3588-i2c,确认内核识别到的compatible值。
风险提示:如果驱动匹配表中有多个兼容条目,但设备树只写了其中一个,且该条目对应的data结构体与硬件实际版本不符(例如把rk3588误写成rk3568),可能导致时钟分频计算错误,I2C通信速率异常。务必核对SoC型号。

4.2.3 案例拆解:为RK3588的UART2添加DMA支持

RK3588的UART2在SDK中默认使用中断模式,但硬件上支持DMA传输。设备树中通过dmas属性引用DMA控制器:

&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart2m0_xfer>;
    dmas = <&dmac0 6>, <&dmac0 7>;
    dma-names = "tx", "rx";
};

这里dmac0是RK3588的DMA控制器节点,6和7是DMA请求号。驱动(drivers/tty/serial/8250/8250_dma.c)通过dma_request_chan()获取DMA通道。如果DMA配置错误,驱动会回退到中断模式,但日志中会出现dmaengine_prep_slave_single failed警告。此时应检查dmas中的请求号是否与TRM(技术参考手册)中的UART2 DMA映射一致。

4.2.4 常见误区与总结

  • 误区一:在驱动中直接使用of_device_is_compatible()判断硬件型号,而不是通过of_match_table的data字段。这会导致代码冗余且难以维护。
  • 误区二:修改了设备树但未重新编译dtb,或编译后未烧录到正确分区。RK3588的dtb通常位于boot分区或单独的dtb分区,需确认U-Boot的环境变量fdtfile。
  • 误区三:compatible字符串拼写错误,例如将"rockchip,rk3588-i2c"写成"rockchip,rk3588_i2c"(下划线代替连字符),导致匹配失败。

4.3 在RK3588上的实际应用与调试

4.3.1 核心原理:pinctrl与GPIO的协同

RK3588的引脚复用(Pin Mux)由pinctrl子系统统一管理。设备树中的pinctrl-0属性引用&i2c0_xfer这样的节点,该节点在rk3588-pinctrl.dtsi中定义,指定了引脚编号和复用功能。例如:

i2c0_xfer: i2c0-xfer {
    rockchip,pins =
        <0 RK_PB0 1 &pcfg_pull_none>,
        <0 RK_PB1 1 &pcfg_pull_none>;
};

这表示I2C0的SCL和SDA分别使用GPIO0_B0和GPIO0_B1,复用功能编号为1(即I2C功能),且无上下拉。当驱动调用devm_pinctrl_get()时,内核会应用该配置,将引脚从GPIO模式切换为I2C模式。

4.3.2 判断方法:使用debugfs排查引脚状态

在RK3588的Linux系统中,挂载debugfs后可以查看引脚实时状态:

mount -t debugfs none /sys/kernel/debug
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep i2c0

输出示例:pin 8 (gpio0-8): ff880000.i2c - i2c0_xfer,说明引脚8已被I2C0占用。如果显示(MUX UNCLAIMED),说明pinctrl配置未生效,需要检查pinctrl-names和pinctrl-0是否在正确的节点下。

4.3.3 案例拆解:GPIO LED点灯与设备树的关系

假设RK3588 EVB板上有一颗LED连接在GPIO3_C4上,高电平点亮。设备树配置:

leds {
    compatible = "gpio-leds";
    work_led: led-0 {
        label = "work";
        gpios = <&gpio3 RK_PC4 GPIO_ACTIVE_HIGH>;
        default-state = "off";
    };
};

驱动(drivers/leds/leds-gpio.c)通过devm_gpiod_get()获取GPIO描述符。这里GPIO_ACTIVE_HIGH表示逻辑1对应物理高电平。如果硬件上LED是低电平点亮,应改为GPIO_ACTIVE_LOW,否则会出现“点灯命令相反”的现象。调试时可以使用echo 1 > /sys/class/leds/work/brightness验证。

4.3.4 常见误区与总结

  • 误区一:GPIO编号计算错误。RK3588的GPIO3_C4对应的全局编号是3*32 + 2*8 + 4 = 116,但设备树中应使用<&gpio3 RK_PC4>,不要手动计算数字。
  • 误区二:忘记在板级dts中#include "rk3588-evb.dtsi",导致自定义节点未生效。
  • 误区三:修改设备树后未清理编译缓存,使用make dtbs时使用了旧的.dtb。建议执行make clean后重新编译。
本章核心要点:
  • 设备树是硬件描述语言,节点对应设备,属性描述参数,compatible是驱动匹配的钥匙。
  • RK3588的.dtsi提供SoC级描述,板级.dts通过&节点方式追加配置,status = "okay"是启用外设的关键。
  • pinctrl子系统管理引脚复用,GPIO子系统管理电平读写,两者通过设备树属性协同工作。
  • 调试设备树问题的标准流程:反编译dtb确认实际内容 → 查看dmesg驱动日志 → 检查sysfs设备节点 → 使用debugfs验证引脚状态。