3. 设备树基础概念

在RK3588这类高性能ARM SoC的嵌入式Linux开发中,设备树(Device Tree,DT)是连接硬件描述与内核驱动之间的核心桥梁。与传统的ACPI(x86平台)或硬编码板级文件(早期ARM平台)不同,设备树通过一种结构化的、与硬件描述解耦的数据格式,将“板级硬件拓扑”与“内核驱动代码”彻底分离。对于RK3588开发者而言,理解设备树不仅是编写DTS文件的前提,更是进行引脚复用(IOMUX)、电源域管理、外设时钟配置以及驱动匹配的基础。本章将深入剖析设备树的作用机制、语法结构、节点与属性规则,并重点讲解兼容性字符串与绑定文档的实战意义。

RK3588 设备树核心概念关系图 根节点 "/" soc 节点 #address-cells = <2>; #size-cells = <2>; chosen 节点 bootargs / stdout-path memory 节点 reg = <0x0 0x200000 0x0 0x...> uart0 节点 compatible = "rockchip,rk3588-uart" reg = <0x0 0xfd8a0000 0x0 0x100> i2c2 节点 compatible = "rockchip,rk3588-i2c" clock-frequency = <400000>; pinctrl 节点 gpio0 / iomux-group rockchip,pins = <0 RK_PB2 1>; 核心机制:兼容性字符串与绑定文档 compatible = "vendor,model" → 驱动通过 of_match_table 匹配 绑定文档 (Documentation/devicetree/bindings/) 定义节点属性合法性

上图展示了RK3588设备树的核心概念框架。从根节点出发,系统会划分出soc、chosen、memory等顶层节点,而具体的外设(如UART、I2C、GPIO)则作为子节点挂载在soc节点下。每个节点通过属性描述硬件资源(地址、中断、时钟、引脚),而兼容性字符串则是驱动与节点匹配的关键索引。下面我们将逐一拆解这些概念。

3.1 设备树的作用:为什么RK3588离不开它?

设备树的核心作用可以概括为“硬件描述的标准化与解耦”。在RK3588的Linux开发中,它解决了三个核心痛点:

  • 板级差异隔离:RK3588芯片本身是固定的,但不同开发板(如Rock 5B、香橙派5、自研核心板)的外设配置不同。设备树允许我们通过修改DTS文件来适配不同板卡,而无需重新编译内核镜像。
  • 资源描述集中化:外设的寄存器基地址、中断号、DMA通道、时钟频率、引脚复用功能等,全部以节点和属性的形式集中描述。内核驱动通过标准API(如of_iomap()、platform_get_resource())获取这些资源,避免硬编码。
  • 减少内核代码冗余:在没有设备树的时代,每个板卡都需要在arch/arm/mach-xxx下编写大量板级文件。设备树将这部分信息移出内核,使得RK3588的mainline内核可以一套代码支持多种设备。

在RK3588的实际启动流程中,Bootloader(U-Boot)会加载rk3588-evb.dtb文件,并将其传递给内核。内核在启动阶段解析DTB(Device Tree Blob),构建platform_device,然后与驱动进行匹配。如果设备树描述错误,即使驱动代码正确,外设也无法正常工作。

3.2 设备树的结构:从根节点到叶子节点

设备树是一个具有单一根节点的树状结构。在RK3588的DTS文件中,结构通常如下:

/dts-v1/;
#include "rk3588.dtsi"  // 包含芯片级公共定义

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

    chosen {
        stdout-path = &uart2;
    };

    memory@200000 {
        device_type = "memory";
        reg = <0x0 0x200000 0x0 0x8000000>;  // 起始地址0x200000,大小128MB
    };

    soc {
        #address-cells = <2>;
        #size-cells = <2>;
        
        uart2: serial@fd890000 {
            compatible = "rockchip,rk3588-uart";
            reg = <0x0 0xfd890000 0x0 0x100>;
            interrupts = <GIC_SPI 317 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
            clock-names = "baudclk", "apb_pclk";
            pinctrl-names = "default";
            pinctrl-0 = <&uart2m0_xfer>;
            status = "okay";
        };
    };
};

关键结构要素包括:

  • 根节点"/":包含model(板卡型号)和compatible(板级兼容性)。
  • soc节点:通常定义#address-cells和#size-cells,用于描述子节点reg属性中的地址长度。RK3588是64位SoC,因此使用<2>表示64位地址。
  • 外设节点:每个外设节点包含compatible、reg、interrupts、clocks、pinctrl等属性。节点名称格式为名称@地址,如serial@fd890000。

