第3章 设备树基础:DTS语法与结构、RK平台编写规范、GPIO与PinCtrl配置

设备树(Device Tree)这东西,刚接触时觉得挺绕。说白了,它就是一块「硬件描述板」,告诉内核:你手上这块板子有哪些外设、地址在哪、中断怎么接、GPIO怎么用。没有它,Linux内核根本不知道该怎么跟你的硬件打交道。

我在RK3399项目上吃过一次亏——设备树里一个GPIO的pinmux配错了,结果屏幕死活点不亮。查了两天才发现,原来是某个引脚被复用了两遍。从那以后,我对设备树的每一个节点都格外小心。

3.1 DTS语法基础——你得像写配置文件一样写它

设备树源文件(.dts)的语法其实不复杂。它用树形结构描述硬件,每个节点代表一个设备或一段总线。我习惯把它看成「硬件版的JSON」。

核心规则:

  • 每个节点用 node-name@unit-address 命名
  • 属性用 key = value; 赋值
  • 字符串用双引号,数值用十六进制或十进制
  • 引用用 &label,包含用 #include
// 一个简单的设备树节点示例
/dts-v1/;

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

    chosen {
        stdout-path = &uart2;
    };

    memory@a0000000 {
        device_type = "memory";
        reg = <0x0 0xa0000000 0x0 0x80000000>;
    };

    uart2: serial@fe660000 {
        compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
        reg = <0x0 0xfe660000 0x0 0x100>;
        interrupts = ;
        clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
        clock-names = "baudclk", "apb_pclk";
        status = "disabled";
    };
};

看到没?uart2 这个节点里,reg 描述了寄存器基地址和范围,interrupts 告诉内核中断号,clocks 指定了时钟源。这些信息,内核驱动通过 of_match_table 和 platform_driver 就能自动拿到。

我的习惯:写设备树时,先把 compatible 字符串跟驱动里的 of_device_id 对齐。名字写错了,驱动根本不会 probe。

3.2 RK平台设备树编写规范——别自己造轮子

Rockchip 平台有自己的一套设备树组织方式。你打开内核源码的 arch/arm64/boot/dts/rockchip/ 目录,会发现一堆 .dtsi 文件。这些是 SoC 级描述文件,厂家已经帮你写好了大部分外设节点。

我们做板级适配时,只需要做三件事:

  1. 包含正确的 .dtsi——比如 RK3568 就包含 rk3568.dtsi
  2. 使能需要的节点——把 status = "disabled" 改成 "okay"
  3. 覆盖或补充属性——比如修改 reg、pinctrl、gpio 等
// rk3568-evb.dts 示例
#include "rk3568.dtsi"

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

    // 板级LED
    leds {
        compatible = "gpio-leds";
        work_led: led-0 {
            gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

// 使能UART2
&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart2m0_xfer>;
};

注意:千万不要直接修改 .dtsi 文件。那是 SoC 级描述,改了会影响所有板子。正确的做法是在你的 .dts 里用 &label 引用并覆盖。

RK平台设备树还有一个特点:pinctrl 节点是自动生成的。你只需要在 .dtsi 里找到对应的 pinmux 组,比如 uart2m0_xfer,然后在板级文件里引用就行。我刚开始不知道这个,自己手写 pinctrl 节点,结果编译报错——白费了半天功夫。

3.3 GPIO与PinCtrl配置——别让引脚打架

GPIO 和 PinCtrl 是设备树里最容易出问题的地方。说白了,一个物理引脚只能干一件事。你把它配成 UART 的 TX,就不能同时当 GPIO 用。这就是「引脚复用」的概念。

在 RK 平台上,PinCtrl 配置通常写在 .dtsi 的 pinctrl 节点里。每个功能对应一个子节点,里面指定了哪些引脚、什么功能。

// pinctrl 节点示例(来自 rk3568.dtsi)
pinctrl: pinctrl {
    compatible = "rockchip,rk3568-pinctrl";
    rockchip,grf = <&grf>;
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    uart2 {
        uart2m0_xfer: uart2m0-xfer {
            rockchip,pins =
                <0 RK_PB1 2 &pcfg_pull_up>,  // TX
                <0 RK_PB2 2 &pcfg_pull_up>;  // RX
        };
    };

    i2c1 {
        i2c1m0_xfer: i2c1m0-xfer {
            rockchip,pins =
                <0 RK_PB3 1 &pcfg_pull_none>,  // SCL
                <0 RK_PB4 1 &pcfg_pull_none>;  // SDA
        };
    };
};

每个 rockchip,pins 条目包含四个字段:bank、pin、function、pull配置。比如 <0 RK_PB1 2 &pcfg_pull_up> 表示 GPIO0_B1,功能2(UART TX),上拉。

避坑指南:我曾经在 RK3588 项目上,把 I2C 的 SCL 和某个 GPIO 输出配到了同一个引脚上。结果 I2C 总线一直报错,设备时灵时不灵。查了半天才发现是 pinmux 冲突。所以我的建议是——画一张引脚分配表,每个引脚只配一次。

3.4 GPIO 在设备树中的描述方式

GPIO 在设备树里通常用 gpios 或 xxx-gpios 属性来描述。格式是:<gpio-controller bank pin flags>。

属性名 含义 示例
gpios 通用 GPIO 引用 gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>;
reset-gpios 复位引脚 reset-gpios = <&gpio1 RK_PC0 GPIO_ACTIVE_LOW>;
irq-gpios 中断引脚 irq-gpios = <&gpio2 RK_PD3 GPIO_ACTIVE_LOW>;

GPIO_ACTIVE_HIGH 和 GPIO_ACTIVE_LOW 是标志位,告诉内核这个 GPIO 是「高电平有效」还是「低电平有效」。驱动里用 gpiod_set_value() 时,内核会自动帮你做电平反转。

小技巧:调试 GPIO 时,我经常在 /sys/kernel/debug/gpio 里看引脚状态。如果发现某个引脚方向或电平不对,先查设备树里的 gpios 属性和 pinctrl 配置是否一致。

3.5 知识结构总览

下面这张图,是我自己总结的设备树与硬件适配的核心流程。每次写新板子,我都会对着它过一遍。

设备树与硬件适配核心流程 设备树与硬件适配核心流程 硬件原理图 / 数据手册 设备树编写(.dts + .dtsi) 节点/属性定义 PinCtrl 引脚复用 GPIO 配置 dtc 编译生成 .dtb 内核解析 .dtb → 驱动 probe → 硬件工作 常见问题 • 引脚复用冲突 • GPIO 电平反了 • compatible 不匹配 • 时钟/中断号错误 • status 未改 okay • 忘记包含 .dtsi • reg 地址范围不对 • pinctrl 引用错误 • 多个设备抢同一个 中断号

嗯,这张图基本概括了设备树适配的全流程。从硬件手册出发,到设备树编写,再到编译加载,每一步都可能踩坑。但只要你把 PinCtrl 和 GPIO 这两块理清楚,剩下的就是体力活了。

最后提醒一句:设备树写完后,一定要用 dtc -I dts -O dtb -o test.dtb test.dts 编译检查。我见过太多人把语法写错了,编译报错还一脸懵。另外,make dtbs 可以批量编译,省事不少。