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 唤醒,通常需要做三件事:
- 将 GPIO 配置为中断模式:设置触发边沿(上升沿、下降沿或双边沿)
- 注册中断处理函数:调用
request_irq()或devm_request_irq() - 使能唤醒能力:调用
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 中断唤醒配置的完整流程
下面这张图展示了从硬件中断到系统唤醒的完整链路:
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 的回调函数里打印关键寄存器的值。这样能快速定位是哪个环节出了问题。