3.3 节点与属性:设备树的“语法”核心

节点(Node)是设备树的基本单元,属性(Property)是节点的键值对。在RK3588开发中,最常用的属性如下表所示:

属性名 含义 RK3588典型示例
compatible 兼容性字符串,驱动匹配的关键 "rockchip,rk3588-uart"
reg 寄存器地址及范围 <0x0 0xfd890000 0x0 0x100>
interrupts 中断号及触发类型 <GIC_SPI 317 IRQ_TYPE_LEVEL_HIGH>
clocks / clock-names 时钟句柄及名称 <&cru SCLK_UART2> / "baudclk"
pinctrl-0 引脚复用配置 <&uart2m0_xfer>
status 设备状态(okay/disabled) "okay"

在编写节点时,必须遵循以下规则:

  1. 节点名称:小写字母、数字、逗号和连字符,格式为device-type@unit-address。例如i2c@fd880000。
  2. 属性值类型:支持<u32>(尖括号)、"字符串"(双引号)、[二进制](方括号)、&引用(&符号)。
  3. 标签(Label):如uart2:,用于在DTS文件中通过&uart2进行引用追加或修改。

3.4 兼容性字符串与绑定文档:驱动匹配的“身份证”

兼容性字符串是设备树中最重要的属性。它的格式为"厂商,型号",例如"rockchip,rk3588-uart"。内核驱动通过of_match_table来匹配该字符串。匹配规则如下:

  • 驱动中的compatible列表与设备节点中的compatible属性进行完全字符串匹配。
  • 设备节点可以包含多个兼容性字符串,按优先级从高到低排列。例如compatible = "rockchip,rk3588-evb", "rockchip,rk3588",内核会先尝试匹配第一个,失败后再尝试第二个。

绑定文档(Binding Documentation)是设备树节点的“使用说明书”。它位于内核源码的Documentation/devicetree/bindings/目录下。例如,RK3588的UART绑定文档会定义:

Required properties:
- compatible: Should be "rockchip,rk3588-uart"
- reg: Register base address and size
- interrupts: Should contain the UART interrupt

Optional properties:
- dmas: DMA channel specifiers
- clock-frequency: UART clock frequency

在开发中,务必遵循绑定文档,否则即使驱动能匹配,也可能因缺少必要属性而初始化失败。

实战场景:如何为RK3588新增一个SPI设备?

假设我们需要在RK3588的SPI2总线上挂载一个MCP2515 CAN控制器。步骤如下:

  1. 在rk3588.dtsi中找到spi2节点,确认其默认状态为disabled。
  2. 在板级DTS文件中追加子节点:
&spi2 {
    status = "okay";
    mcp2515: can@0 {
        compatible = "microchip,mcp2515";
        reg = <0>;
        clocks = <&cru CLK_REF_MCP2515>;
        interrupt-parent = <&gpio1>;
        interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>;
        spi-max-frequency = <10000000>;
    };
};

这里的关键是compatible必须与内核中mcp251x驱动的of_match_table一致。同时,需要确认CLK_REF_MCP2515时钟已在CRU节点中定义。

3.5 常见误区与总结复盘

在RK3588设备树开发中,以下误区需要特别注意:

误区1:忘记修改status属性。RK3588的.dtsi中大部分外设默认status = "disabled",如果板级文件只添加了子节点而未将父节点设为"okay",设备不会被注册。

误区2:reg地址与芯片手册不一致。RK3588的寄存器地址是固定的,例如UART2基地址为0xFD890000。如果拼写错误,驱动通过of_iomap()映射时会得到错误地址,导致访问异常。

误区3:忽略pinctrl配置。RK3588的引脚复用非常灵活,同一个UART可以映射到多组引脚。如果pinctrl-0未正确指向&uart2m0_xfer,即使驱动加载成功,引脚功能也不正确。

总结复盘:设备树是RK3588 Linux开发的基石。掌握节点、属性、兼容性字符串和绑定文档,是进行后续驱动开发的前提。建议开发者在编写DTS时,先查阅芯片手册确认寄存器地址,再参考内核中的Documentation/devicetree/bindings/目录,最后通过dtc工具编译DTS并检查警告信息。在调试阶段,可以使用ls /proc/device-tree/查看内核实际解析的设备树内容,以验证节点是否被正确加载。