3. 设备树基础:DTS语法、Rockchip平台设备树结构、GPIO与PinCtrl配置

设备树(Device Tree)是什么?说白了,它就是一块「硬件描述板」。以前内核里塞满了各种板级文件,换个引脚就要重新编译内核,太折腾了。Rockchip 平台更是如此——芯片型号多、引脚复用复杂,没有设备树,你连灯都点不亮。

我个人习惯把设备树看作「硬件与驱动的契约」。驱动工程师写代码,硬件工程师画原理图,设备树就是中间那个翻译官。今天我们就把它彻底讲透。

3.1 DTS 语法速通

先看一个最简单的节点:

/dts-v1/;

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

    chosen {
        stdout-path = &uart2;
    };
};

这里有几个关键点:

  • /dts-v1/:版本声明,必须写在第一行。
  • /:根节点,整棵树从这里开始。
  • model:板子名字,给人看的。
  • compatible:匹配字符串,内核靠它找到对应驱动。顺序很重要——越具体越靠前。

节点里可以放属性,属性值可以是字符串、数字、数组,甚至二进制数据。举个例子:

memory@200000 {
    device_type = "memory";
    reg = <0x00200000 0x3fe00000>;
};

reg 表示地址和长度,格式是 <address length>。多段内存就写多组:

reg = <0x00200000 0x3fe00000>,
      <0x40000000 0x40000000>;

小技巧:我刚开始写 DTS 时经常搞混 reg 的顺序。记住一个口诀——「地址在前,长度在后,单位是字节」。

引用节点用 &label 语法。比如:

&uart2 {
    status = "okay";
};

这比重新写一遍节点优雅多了。你想想看,如果每个板子都复制粘贴整个 uart2 节点,那设备树得膨胀成什么样。

3.2 Rockchip 平台设备树结构

Rockchip 的设备树分三层:芯片级 .dtsi → 核心板级 .dtsi → 底板级 .dts。我画了张图,你看一眼就明白了:

Rockchip 设备树层级结构 rk3568.dtsi(芯片级) CPU、GIC、Timer、PLL、总线 rk3568-evb.dtsi(核心板级) DDR 参数、PMIC、eMMC、SD 卡 rk3568-evb1-ddr4-v10.dts(底板级) GPIO 复用、LCD、Camera、USB HUB 🔍 包含关系: rk3568-evb1-ddr4-v10.dts → #include "rk3568-evb.dtsi" → #include "rk3568.dtsi" 越下层越具体,越上层越通用。核心板厂商提供 .dtsi,底板厂商只需写 .dts。

这种分层设计的好处很明显:芯片厂商维护 .dtsi,核心板厂商维护另一个 .dtsi,底板厂商只写自己的 .dts。各司其职,互不干扰。

我记得第一次接触 RK3399 时,看到 #include 链差点晕了。后来发现,只要顺着 #include 往上找,总能找到源头。内核编译时会把所有 #include 展开成一个完整的 .dts,然后编译成 .dtb。

3.3 GPIO 与 PinCtrl 配置

这部分是实战中的重灾区。很多新手点不亮 LED,十有八九是 PinCtrl 没配对。

3.3.1 GPIO 基础属性

一个典型的 GPIO 节点长这样:

&gpio0 {
    led-gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
};

这里 RK_PA5 是 Rockchip 自定义的宏,展开后就是 5。GPIO_ACTIVE_HIGH 表示高电平有效。如果你想要低电平有效,就改成 GPIO_ACTIVE_LOW。

注意:我曾经遇到一个坑——GPIO 编号搞错了。RK3568 的 GPIO0 有 32 个引脚,PA0 对应 0,PA5 对应 5。但有些芯片的 GPIO 编号不是连续的,一定要查 datasheet 确认。

3.3.2 PinCtrl 配置

PinCtrl 负责引脚复用和电气属性配置。看这个例子:

