设备驱动休眠回调机制:dev_pm_ops 结构体详解
驱动休眠回调,说白了就是内核告诉驱动:「系统要睡了,你把手头的活儿停一下,保存现场;醒来时再把摊子支起来。」这个机制的核心,就是 dev_pm_ops 这个结构体。
dev_pm_ops 结构体长什么样?
先看定义,在 include/linux/pm.h 里:
struct dev_pm_ops {
int (*prepare)(struct device *dev);
void (*complete)(struct device *dev);
int (*suspend)(struct device *dev);
int (*resume)(struct device *dev);
int (*freeze)(struct device *dev);
int (*thaw)(struct device *dev);
int (*poweroff)(struct device *dev);
int (*restore)(struct device *dev);
int (*suspend_late)(struct device *dev);
int (*resume_early)(struct device *dev);
int (*freeze_late)(struct device *dev);
int (*thaw_early)(struct device *dev);
int (*poweroff_late)(struct device *dev);
int (*restore_early)(struct device *dev);
int (*suspend_noirq)(struct device *dev);
int (*resume_noirq)(struct device *dev);
int (*freeze_noirq)(struct device *dev);
int (*thaw_noirq)(struct device *dev);
int (*poweroff_noirq)(struct device *dev);
int (*restore_noirq)(struct device *dev);
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
嗯,看着挺长。但别慌,实际驱动里你只需要实现其中几个。我刚开始接触时也被这阵势吓到,后来发现真正干活的就是那三组回调。
三组回调,各司其职
| 阶段 | 回调函数 | 典型操作 |
|---|---|---|
| suspend/resume | suspend, resume | 保存寄存器、关闭时钟、停止 DMA |
| freeze/thaw | freeze, thaw | hibernate 快照时的冻结操作 |
| poweroff/restore | poweroff, restore | hibernate 恢复后的硬件重初始化 |
你想想看,为什么要有三套?因为系统休眠不止一种方式。suspend/resume 对应 suspend-to-RAM,freeze/thaw 对应 hibernate 的镜像创建阶段,poweroff/restore 对应 hibernate 的恢复阶段。我在项目中遇到过有人把 resume 和 restore 混着用,结果 hibernate 恢复时设备直接罢工。
system_sleep 与 runtime PM 的区别
这是新手最容易搞混的地方。我画个图帮你理清思路:
我个人的习惯是:system_sleep 的回调里做「全量保存和恢复」,比如把所有寄存器值存到内存里;runtime PM 的回调里做「增量操作」,比如只关掉时钟、保持寄存器状态不变。为什么?因为 runtime PM 切换频繁,全量保存太费时间。
suspend/resume 回调执行顺序
这个顺序我当年背了好久,后来发现其实就一句话:设备树里从根到叶 suspend,从叶到根 resume。但实际执行时还有 late 和 noirq 阶段,顺序更细。
看一个典型的执行流程:
// suspend 阶段(从上到下)
1. prepare() // 所有设备,按依赖顺序
2. suspend() // 设备 suspend 回调
3. suspend_late() // 中断仍使能,但部分子系统已暂停
4. suspend_noirq() // 中断已关闭,最后关头
// resume 阶段(从下到上)
5. resume_noirq() // 中断刚恢复,最早执行
6. resume_early() // 中断使能,设备逐步恢复
7. resume() // 设备 resume 回调
8. complete() // 所有设备恢复完成
为什么要有 noirq 阶段?因为有些设备在中断关闭后才能安全操作。我记得有一次调试一个 SPI 触摸屏,它在 resume_early 里就尝试发 SPI 消息,结果 SPI 控制器还没恢复,直接死锁。后来把 SPI 通信挪到 resume 阶段才解决。
如何注册 dev_pm_ops?
有两种方式,看你的驱动类型:
// 方式一:传统 platform 驱动
static struct platform_driver my_driver = {
.driver = {
.name = "my_device",
.pm = &my_dev_pm_ops, // 指向 dev_pm_ops 结构体
},
.probe = my_probe,
.remove = my_remove,
};
// 方式二:使用 SET_SYSTEM_SLEEP_PM_OPS 宏
static struct dev_pm_ops my_dev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(my_suspend, my_resume)
SET_RUNTIME_PM_OPS(my_runtime_suspend, my_runtime_resume, NULL)
};
我个人推荐用宏的方式,代码更简洁。而且 SET_SYSTEM_SLEEP_PM_OPS 在 CONFIG_PM_SLEEP 未使能时会自动把回调置空,省得你写一堆 #ifdef。
一个完整的例子
结合前面讲的,写一个 GPIO 按键驱动的休眠回调:
static int gpio_key_suspend(struct device *dev)
{
struct gpio_key_data *data = dev_get_drvdata(dev);
// 保存中断状态
data->irq_enabled = !disable_irq_wake(data->irq);
// 关闭按键扫描定时器
del_timer_sync(&data->debounce_timer);
dev_dbg(dev, "suspend done, irq_wake=%d\n", data->irq_enabled);
return 0;
}
static int gpio_key_resume(struct device *dev)
{
struct gpio_key_data *data = dev_get_drvdata(dev);
// 重新使能中断
enable_irq(data->irq);
// 重新初始化硬件状态
gpio_key_hw_init(data);
dev_dbg(dev, "resume done\n");
return 0;
}
static struct dev_pm_ops gpio_key_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(gpio_key_suspend, gpio_key_resume)
};
disable_irq_wake 的返回值。如果返回 0,说明这个中断可以唤醒系统,那 suspend 时就不能真的关掉它。我在项目中遇到过因为没检查这个返回值,导致按键无法唤醒系统的问题。
调试小贴士
调试休眠回调时,我常用的三板斧:
- 加 dev_dbg 打印:在每个回调入口和出口加打印,看执行顺序对不对。
- 检查返回值:suspend 回调返回非零值,内核会立即 abort 休眠。用
echo mem > /sys/power/state触发,看 dmesg 里有没有报错。 - 看 /sys/kernel/debug/suspend_stats:这里记录了每次休眠的失败原因,非常有用。
嗯,关于 dev_pm_ops 的核心内容就这些。记住三组回调的区别,搞清 system_sleep 和 runtime PM 的适用场景,再掌握执行顺序,大部分休眠问题你都能定位了。