4、设备运行时电源管理(Runtime PM)
Runtime PM框架介绍
Runtime PM,说白了就是让设备在不工作的时候自动休眠,需要的时候再醒来。这跟系统级的suspend/resume不一样——系统休眠是整个板子都睡,Runtime PM是单个设备自己睡自己的。
我刚开始接触这个机制时,觉得它挺鸡肋的。心想:设备都空闲了,省那点电有啥意义?直到有一次做一款电池供电的IoT设备,发现大部分外设其实都在空转,白白耗电。嗯,从那以后,我对Runtime PM就格外重视了。
Runtime PM的核心思想很简单:
- 设备空闲 → 自动挂起(suspend)
- 设备需要工作 → 自动唤醒(resume)
- 整个过程对驱动开发者透明,你只需要调用几个API
你想想看,一个嵌入式设备里可能有几十个外设:I2C、SPI、UART、GPIO……如果每个都在空闲时自动休眠,整体功耗能降一个数量级。
核心API:pm_runtime_get/put
这两个API是Runtime PM的左膀右臂。我习惯把它们理解成「引用计数锁」:
pm_runtime_get():告诉内核「我要用这个设备了,别让它睡」pm_runtime_put():告诉内核「我用完了,可以睡了」
来看一个实际例子。我在做触摸屏驱动时,每次上报触摸事件前都要先唤醒设备:
/* 触摸中断处理函数 */
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
struct device *dev = dev_id;
/* 告诉内核:我要用设备了 */
pm_runtime_get_sync(dev);
/* 读取触摸坐标 */
read_touch_data(dev);
/* 告诉内核:我用完了 */
pm_runtime_put(dev);
return IRQ_HANDLED;
}
这里有个细节要注意:pm_runtime_get_sync()是同步版本,它会等设备完全唤醒后才返回。还有个异步版本pm_runtime_get(),它只是提交请求就返回了。我个人习惯在中断上下文用同步版本,因为中断处理需要尽快完成,等唤醒完成再操作更安全。
pm_runtime_get_sync()时,如果设备正在休眠,这个调用会触发resume流程,而resume里可能又去操作同一个设备,导致死锁。解决办法是:在resume回调里不要调用任何pm_runtime_*函数。
核心API:pm_runtime_enable/disable
这两个API控制设备的Runtime PM功能是否开启:
pm_runtime_enable():开启Runtime PM,设备可以自动休眠pm_runtime_disable():关闭Runtime PM,设备一直保持唤醒
一般在驱动的probe()函数里开启,在remove()函数里关闭:
static int my_driver_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
/* 初始化硬件 */
init_hardware(dev);
/* 开启Runtime PM */
pm_runtime_enable(dev);
/* 设置自动挂起的超时时间(单位:ms) */
pm_runtime_set_autosuspend_delay(dev, 1000);
pm_runtime_use_autosuspend(dev);
return 0;
}
static int my_driver_remove(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
/* 关闭Runtime PM */
pm_runtime_disable(dev);
/* 清理硬件 */
cleanup_hardware(dev);
return 0;
}
为什么要在probe里就enable?因为设备初始化完成后,可能马上就空闲了,这时候就应该允许它休眠。我见过一些驱动在probe里忘了enable,结果设备一直醒着,白白耗电。
自动挂起与唤醒机制
自动挂起,就是设备空闲一段时间后自己决定休眠。这个「一段时间」就是上面代码里的autosuspend_delay。
为什么要有延迟?你想想看,如果设备刚空闲就休眠,下一秒又来一个请求,又得唤醒,这来回折腾的功耗可能比不休眠还大。所以设置一个合理的延迟很重要。
我一般这样设置延迟时间:
| 设备类型 | 建议延迟 | 说明 |
|---|---|---|
| 触摸屏 | 100-500ms | 用户触摸间隔通常较长 |
| 传感器 | 50-200ms | 数据采集频率较高 |
| 网络接口 | 1-5s | 网络流量突发性强 |
| 存储设备 | 2-10s | 文件操作可能密集出现 |
自动唤醒机制就更简单了——只要有设备访问请求,内核会自动调用resume回调。你不需要手动干预。
但这里有个容易忽略的点:唤醒的时机。我记得有一次调试一个音频设备,播放完音乐后设备自动休眠了,但用户很快又点击播放,结果唤醒有延迟,导致开头几毫秒的声音丢失了。后来我把延迟时间从500ms改成了2s,问题就解决了。
cat control查看当前策略,echo on > control强制唤醒设备。这在调试时特别有用。
驱动中需要实现的回调
要让Runtime PM工作,驱动需要实现两个回调:
static int my_dev_runtime_suspend(struct device *dev)
{
/* 保存硬件状态,关闭时钟 */
save_hw_state(dev);
clk_disable(dev->clk);
return 0;
}
static int my_dev_runtime_resume(struct device *dev)
{
/* 开启时钟,恢复硬件状态 */
clk_enable(dev->clk);
restore_hw_state(dev);
return 0;
}
static const struct dev_pm_ops my_dev_pm_ops = {
SET_RUNTIME_PM_OPS(my_dev_runtime_suspend,
my_dev_runtime_resume,
NULL)
};
这里SET_RUNTIME_PM_OPS宏会帮你注册suspend和resume回调。第三个参数是runtime_idle回调,一般设为NULL就行,内核会用默认行为。
好了,关于Runtime PM的核心内容就这些。说白了就是三件事:引用计数控制休眠、设置延迟避免频繁切换、实现回调完成实际操作。掌握了这些,你就能让设备在空闲时自动省电,需要时立刻响应。