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,问题就解决了。

我的调试小技巧:在/sys/devices/.../power/目录下,你可以直接查看和修改Runtime PM的状态。比如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的suspend/resume回调里,不要做耗时操作。我曾经在一个resume回调里做了msleep(100),结果导致整个系统的响应都变慢了。记住:休眠和唤醒应该尽可能快,最好在微秒级别完成。

好了,关于Runtime PM的核心内容就这些。说白了就是三件事:引用计数控制休眠、设置延迟避免频繁切换、实现回调完成实际操作。掌握了这些,你就能让设备在空闲时自动省电,需要时立刻响应。