4. 设备树实战:GPIO控制器绑定、时钟控制器绑定、pinctrl子系统绑定、中断控制器绑定

设备树这东西,说白了就是硬件的“户口本”。内核启动时靠它来认设备、配资源。今天咱们就把它最核心的四个绑定讲透——GPIO、时钟、pinctrl、中断。这四个绑定了,你基本就能搞定90%的嵌入式设备树。

核心观点:设备树绑定的本质,就是告诉内核“这个设备用哪个引脚、吃多少时钟、怎么配置IO、中断挂在哪”。四个绑定缺一不可,顺序错了也不行。

4.1 GPIO控制器绑定——管脚资源的“身份证”

GPIO绑定,我习惯把它分成两层:控制器节点和使用者节点。控制器节点描述硬件本身,使用者节点描述谁在用哪个引脚。

4.1.1 控制器节点怎么写

先看一个典型的GPIO控制器节点:

gpio0: gpio@ff730000 {
    compatible = "rockchip,gpio-bank";
    reg = <0x0 0xff730000 0x0 0x100>;
    interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_gpio0>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

这里有几个关键点:

  • gpio-controller:声明这是个GPIO控制器,没有这个属性,内核不认。
  • #gpio-cells = <2>:表示每个GPIO引用需要2个参数——引脚号和标志位。
  • interrupt-controller:GPIO还能当中断控制器用,这个后面细说。

我的经验:#gpio-cells的值一定要和驱动匹配。我见过有人写3,驱动却只解析2,结果引脚号全错位,灯死活不亮。查了两天才发现是cells数对不上。

4.1.2 使用者节点怎么引用

设备要使用GPIO,通过gpios或xxx-gpios属性引用:

&led {
    compatible = "gpio-leds";
    led-red {
        gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_LOW>;
        label = "red-led";
    };
};

这里RK_PA5是引脚号,GPIO_ACTIVE_LOW是标志位。标志位常用值:

宏定义 值 含义
GPIO_ACTIVE_HIGH 0 高电平有效
GPIO_ACTIVE_LOW 1 低电平有效
GPIO_OPEN_DRAIN 2 开漏输出
GPIO_OPEN_SOURCE 3 开源输出

注意:GPIO_ACTIVE_LOW不是把电平拉低,而是告诉驱动“这个引脚低电平时表示有效状态”。我曾经在项目里搞反了,按键按下是低电平,我配成ACTIVE_HIGH,结果中断一直触发,CPU根本睡不了觉。

4.2 时钟控制器绑定——设备的“心跳”

时钟绑定比GPIO复杂一些,因为时钟有父子关系、频率计算、使能控制。我个人习惯先把时钟树理清楚,再写设备树。

4.2.1 时钟提供者

时钟控制器节点示例:

clk_core: clock-controller@ff760000 {
    compatible = "rockchip,rk3399-cru";
    reg = <0x0 0xff760000 0x0 0x1000>;
    #clock-cells = <1>;
    #reset-cells = <1>;
};

#clock-cells = <1>表示每个时钟引用需要1个参数,通常是时钟ID。这个ID定义在驱动头文件里,比如clk-rk3399.h。

4.2.2 时钟消费者

设备怎么要时钟?看这个:

&uart0 {
    compatible = "rockchip,rk3399-uart";
    reg = <0x0 0xff180000 0x0 0x100>;
    clocks = <&clk_core SCLK_UART0>, <&clk_core PCLK_UART0>;
    clock-names = "baudclk", "apb_pclk";
};

clock-names很重要,驱动通过名字找时钟,而不是靠顺序。我建议你养成写名字的习惯,不然驱动升级时顺序一变,代码就崩了。

避坑指南:我曾经在项目里只配了一个时钟,结果UART波特率死活不对。后来发现UART需要两个时钟:一个给波特率发生器,一个给APB总线。少一个,数据就乱码。所以配时钟前,一定先看datasheet里设备需要几个时钟域。

4.3 pinctrl子系统绑定——引脚的“多面手”

pinctrl是设备树里最灵活也最容易出错的部分。它负责把引脚配置成GPIO、UART、I2C等功能。说白了,就是引脚的“角色切换器”。

4.3.1 控制器节点

&pinctrl {
    compatible = "rockchip,rk3399-pinctrl";
    rockchip,grf = <&grf>;
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    uart0 {
        uart0_xfer: uart0-xfer {
            rockchip,pins =
                <2 RK_PB4 RK_FUNC_1 &pcfg_pull_up>,
                <2 RK_PB5 RK_FUNC_1 &pcfg_pull_up>;
        };
    };
};

每个引脚配置包含4个要素:bank号、引脚号、功能号、上下拉配置。功能号RK_FUNC_1表示复用功能1,具体对应什么功能要看芯片手册。

4.3.2 设备引用pinctrl

&uart0 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&uart0_xfer>;
    pinctrl-1 = <&uart0_sleep>;
};

