2、GPIO驱动开发:从寄存器到中断,一次讲透

GPIO,说白了就是芯片的“手脚”。你要让灯亮、让电机转、检测按键按下,都离不开它。瑞芯微RK系列平台的GPIO模块,设计得挺巧妙——它把GPIO和中断控制器揉在一起,用起来方便,但坑也不少。

我个人习惯,拿到一个新平台,第一件事就是翻GPIO控制器的寄存器手册。为什么?因为后面所有操作,本质上都是在摆弄这些寄存器。你想想看,gpiolib封装得再好,底层还是得读写那几个地址。

2.1 GPIO控制器寄存器详解

RK平台的GPIO控制器,每个Bank管理32个引脚。以RK3588为例,它有5个GPIO Bank(GPIO0~GPIO4)。每个Bank的寄存器基地址,在芯片手册里都能查到。

核心寄存器就那么几个,我列个表,你一看就明白:

寄存器名称 偏移地址 位宽 功能描述
SWPORTA_DR 0x0000 32位 数据输出寄存器。写1拉高,写0拉低。
SWPORTA_DDR 0x0004 32位 方向寄存器。1=输出,0=输入。
SWPORA_PSR 0x0008 32位 引脚状态寄存器。读这个得到引脚当前电平。
INTEN 0x0030 32位 中断使能寄存器。1=使能该引脚中断。
INTTYPE 0x0038 32位 中断类型。1=电平触发,0=边沿触发。
INT_POLARITY 0x003C 32位 中断极性。电平触发时:1=高电平,0=低电平;边沿触发时:1=下降沿,0=上升沿。
INTSTS 0x0040 32位 中断状态寄存器。读得知哪个引脚触发了中断,写1清除中断标志。
我的小技巧:调试时,我习惯先读SWPORA_PSR确认引脚电平,再配置DDR。这样可以快速排除硬件连接问题。曾经有个项目,死活读不到按键电平,折腾半天发现是DDR没配成输入——低级错误,但很容易犯。

2.2 GPIO操作接口:gpiolib的使用

直接操作寄存器当然可以,但内核提供了gpiolib,把底层封装好了。我建议你尽量用这套接口,代码可移植性好,也省得自己处理并发问题。

gpiolib的核心流程,我画了张图:

gpiolib 核心操作流程 1. 申请GPIO gpio_request() 2. 设置方向 gpio_direction_input/output() 3. 读写操作 gpio_get_value() / gpio_set_value() 4. 中断配置(可选) gpio_to_irq() + request_irq() 5. 释放GPIO gpio_free()

看明白了吗?流程很清晰。我重点说说几个容易踩坑的地方。

2.2.1 申请GPIO

用 gpio_request() 之前,你得先知道GPIO编号。RK平台里,GPIO编号的计算公式是:

gpio_number = bank * 32 + pin_offset

比如GPIO1_B5,bank=1,pin_offset=13(B组偏移8,再加5),所以编号是1*32+13=45。

注意:申请GPIO时,一定要检查返回值。我见过太多人直接忽略错误码,结果后面操作全失败,还找不到原因。曾经有个同事,gpio_request返回-EBUSY,他没管,结果驱动加载后GPIO完全不受控——查了两天才发现是pinctrl子系统已经把引脚占用了。

2.2.2 设置方向

设置方向有两个函数:

  • gpio_direction_input() —— 设为输入
  • gpio_direction_output() —— 设为输出,同时可以设置初始电平

这里有个细节:gpio_direction_output() 的第二个参数是初始值。我建议你显式传参,别依赖默认值。因为不同内核版本,默认行为可能不一样。

2.2.3 读写操作

读写就简单了:

// 读引脚
int val = gpio_get_value(gpio_num);

// 写引脚
gpio_set_value(gpio_num, 1);  // 拉高
gpio_set_value(gpio_num, 0);  // 拉低

嗯,这里要注意:gpio_set_value() 在原子上下文也能用,但如果你在进程上下文,建议用 gpio_set_value_cansleep(),它内部会处理可能导致的睡眠问题。

2.3 GPIO中断配置与调试

GPIO中断,是驱动开发里最常用的功能之一。按键、传感器、外设状态变化,都靠它来通知CPU。

