设备驱动休眠回调机制: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 vs Runtime PM 对比 System Sleep (suspend-to-RAM) 触发:echo mem > /sys/power/state 范围:所有设备,全局同步 回调:suspend → suspend_late → suspend_noirq Runtime PM (运行时电源管理) 触发:pm_runtime_put_sync() 范围:单个设备,异步/同步 回调:runtime_suspend → runtime_resume 核心区别 System Sleep:内核主导,所有设备按序休眠/唤醒 Runtime PM:驱动自主控制,设备空闲时自动降功耗

我个人的习惯是:system_sleep 的回调里做「全量保存和恢复」,比如把所有寄存器值存到内存里;runtime PM 的回调里做「增量操作」,比如只关掉时钟、保持寄存器状态不变。为什么?因为 runtime PM 切换频繁,全量保存太费时间。

小技巧: 如果你的设备在 runtime suspend 时把寄存器全保存了,那 system_sleep 的 suspend 回调里可以直接复用 runtime_suspend 的保存函数。别写两遍,容易出 bug。

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 阶段才解决。

避坑指南: 我曾经在 suspend_late 里调用了 msleep(),结果系统直接 panic。因为 suspend_late 阶段调度器已经部分冻结,不能睡眠等待。noirq 阶段更是连 spinlock 都要小心用。

如何注册 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 时就不能真的关掉它。我在项目中遇到过因为没检查这个返回值,导致按键无法唤醒系统的问题。

调试小贴士

调试休眠回调时,我常用的三板斧:

  1. 加 dev_dbg 打印:在每个回调入口和出口加打印,看执行顺序对不对。
  2. 检查返回值:suspend 回调返回非零值,内核会立即 abort 休眠。用 echo mem > /sys/power/state 触发,看 dmesg 里有没有报错。
  3. 看 /sys/kernel/debug/suspend_stats:这里记录了每次休眠的失败原因,非常有用。

嗯,关于 dev_pm_ops 的核心内容就这些。记住三组回调的区别,搞清 system_sleep 和 runtime PM 的适用场景,再掌握执行顺序,大部分休眠问题你都能定位了。