第3章 RK GPIO驱动实战:从框架到调试

GPIO,说白了就是芯片最基础的“手脚”。你让它高它就高,让它低它就低。但到了RK平台,事情就没那么简单了——背后牵扯着GPIO子系统、pinctrl、中断控制器、底半部处理一堆东西。这一章,我们就把它彻底捋清楚。

3.1 GPIO子系统框架:三层结构

RK平台的GPIO驱动,内核里分了三层。我刚开始接触时也觉得绕,后来画了张图才明白。

核心思路:应用层 → GPIO子系统核心层 → 硬件驱动层。每一层只干自己的事,互不干扰。
RK GPIO 子系统三层架构 应用层 / 用户空间 sysfs / libgpiod / 字符设备 GPIO 子系统核心层 (gpiolib) gpio_chip 注册 / 中断映射 / 控制接口 硬件驱动层 (pinctrl + GPIO 控制器) RK pinctrl 驱动 / GPIO 寄存器操作 RK 芯片 GPIO 硬件寄存器 用户通过 sysfs 或 libgpiod 操作 GPIO gpiolib 提供统一 API 屏蔽硬件差异 直接操作 寄存器

每一层都有明确的职责。应用层只管“我要这个脚输出高电平”,核心层负责把请求翻译成标准调用,硬件驱动层才真正去读写寄存器。我在项目中见过有人直接绕过核心层去操作寄存器,结果换内核版本就崩了——千万别这么干。

3.2 pinctrl驱动配置:管脚复用

RK芯片的GPIO,大部分都是多功能管脚。同一个物理脚,可能是GPIO,也可能是I2C的SCL,还可能是UART的TX。怎么切换?靠pinctrl。

我的习惯:拿到新板子,第一件事就是看 rkxx-pinctrl.dtsi 里的管脚配置。管脚复用配错了,后面所有驱动都跑不起来。

设备树里典型的pinctrl配置长这样:

// rk3588-pinctrl.dtsi 片段
pinctrl: pinctrl {
    compatible = "rockchip,rk3588-pinctrl";
    rockchip,grf = <&grf>;
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    // 定义一个i2c2的管脚组
    i2c2 {
        i2c2_xfer: i2c2-xfer {
            rockchip,pins =
                <3 RK_PB0 1 &pcfg_pull_up>,  // GPIO3_B0 → I2C2_SCL
                <3 RK_PB1 1 &pcfg_pull_up>;  // GPIO3_B1 → I2C2_SDA
        };
    };
};

这里 rockchip,pins 的四个字段分别是:bank号、pin号、function选择、上下拉配置。function为0时就是普通GPIO,非0值对应不同的复用功能。

我曾经踩过的坑:配完pinctrl后,发现I2C死活不通。量波形发现SCL一直拉低。查了半天,原来是pinctrl里忘了配pull-up,管脚内部浮空导致电平不确定。后来我养成了习惯——配完pinctrl先测静态电平。

3.3 GPIO中断注册与底半部处理

GPIO中断,嵌入式开发里天天用。按键、插拔检测、边沿触发……但很多人只会在设备树里配个 interrupts 属性,对内核里怎么注册、怎么处理底半部,一知半解。

RK平台GPIO中断的注册流程,我总结成三步:

  1. 设备树声明中断资源——指定GPIO和触发类型
  2. 驱动里调用 gpiod_to_irq()——拿到中断号
  3. request_threaded_irq()——注册中断处理函数,区分顶半部和底半部

看个实际例子:

// 设备树节点
gpio-key {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    pinctrl-0 = <&key_pin>;
    key-gpios = <&gpio1 RK_PA5 GPIO_ACTIVE_LOW>;
    interrupts-extended = <&gpio1 RK_PA5 IRQ_TYPE_EDGE_FALLING>;
};

// 驱动中注册中断
static int key_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct gpio_desc *desc;
    int irq, ret;

    desc = devm_gpiod_get(dev, "key", GPIOD_IN);
    if (IS_ERR(desc))
        return PTR_ERR(desc);

    irq = gpiod_to_irq(desc);
    if (irq < 0)
        return irq;

    // 注册线程化中断——底半部自动在进程上下文执行
    ret = devm_request_threaded_irq(dev, irq,
                    NULL,           // 顶半部:NULL表示全部交给底半部
                    key_isr_thread, // 底半部:可以睡眠,可以做耗时操作
                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                    "gpio-key", dev);
    if (ret)
        dev_err(dev, "request irq failed: %d\n", ret);

    return ret;
}

static irqreturn_t key_isr_thread(int irq, void *dev_id)
{
    struct device *dev = dev_id;
    int val;

    // 底半部里可以调用msleep、i2c读写等阻塞操作
    msleep(20);  // 简单消抖
    val = gpiod_get_value(desc);
    if (val == 0) {
        dev_info(dev, "key pressed!\n");
        // 上报输入事件或唤醒用户态
    }

    return IRQ_HANDLED;
}
关键点:顶半部只做最少的硬件操作(比如清中断标志),底半部做实际业务。RK平台推荐用线程化中断(threaded IRQ),省去自己写workqueue的麻烦。

为什么推荐线程化中断?我举个例子。有一次客户反馈按键偶尔丢失,查了三天发现是顶半部里调了 i2c_master_send()——顶半部在中断上下文,不能睡眠,I2C传输需要等待应答,直接导致内核报错。改成线程化中断后,问题消失。

3.4 GPIO电平测量与调试

驱动写完了,怎么确认GPIO状态对不对?我见过有人直接拿万用表去量,但有些管脚是1.8V电平,万用表反应慢,瞬间脉冲根本抓不到。这时候得用示波器,或者——先在内核里确认。

内核提供了几个调试手段:

调试方法 命令/接口 适用场景
sysfs 导出 echo 120 > /sys/class/gpio/export 快速验证GPIO方向、电平
debugfs 查看 cat /sys/kernel/debug/gpio 查看所有GPIO的当前状态、使用者
pinctrl debug cat /sys/kernel/debug/pinctrl/pinctrl-handles 确认管脚复用是否正确
devmem 读寄存器 devmem 0xfe2b0000 32 直接读GPIO数据寄存器,绕过驱动层

我个人最常用的是 debugfs/gpio。它会把每个GPIO的bank、label、方向、当前电平、使用者都列出来。有一次排查HDMI热插拔检测,发现GPIO方向被某个驱动改成了输出,就是靠这个抓到的。

调试小技巧:在驱动里加 gpiod_get_value() 打印电平变化,配合 printk 的时间戳,可以精确到微秒级。比拿示波器量快多了。

再说一个实际案例。某项目用RK3588做边缘计算盒子,GPIO4_C6控制一个外设的复位脚。设备树配的是高电平复位,但外设死活不工作。我量了管脚电压——1.8V,没错啊。后来用 devmem 读GPIO数据寄存器,发现寄存器里确实是高,但外设那边就是没反应。最后查原理图,发现这个管脚经过了一个电平转换芯片,转换芯片的使能脚没拉高。嗯,硬件问题,驱动背了锅。

避坑指南:GPIO电平测量,不要只看驱动层。量一下管脚物理电平,再量一下外设端的电平,中间有没有电平转换、有没有上拉电阻、有没有串联电阻,都要确认。我曾经被一个100欧姆的串联电阻坑了整整两天。

总结一下这一章的核心:GPIO驱动不是简单的“拉高拉低”,它背后是pinctrl的管脚复用、gpiolib的框架抽象、中断的顶底半部处理。调试时,先用内核接口确认软件状态,再用万用表/示波器确认硬件状态,两边都对得上,问题才能定位。