4. Runtime PM(运行时电源管理)核心机制

各位同学,今天我们来聊聊 Runtime PM。说实话,这个机制是我在嵌入式低功耗设计中打交道最多的一个。你想想看,系统级休眠固然重要,但很多时候我们只需要让某个外设在不工作时偷偷睡个觉——这就是 Runtime PM 的用武之地。

4.1 Runtime PM 的框架结构

Runtime PM 的框架,说白了就是一套「设备级电源管理」的调度系统。它不像系统休眠那样一刀切,而是精细到每个设备。

我个人习惯把它的框架想象成三层结构:

  • 底层:硬件操作层。负责实际控制设备的电源门控、时钟门控。
  • 中间层:核心调度层。管理引用计数、状态机转换、自动挂起定时器。
  • 上层:驱动接口层。提供 pm_runtime_get/put 等 API 给驱动开发者调用。

嗯,这里要注意:中间层是 Linux 内核帮你实现的,你只需要在驱动里注册好回调函数就行。

核心数据结构:每个设备都有一个 struct dev_pm_info,里面记录了引用计数、挂起状态、自动挂起延迟等关键信息。

上层:驱动接口层 pm_runtime_get() / pm_runtime_put() / pm_runtime_autosuspend() 中间层:核心调度层 引用计数管理 / 状态机转换 / 自动挂起定时器 底层:硬件操作层 电源门控 / 时钟门控 / 寄存器保存与恢复

4.2 核心 API:pm_runtime_get / put

这两个 API 是 Runtime PM 的灵魂。我刚开始用的时候也犯过糊涂,后来总结了一句话:get 就是「我要用设备了,别睡」;put 就是「我用完了,可以睡了」

4.2.1 pm_runtime_get_sync()

这个函数会做两件事:

  1. 增加设备的引用计数
  2. 如果设备当前处于挂起状态,立即唤醒它
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,把整个流程走一遍,再应用到复杂设备上。

专注资料整理