pinctrl-names定义状态名,pinctrl-0对应default状态。系统在设备初始化时自动应用default状态,休眠时切换到sleep状态。这个机制对低功耗设计特别有用。

低功耗技巧:在sleep状态里把不用的引脚配成模拟输入或下拉,能省不少电。我做过一个项目,光靠优化pinctrl的sleep配置,待机电流从5mA降到了0.8mA。

4.4 中断控制器绑定——事件的“快递员”

中断绑定是设备树里最讲究“层级关系”的部分。一个设备的中断可能经过多个控制器才到达CPU。

4.4.1 中断控制器节点

&gic {
    compatible = "arm,gic-v3";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x0 0xfe000000 0x0 0x10000>;
};

#interrupt-cells = <3>表示每个中断描述需要3个参数:中断类型、中断号、触发方式。

4.4.2 中断消费者

&i2c0 {
    interrupt-parent = <&gic>;
    interrupts = <GIC_SPI 57 IRQ_TYPE_LEVEL_HIGH>;

    touch@38 {
        interrupt-parent = <&gpio0>;
        interrupts = <RK_PA5 IRQ_TYPE_EDGE_FALLING>;
    };
};

这里有个关键点:中断可以级联。I2C控制器挂在GIC上,触摸屏挂在GPIO0上。GPIO0本身也是中断控制器,它收到触摸屏的中断后,再通过自己的中断线报告给GIC。

我曾经踩过的坑:写触摸屏设备树时,忘了指定interrupt-parent,结果中断一直往GIC送。GIC收到的是GPIO的bank中断号,不是触摸屏的引脚中断号。驱动根本不知道是谁触发了中断。所以,子设备的中断父节点一定要指向它的直接中断控制器,不能偷懒。

4.5 四个绑定的协同关系

这四个绑定不是孤立的。一个典型的设备节点可能同时用到它们:

&spi1 {
    compatible = "rockchip,rk3399-spi";
    reg = <0x0 0xff1c0000 0x0 0x1000>;
    interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_core SCLK_SPI1>, <&clk_core PCLK_SPI1>;
    clock-names = "spiclk", "apb_pclk";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_clk &spi1_cs0 &spi1_mosi &spi1_miso>;
    cs-gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;
};

这个节点同时绑定了中断、时钟、pinctrl和GPIO。内核初始化时会按顺序解析:先配时钟,再设pinctrl,然后注册中断,最后才调用驱动的probe函数。

设备树绑定协同工作流程 SPI设备节点 时钟绑定 pinctrl绑定 中断绑定 GPIO绑定 内核解析顺序:时钟 → pinctrl → 中断 → GPIO → probe 驱动probe函数 顺序不能乱:先有时钟才能配寄存器,配完寄存器才能设中断,最后才能用GPIO

4.6 调试技巧与常见错误

写设备树难免出错,我总结几个高频问题:

  1. GPIO号对不上:检查bank号和引脚号是否从0开始计数,不同厂商的编码方式可能不同。
  2. 时钟找不到:确认clock-names和驱动里devm_clk_get用的名字一致。
  3. pinctrl不生效:检查rockchip,pins里的bank号、引脚号、功能号是否和datasheet一致。
  4. 中断不触发:先确认interrupt-parent指向正确,再看触发类型是否匹配硬件。

我的调试习惯:在驱动probe函数里加dev_info打印,把获取到的GPIO号、时钟频率、中断号都打出来。对比设备树里的值,一眼就能看出哪里配错了。这招帮我省了无数查datasheet的时间。

嗯,四个绑定讲完了。记住一个原则:设备树是给内核看的“说明书”,不是给编译器看的“代码”。写的时候多想想“内核看到这个属性会怎么解析”,很多问题就能避免。