第3章:GPIO驱动与中断问题定位

GPIO和中断,是嵌入式驱动开发里最基础也最容易出问题的两个东西。我见过不少工程师,内核配置配得飞起,设备树写得眼花缭乱,结果一个GPIO中断没配好,系统直接卡死。说白了,中断风暴一上来,CPU啥也干不了。

这一章,我们就拿RK3568平台来拆解GPIO子系统的框架、中断注册流程,以及怎么用devmem2这种土办法去定位硬件问题。嗯,都是实战里摸爬滚打出来的经验。

3.1 GPIO子系统框架

RK3568的GPIO控制器挂在APB总线上,一共有5组GPIO:GPIO0到GPIO4。每组32个引脚,总共160个。每组内部又分成4个bank,每个bank 8个引脚。

内核里的GPIO子系统分三层:

  • GPIO硬件层:直接操作寄存器,比如方向寄存器、数据寄存器。
  • GPIO核心层:提供统一的gpio_request()、gpio_direction_output()等API。
  • GPIO消费层:你的驱动通过gpiod_get()拿描述符,然后操作。

我个人习惯,在设备树里把GPIO的用途写清楚,比如:

my_device {
    compatible = "my,driver";
    reset-gpios = &gpio3 RK_PB5 GPIO_ACTIVE_LOW;
    irq-gpios = &gpio1 RK_PA2 GPIO_ACTIVE_HIGH;
};

这样驱动里直接用devm_gpiod_get()拿,省心。

关键点:RK3568的GPIO复用功能在pinctrl里配置,别直接在驱动里写死寄存器值,否则换板子就废了。

3.2 中断注册与处理

GPIO中断的注册流程,我总结成四步:

  1. 申请GPIO:devm_gpiod_get()或gpio_request()。
  2. 设置中断触发类型:gpiod_to_irq()拿到中断号,然后request_irq()。
  3. 实现中断处理函数:注意区分顶半部和底半部。
  4. 使能中断:enable_irq()。

代码示例:

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    // 顶半部:快速处理,只做标记
    schedule_work(&dev->work);
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int irq;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
    irq = gpiod_to_irq(dev->gpio);

    devm_request_irq(&pdev->dev, irq, my_irq_handler,
                     IRQF_TRIGGER_FALLING, "my_device", dev);
    return 0;
}

这里有个坑:devm_request_irq()虽然自动释放,但如果你在中断处理函数里访问了已经释放的内存,系统直接panic。我曾经因为这个查了两天,最后发现是probe失败时资源释放顺序不对。

我的建议:中断处理函数里尽量别做耗时操作。实在要做,用tasklet或workqueue推到底半部。

3.3 使用devmem2操作寄存器

有时候内核还没起来,或者驱动加载失败,你想确认硬件电平对不对。这时候devmem2就是神器。

RK3568的GPIO寄存器基地址:

GPIO组 基地址 范围
GPIO0 0xFE740000 0x1000
GPIO1 0xFE750000 0x1000
GPIO2 0xFE760000 0x1000
GPIO3 0xFE770000 0x1000
GPIO4 0xFE780000 0x1000

常用寄存器偏移:

  • SWPORTA_DR (0x0000):数据寄存器,读写引脚电平。
  • SWPORTA_DDR (0x0004):方向寄存器,0=输入,1=输出。
  • INTEN (0x0030):中断使能寄存器。
  • INTSTATUS (0x0040):中断状态寄存器,读清除。

举个例子,我想查GPIO1_PA2的电平:

# 先确认方向
devmem2 0xFE750004
# 再读数据
devmem2 0xFE750000

如果读出来是0x04,说明PA2是高电平。嗯,就这么简单。

注意:devmem2直接操作物理地址,没有内核保护。写错了寄存器,系统可能死机。我建议你先读,确认了再写。

3.4 中断风暴排查

中断风暴,说白了就是中断频率太高,CPU一直在响应中断,没时间干正事。现象就是系统响应慢,top看到irq占用100%。

排查步骤:

  1. 看/proc/interrupts:哪个中断号计数暴涨,一目了然。
  2. 确认触发类型:是不是设成了IRQF_TRIGGER_RISING但信号有毛刺?
  3. 用示波器抓波形:硬件上是不是有抖动?
  4. 软件去抖:在中断处理函数里加延时或状态机。

我曾经遇到一个案子,GPIO接了按键,每次按下触发几十次中断。查了半天,发现是按键硬件没做RC滤波,软件也没去抖。解决方案:

static irqreturn_t btn_irq_handler(int irq, void *dev_id)
{
    static unsigned long last_jiffies = 0;
    if (time_before(jiffies, last_jiffies + msecs_to_jiffies(50)))
        return IRQ_HANDLED;  // 50ms内重复触发,忽略
    last_jiffies = jiffies;
    // 真正的处理
    return IRQ_HANDLED;
}

这个办法虽然土,但有效。当然,更好的做法是在设备树里配debounce-interval,让硬件帮你过滤。

核心思路:中断风暴的根因要么是硬件信号质量问题,要么是软件触发类型配置错误。先看/proc/interrupts定位,再用devmem2确认寄存器状态,最后对症下药。

3.5 知识结构图

下面这张图,把GPIO中断的整个链路画清楚了:

GPIO中断处理流程 GPIO中断处理流程 硬件GPIO电平变化 GIC中断控制器 内核中断子系统 (request_irq) 顶半部 (快速处理) 底半部 (workqueue) 驱动业务逻辑 排查中断风暴时,重点关注顶半部执行时间和触发频率

3.6 避坑指南

做GPIO驱动,有几个坑我踩过,写出来大家别重蹈覆辙:

  • 中断号重复:多个设备共享一个中断号时,request_irq的dev_id参数必须唯一,否则free_irq会误删别人的处理函数。
  • 中断上下文睡眠:顶半部里不能调用msleep()、mutex_lock()等可能睡眠的函数。我曾经用printk太多导致系统卡死,因为printk在中断上下文里也可能阻塞。
  • GPIO复用冲突:RK3568的引脚功能由pinctrl管理,如果你在驱动里直接写GRF寄存器改复用,而设备树里又配了别的功能,两个打架,结果就是谁都不干活。

我的习惯:每次加新中断,先在/proc/interrupts确认中断号,再用devmem2读INTSTATUS寄存器,确保硬件确实产生了中断。这一步能省掉80%的排查时间。

好了,这一章的内容就这些。GPIO和中断看似简单,但真出了问题,往往牵一发而动全身。记住:先看硬件,再查软件,最后才是代码逻辑。顺序别搞反了。