&pinctrl {
    led {
        led_pin: led-pin {
            rockchip,pins =
                <0 RK_PA5 1 &pcfg_pull_none>;
        };
    };
};

每个 rockchip,pins 条目包含四个字段:

字段 含义 示例值
bank GPIO 组号 0(GPIO0)
pin 引脚编号 RK_PA5(5)
func 功能编号 1(GPIO 功能)
config 电气配置 &pcfg_pull_none

func 编号是关键。0 通常是保留,1 是 GPIO,2 以上是复用功能。比如 UART2 的 TX 可能是 func 2,RX 可能是 func 3。具体值要看 datasheet 里的 Pin Mux Table。

个人经验:我习惯把 PinCtrl 节点按功能分组,比如 led、uart2、i2c1。这样后期维护时一目了然。别把所有引脚塞到一个节点里,那会把自己逼疯。

3.3.3 完整示例:点亮一个 LED

把上面两部分串起来,就是一个完整的 LED 节点:

// 在根节点下添加
leds {
    compatible = "gpio-leds";
    status = "okay";

    work_led: led-0 {
        label = "work";
        gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
        linux,default-trigger = "heartbeat";
        pinctrl-names = "default";
        pinctrl-0 = <&led_pin>;
    };
};

// 在 pinctrl 节点下添加
&pinctrl {
    led {
        led_pin: led-pin {
            rockchip,pins =
                <0 RK_PA5 1 &pcfg_pull_none>;
        };
    };
};

这里 pinctrl-names 和 pinctrl-0 把 LED 节点和 PinCtrl 配置关联起来。内核在 probe 时会自动应用这个配置。

为什么需要 pinctrl-names?因为一个设备可能有多种状态,比如 default、sleep、active。驱动可以根据电源状态切换引脚配置。不过大部分场景只用 default。

3.4 避坑指南

这些年我在 Rockchip 平台上踩过的坑,列几个典型的:

  • 引脚冲突:两个节点用了同一个引脚,但 func 不同。编译不会报错,运行时只有一个功能生效。我建议每次改完 DTS 都 grep 一下引脚号,确认没有被其他地方占用。
  • 漏配电源域:有些 GPIO 需要先打开对应的电源域(比如 vccio1),否则电平不对。我曾经因为漏配 vcc3v3_sys,导致 SD 卡检测脚一直低电平,折腾了两天。
  • status 忘写 "okay":默认是 "disabled",不写就等于没配。这个低级错误我犯过不止一次。
  • 中断号搞错:GPIO 中断号不是随便写的。RK3568 的 GPIO0 中断号是 32,GPIO1 是 33,以此类推。具体数值看 interrupt-cells 定义。

核心原则:设备树不是写一次就完事的。每次硬件改版,都要同步更新 DTS。我见过太多「硬件改了,DTS 没改」的案例,结果点不亮、跑不稳,最后查出来是引脚没对上。

3.5 调试方法

配完 DTS 怎么验证?我一般用这几招:

  1. 查看 /sys/kernel/debug/gpio:确认 GPIO 状态、方向、电平。
  2. 查看 /sys/kernel/debug/pinctrl/pinctrl-handles:确认 PinCtrl 是否被正确应用。
  3. 用 devmem2 直接读写寄存器:绕过驱动,直接操作 GPIO 寄存器,确认硬件没问题。
  4. 编译时加 -@ 选项:生成符号表,方便用 dtc 反编译后对比。

举个例子,查看 GPIO0_PA5 的状态:

cat /sys/kernel/debug/gpio | grep "PA5"

如果输出显示 gpio-5 且方向是 out,说明配置正确。如果没出现,那就是 PinCtrl 没生效。

嗯,设备树这部分内容不少,但核心就三点:语法要熟、层级要清、GPIO/PinCtrl 要准。把这三点拿捏住,Rockchip 平台上的大部分硬件适配问题都能迎刃而解。