配置中断的步骤,我总结为四步走:

  1. 申请GPIO并设为输入 —— 中断引脚必须是输入模式
  2. 获取中断号 —— 用 gpio_to_irq() 把GPIO编号转成中断号
  3. 注册中断处理函数 —— 用 request_irq() 或 devm_request_irq()
  4. 实现中断处理 —— 在顶半部做紧急处理,底半部用tasklet或workqueue

代码示例,我写个按键中断的典型实现:

#include <linux/interrupt.h>
#include <linux/gpio.h>

#define KEY_GPIO 45  // GPIO1_B5

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    // 顶半部:只做最紧急的事
    // 比如设置标志位,唤醒等待队列
    // 不要在这里做耗时操作!
    
    return IRQ_WAKE_THREAD;  // 唤醒线程化中断处理
}

static irqreturn_t key_irq_thread(int irq, void *dev_id)
{
    // 底半部:在这里做实际处理
    // 比如上报输入事件、打印日志
    int val = gpio_get_value(KEY_GPIO);
    pr_info("Key pressed, value=%d\n", val);
    
    return IRQ_HANDLED;
}

static int __init key_drv_init(void)
{
    int ret, irq;
    
    // 1. 申请GPIO
    ret = gpio_request(KEY_GPIO, "key-gpio");
    if (ret) {
        pr_err("Failed to request GPIO %d\n", KEY_GPIO);
        return ret;
    }
    
    // 2. 设为输入
    gpio_direction_input(KEY_GPIO);
    
    // 3. 获取中断号
    irq = gpio_to_irq(KEY_GPIO);
    if (irq < 0) {
        pr_err("Failed to get IRQ for GPIO %d\n", KEY_GPIO);
        gpio_free(KEY_GPIO);
        return irq;
    }
    
    // 4. 注册中断(线程化,下降沿触发)
    ret = request_threaded_irq(irq, key_irq_handler, key_irq_thread,
                               IRQF_TRIGGER_FALLING, "key-irq", NULL);
    if (ret) {
        pr_err("Failed to request IRQ %d\n", irq);
        gpio_free(KEY_GPIO);
        return ret;
    }
    
    pr_info("Key driver initialized, IRQ=%d\n", irq);
    return 0;
}
关键点:我为什么用 request_threaded_irq() 而不是 request_irq()?因为线程化中断可以把大部分工作放到内核线程里做,不会阻塞其他中断。这在RK平台上特别有用——它的中断控制器支持嵌套中断,但处理不好容易导致中断丢失。

2.3.1 中断触发类型

RK平台的GPIO中断支持四种触发方式:

触发标志 含义 适用场景
IRQF_TRIGGER_RISING 上升沿触发 按键按下(从低到高)
IRQF_TRIGGER_FALLING 下降沿触发 按键按下(从高到低)
IRQF_TRIGGER_HIGH 高电平触发 电平敏感型外设
IRQF_TRIGGER_LOW 低电平触发 电平敏感型外设
避坑指南:我曾经在一个项目里,用上升沿触发检测按键,结果按键按下一次,中断触发了七八次。为什么?因为按键有机械抖动!解决办法:要么硬件加RC滤波,要么软件做去抖——在中断处理里加一个10~20ms的延时再读电平。我建议两种都做,硬件滤波+软件去抖,双保险。

2.3.2 中断调试技巧

调试GPIO中断,我常用的三板斧:

  • 看中断计数: cat /proc/interrupts | grep gpio,看中断有没有触发,触发了多少次
  • 看GPIO状态: cat /sys/kernel/debug/gpio,确认引脚方向、电平、使用情况
  • 加调试打印: 在中断处理函数里加 pr_info,但注意别打太多,否则影响性能

嗯,这里有个小技巧:如果中断没触发,先别急着查代码。拿万用表量一下引脚电平,确认硬件连接没问题。我遇到过好几次,都是飞线虚焊导致的问题。

2.4 本章小结

GPIO驱动开发,说难不难,说简单也不简单。核心就三件事:

  • 寄存器层面: 搞清楚SWPORTA_DR、DDR、PSR、INTEN、INTSTS这几个寄存器怎么用
  • 接口层面: 熟练使用gpiolib的request、direction、get/set、to_irq、free这套流程
  • 中断层面: 掌握中断配置四步法,注意去抖和线程化处理

把这些搞透了,RK平台上的GPIO驱动,你基本就拿下了一半。剩下的,就是多写多练,踩坑多了自然就熟了。