4. Runtime PM(运行时电源管理)核心机制
各位同学,今天我们来聊聊 Runtime PM。说实话,这个机制是我在嵌入式低功耗设计中打交道最多的一个。你想想看,系统级休眠固然重要,但很多时候我们只需要让某个外设在不工作时偷偷睡个觉——这就是 Runtime PM 的用武之地。
4.1 Runtime PM 的框架结构
Runtime PM 的框架,说白了就是一套「设备级电源管理」的调度系统。它不像系统休眠那样一刀切,而是精细到每个设备。
我个人习惯把它的框架想象成三层结构:
- 底层:硬件操作层。负责实际控制设备的电源门控、时钟门控。
- 中间层:核心调度层。管理引用计数、状态机转换、自动挂起定时器。
- 上层:驱动接口层。提供
pm_runtime_get/put等 API 给驱动开发者调用。
嗯,这里要注意:中间层是 Linux 内核帮你实现的,你只需要在驱动里注册好回调函数就行。
核心数据结构:每个设备都有一个 struct dev_pm_info,里面记录了引用计数、挂起状态、自动挂起延迟等关键信息。
4.2 核心 API:pm_runtime_get / put
这两个 API 是 Runtime PM 的灵魂。我刚开始用的时候也犯过糊涂,后来总结了一句话:get 就是「我要用设备了,别睡」;put 就是「我用完了,可以睡了」。
4.2.1 pm_runtime_get_sync()
这个函数会做两件事:
- 增加设备的引用计数
- 如果设备当前处于挂起状态,立即唤醒它
int ret;
ret = pm_runtime_get_sync(&spi_dev->dev);
if (ret < 0) {
dev_err(&spi_dev->dev, "唤醒失败,ret=%d\n", ret);
pm_runtime_put_noidle(&spi_dev->dev);
return ret;
}
/* 现在可以安全操作设备了 */
spi_write(...);
避坑指南:我曾经在中断上下文里直接调用 pm_runtime_get_sync(),结果系统直接崩了。记住,这个函数可能会睡眠,不能在原子上下文调用。如果必须在中断里用,请用 pm_runtime_get_noresume() + pm_request_resume() 的组合。
4.2.2 pm_runtime_put()
对应的,put 函数会减少引用计数。当计数降到 0 时,内核会触发挂起流程。
/* 操作完成,释放设备 */
pm_runtime_mark_last_busy(&spi_dev->dev);
pm_runtime_put_autosuspend(&spi_dev->dev);
为什么这里要用 put_autosuspend 而不是普通的 put?你想想看,如果每次操作完立刻挂起,频繁开关电源反而更耗电。自动挂起机制就是为了解决这个问题——延迟一段时间再挂起,避免频繁开关。
4.3 自动挂起与唤醒
自动挂起是 Runtime PM 的精髓。说白了就是:设备空闲一段时间后,内核自动帮它挂起。
4.3.1 自动挂起配置
在驱动初始化时,你需要设置自动挂起的延迟时间:
/* 设置自动挂起延迟为 100ms */
pm_runtime_set_autosuspend_delay(&dev->dev, 100);
pm_runtime_use_autosuspend(&dev->dev);
pm_runtime_enable(&dev->dev);
这个 100ms 怎么来的?我在项目中踩过坑。一开始设了 10ms,结果设备频繁挂起唤醒,功耗反而高了。后来改成 100ms,效果好了很多。具体数值要根据你的业务场景来调。
4.3.2 唤醒机制
唤醒有两种方式:
- 主动唤醒:调用
pm_runtime_get_sync()或pm_request_resume() - 被动唤醒:硬件中断触发,在中断处理中调用
pm_runtime_resume()
注意:被动唤醒时,中断处理函数里不能直接调用 pm_runtime_get_sync(),因为此时设备可能还没完全恢复。正确的做法是:在中断中标记一个标志位,然后调用 pm_request_resume(),在 resume 回调中处理数据。
4.4 状态机与生命周期
Runtime PM 的状态机其实很简单,就三个状态:
| 状态 | 含义 | 引用计数 |
|---|---|---|
| RPM_ACTIVE | 设备正常工作 | > 0 |
| RPM_SUSPENDED | 设备已挂起 | 0 |
| RPM_SUSPENDING | 正在挂起中(过渡态) | 0 |
嗯,这里要记住:引用计数为 0 不代表设备一定挂起了,只是代表「可以挂起」。实际是否挂起,还要看自动挂起定时器是否到期。
4.5 驱动中的回调函数
你需要实现三个回调:
static int mydev_runtime_suspend(struct device *dev)
{
/* 保存寄存器状态,关闭时钟 */
mydev_save_context(dev);
clk_disable(priv->clk);
return 0;
}
static int mydev_runtime_resume(struct device *dev)
{
/* 开启时钟,恢复寄存器状态 */
clk_enable(priv->clk);
mydev_restore_context(dev);
return 0;
}
static int mydev_runtime_idle(struct device *dev)
{
/* 检查是否可以挂起,返回 -EBUSY 阻止挂起 */
return 0;
}
关键点:runtime_idle 回调可以返回 -EBUSY 来阻止自动挂起。我在一个音频驱动中就用过这个技巧——当音频缓冲区还有数据时,不让设备挂起。
4.6 实战经验总结
最后,分享几个我在项目中积累的经验:
- 引用计数要配对:每个 get 必须有对应的 put,否则设备永远无法挂起。我建议在驱动入口和出口处打印引用计数,方便调试。
- 自动挂起延迟别设太小:低于 50ms 的延迟往往弊大于利。
- 注意竞态条件:get 和 put 可能在多个线程中并发调用,内核已经用 spinlock 保护了,但你的回调函数里要注意。
- 调试手段:打开
/sys/devices/.../power/runtime_status可以查看设备当前状态,非常实用。
Runtime PM 用好了,系统功耗能降 30% 以上。但用不好,反而会引入各种奇怪的问题。我建议你在开发板上先跑通一个简单的 SPI 或 I2C 设备的 Runtime PM,把整个流程走一遍,再应用到复杂设备上。