4. 中断唤醒源配置:enable_irq_wake() 与 disable_irq_wake()

中断唤醒,说白了就是让系统在睡死过去之后,还能被一个外部信号一脚踹醒。这个信号通常来自硬件引脚——比如按键、传感器中断、网络唤醒信号。

我刚开始做休眠调试时,最头疼的就是系统能睡下去,但怎么都醒不过来。后来发现,八成是中断唤醒源没配好。

4.1 核心API:enable_irq_wake() 与 disable_irq_wake()

这两个函数是中断唤醒配置的入口。它们的作用不是开关中断,而是告诉电源管理子系统:这个中断可以用来唤醒系统。

int enable_irq_wake(unsigned int irq);
int disable_irq_wake(unsigned int irq);

调用 enable_irq_wake() 后,内核会在系统进入 suspend 时,把这个中断设置为唤醒源。对应的硬件行为是:在中断控制器里设置对应的唤醒使能位,同时增加一个 wakeup 引用计数。

关键点:enable_irq_wake() 和 disable_irq_wake() 必须成对调用。每调用一次 enable,引用计数 +1;每调用一次 disable,引用计数 -1。只有计数归零时,中断的唤醒能力才会被真正撤销。

我在项目中遇到过一个问题:驱动在 probe 里调了 enable_irq_wake(),但在 remove 时忘了调 disable_irq_wake()。结果模块卸载后,中断仍然被标记为唤醒源,系统一休眠就被这个没人处理的中断唤醒。排查了整整两天才找到原因。

4.2 中断唤醒标志:IRQF_NO_SUSPEND

这个标志和 enable_irq_wake() 容易搞混。我简单说一下区别:

配置方式 作用 典型场景
enable_irq_wake() 允许中断在系统休眠时唤醒系统 按键、RTC、网络唤醒
IRQF_NO_SUSPEND 禁止中断在系统休眠时被关闭 看门狗、IPI、时钟事件

IRQF_NO_SUSPEND 是在 request_irq() 时传入的标志。它告诉内核:这个中断在系统 suspend 时不要被禁用。但注意,它并不保证这个中断能唤醒系统。

举个例子:系统里的 tick 时钟中断,它需要在休眠时继续工作(比如维护时间),但它不需要唤醒系统。这时候就用 IRQF_NO_SUSPEND,而不是 enable_irq_wake()。

避坑指南:我曾经在一个项目中,把 GPIO 按键中断同时设置了 IRQF_NO_SUSPEND 和 enable_irq_wake()。结果系统休眠后,按键中断确实能触发,但中断处理函数在 suspend 状态下执行,访问了已经关闭时钟的外设寄存器,直接导致系统崩溃。正确的做法是:唤醒中断不要加 IRQF_NO_SUSPEND,让内核在唤醒流程中重新使能中断。

4.3 GPIO 唤醒配置

GPIO 是最常见的唤醒源。配置 GPIO 唤醒,通常需要做三件事:

  1. 将 GPIO 配置为中断模式:设置触发边沿(上升沿、下降沿或双边沿)
  2. 注册中断处理函数:调用 request_irq() 或 devm_request_irq()
  3. 使能唤醒能力:调用 enable_irq_wake()

代码示例:

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

static irqreturn_t gpio_wakeup_handler(int irq, void *dev_id)
{
    /* 这里只做唤醒标记,不要做耗时操作 */
    pr_info("GPIO wakeup triggered\n");
    return IRQ_HANDLED;
}

static int __init gpio_wakeup_init(void)
{
    int irq, ret;

    /* 1. 申请 GPIO */
    ret = gpio_request_one(WAKEUP_GPIO, GPIOF_IN, "wakeup-gpio");
    if (ret) return ret;

    /* 2. 获取中断号 */
    irq = gpio_to_irq(WAKEUP_GPIO);

    /* 3. 注册中断 */
    ret = request_irq(irq, gpio_wakeup_handler,
                      IRQF_TRIGGER_FALLING,
                      "wakeup-gpio", NULL);
    if (ret) goto err_free_gpio;

    /* 4. 使能唤醒 */
    ret = enable_irq_wake(irq);
    if (ret) goto err_free_irq;

    return 0;

err_free_irq:
    free_irq(irq, NULL);
err_free_gpio:
    gpio_free(WAKEUP_GPIO);
    return ret;
}

个人经验:GPIO 唤醒中断的处理函数里,尽量不要做复杂操作。我习惯只设置一个标志位,然后返回 IRQ_HANDLED。真正的业务逻辑放到唤醒后的工作队列或线程里处理。因为系统刚唤醒时,很多外设时钟还没恢复,贸然访问寄存器容易出问题。

4.4 中断唤醒配置的完整流程

下面这张图展示了从硬件中断到系统唤醒的完整链路:

中断唤醒配置与执行流程 硬件中断信号 中断控制器 (GIC/INTC) 内核中断子系统 检查 wakeup 计数 计数 > 0? 唤醒系统 (resume) 忽略中断 执行中断处理函数 恢复系统执行 关键:enable_irq_wake() 设置 wakeup 计数,系统 suspend 时根据计数决定是否将中断设为唤醒源

4.5 调试技巧:如何确认中断唤醒配置正确

系统休眠唤不醒,怎么查?我一般按这个顺序排查:

  • 检查 /proc/interrupts:确认中断号存在,触发次数在增加
  • 检查 /proc/irq/<irq_num>/wakeup:这个文件显示 wakeup 计数,非 0 表示已使能唤醒
  • 检查 dmesg:系统 suspend/resume 过程中,内核会打印中断相关的调试信息
  • 检查 GPIO 状态:用 cat /sys/kernel/debug/gpio 确认 GPIO 方向、电平是否正确

快速验证命令:

# 查看中断唤醒计数
cat /proc/irq/<irq_num>/wakeup

# 查看所有中断统计
cat /proc/interrupts | grep wakeup

# 查看 GPIO 调试信息
cat /sys/kernel/debug/gpio

嗯,这里要注意一点:有些 SoC 的中断控制器在 suspend 时会断电,导致唤醒中断无法传递。这时候需要在设备树里配置中断控制器的 wakeup-source 属性,或者用 irq_set_irq_wake() 在父中断控制器上也使能唤醒。

我遇到过最坑的一次:GPIO 中断配置完全正确,wakeup 计数也是 1,但系统就是醒不过来。最后发现是 GPIO 模块的时钟在 suspend 时被关了,中断信号根本传不到中断控制器。解决办法是在设备树里给 GPIO 模块加上 clock-latency 或 wakeup-source 属性。

我的习惯:调试唤醒问题时,我会先在驱动里加一个 #define DEBUG,打开 dev_dbg() 打印。然后在 suspend/resume 的回调函数里打印关键寄存器的值。这样能快速定位是哪个环节出了问题。