4、设备电源管理:设备运行时PM、设备自动PM、设备强制PM

好,咱们接着聊设备电源管理。这部分内容,说白了就是怎么让单个设备在系统里「省着点用电」。Zephyr 在这方面给了我们三种手段:运行时 PM、自动 PM 和强制 PM。我刚开始接触时也觉得有点绕,但用熟了就会发现,它们各有各的用武之地。

4.1 设备运行时 PM(Runtime PM)

运行时 PM,是我个人用得最多的一种方式。它的核心思想是:设备不用的时候就挂起,要用的时候再唤醒。你想想看,一个传感器可能每秒只采样一次,剩下 999 毫秒都在闲着。如果这期间还给它供电,那纯粹是浪费。

在 Zephyr 里,运行时 PM 需要设备驱动主动配合。驱动里要实现两个回调函数:pm_controlpm_device_action。前者负责处理电源状态切换,后者负责执行具体的挂起或恢复动作。

我举个例子,一个简单的 UART 驱动,运行时 PM 的实现大概长这样:

static int uart_pm_control(const struct device *dev,
                           enum pm_device_action action)
{
    switch (action) {
    case PM_DEVICE_ACTION_SUSPEND:
        /* 关闭时钟,保存寄存器状态 */
        uart_disable_clocks(dev);
        break;
    case PM_DEVICE_ACTION_RESUME:
        /* 恢复时钟,重新配置寄存器 */
        uart_enable_clocks(dev);
        uart_restore_regs(dev);
        break;
    default:
        return -ENOTSUP;
    }
    return 0;
}

嗯,这里要注意:运行时 PM 的挂起和恢复,是由设备的使用计数触发的。当设备被打开时,计数加一;关闭时,计数减一。计数归零后,系统就会自动调用挂起函数。我在项目中遇到过一个问题:某个驱动忘记在 close 时释放设备,导致计数永远不为零,设备一直醒着。排查了半天才发现是引用计数没处理好。

小技巧: 调试运行时 PM 时,可以用 pm_device_dump() 打印所有设备的状态。看看哪些设备该睡没睡,一目了然。

4.2 设备自动 PM(Automatic PM)

自动 PM 听起来很智能,对吧?其实它就是在运行时 PM 的基础上,加了一层「自动决策」的逻辑。系统会监控设备的使用情况,如果发现某个设备长时间空闲,就自动把它挂起。

Zephyr 的自动 PM 是通过一个内核线程实现的。这个线程会定期检查所有注册了自动 PM 的设备,看看它们是否满足挂起条件。默认的检查周期是 100 毫秒,你可以通过 CONFIG_PM_DEVICE_AUTO_SLEEP_TICK 调整。

我个人习惯把自动 PM 用在那些「间歇性工作」的设备上。比如一个温度传感器,每 5 秒采集一次数据。如果用手动控制,我得在每次采集前后手动挂起和恢复,太麻烦了。交给自动 PM,它自己会判断空闲时间,然后自动挂起。

但自动 PM 也不是万能的。我曾经踩过一个坑:某个 SPI 闪存芯片,挂起后重新唤醒需要 10 毫秒。自动 PM 觉得它空闲了 200 毫秒,就把它挂了。结果下次读写时,唤醒时间比实际工作还长,反而更耗电。所以,自动 PM 适合那些唤醒延迟很短的设备。

注意: 自动 PM 的挂起策略是「一刀切」的。如果你的设备有特殊的唤醒延迟要求,建议用运行时 PM 手动控制,别偷懒。

4.3 设备强制 PM(Forced PM)

强制 PM,说白了就是「我不管你在干什么,现在立刻给我睡」。这通常用在系统要进入深度睡眠时,或者某个紧急情况下需要快速关闭外设。

在 Zephyr 里,强制 PM 通过 pm_device_force_power_state() 函数实现。调用这个函数后,设备会立即进入指定的电源状态,不管它当前是否在使用中。

我举个例子,系统要进入 Suspend-to-RAM 模式时,需要把所有外设都关掉。这时候用强制 PM 最合适:

void system_suspend_handler(void)
{
    const struct device *devs[] = {
        DEVICE_DT_GET(DT_NODELABEL(uart0)),
        DEVICE_DT_GET(DT_NODELABEL(spi1)),
        DEVICE_DT_GET(DT_NODELABEL(i2c2)),
    };

    for (int i = 0; i < ARRAY_SIZE(devs); i++) {
        pm_device_force_power_state(devs[i], PM_DEVICE_STATE_SUSPEND);
    }
}

强制 PM 有个风险:如果设备正在传输数据,强行挂起可能导致数据丢失。我在项目中遇到过这种情况:强制挂起一个正在写数据的 eMMC,结果文件系统损坏了。后来我加了一个检查,确保设备空闲了再强制挂起。

核心要点: 强制 PM 是最后的手段。能用运行时 PM 或自动 PM 解决的,就别用强制 PM。它就像「拔电源」,虽然快,但容易伤硬件。

4.4 三种方式的对比与选择

说了这么多,你可能想问:到底该用哪种?我整理了一个表格,方便你对比:

特性 运行时 PM 自动 PM 强制 PM
控制粒度 精细(驱动层控制) 中等(系统层决策) 粗糙(强制切换)
使用场景 设备使用频率可预测 设备间歇性工作 系统深度睡眠或紧急关闭
唤醒延迟 低(驱动优化) 中等(需检查空闲时间) 高(可能丢失数据)
实现复杂度 高(需修改驱动) 低(配置即可) 低(直接调用 API)
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

我个人建议:优先用运行时 PM,因为它最灵活,也最省电。如果设备的工作模式比较固定,可以考虑自动 PM。强制 PM 嘛,留着当「核武器」用就行,别动不动就掏出来。

避坑指南: 我曾经在一个项目里,三种 PM 混着用,结果设备状态乱成一锅粥。后来我定了个规矩:每个设备只选一种 PM 方式,不要混用。这样调试起来也方便。

好了,设备电源管理这部分就聊到这儿。记住,电源管理不是「越省电越好」,而是「在满足功能的前提下,尽量省电」。下一章咱们聊聊系统级的电源管理策略,那个更有意思。