4、设备电源管理:设备运行时电源管理(RPM)、设备挂起/恢复机制、设备电源状态回调函数

好,咱们进入设备电源管理这一节。说实话,这部分是Zephyr低功耗设计的核心战场。你想想看,系统级休眠固然重要,但真正让功耗降下来的,往往是每个外设的精细管理。我做过一个物联网网关项目,光靠系统休眠只能省30%的电,把每个传感器、通信模块的电源管起来之后,功耗直接砍掉70%。差距就在这里。

4.1 设备运行时电源管理(RPM)

RPM,全称是Runtime Power Management。说白了,就是设备在工作状态下,也能动态调整功耗。不是非得等到系统休眠才省电。

举个例子。你有一个UART外设,大部分时间它都在等数据。这时候你让它全速运行,电流几十毫安,完全没必要。RPM的思路是:设备空闲时,自动进入低功耗状态;有数据来了,再快速唤醒。

Zephyr里怎么实现?核心是调用pm_device_runtime_getpm_device_runtime_put这对API。

// 获取设备,确保它处于可用状态
int ret = pm_device_runtime_get(dev);
if (ret < 0) {
    // 处理错误
    return ret;
}

// 使用设备进行数据传输
uart_fifo_fill(dev, data, len);

// 使用完毕,释放设备,允许它进入低功耗
pm_device_runtime_put(dev);

这里有个关键点:pm_device_runtime_get会递增设备的引用计数。只有引用计数归零时,设备才会真正进入低功耗。我刚开始用的时候犯过一个错——在中断里调用了pm_device_runtime_put,结果引用计数乱掉了,设备再也醒不过来。嗯,后来我养成了习惯:get和put一定要成对出现,最好在同一个函数作用域里。

我的经验:RPM最适合那些有明确空闲和活跃周期的外设。比如SPI、I2C、UART这类总线外设。对于持续工作的设备,比如ADC连续采样,RPM反而会增加开销。别盲目用。

4.2 设备挂起/恢复机制

挂起和恢复,是设备电源管理的另一个维度。它跟RPM的区别在于:RPM是运行时动态调整,挂起/恢复是系统主动触发的状态切换。

Zephyr里,设备挂起通过pm_device_action_run来实现。这个函数会调用设备驱动里注册的回调函数。

// 挂起设备
int ret = pm_device_action_run(dev, PM_DEVICE_ACTION_SUSPEND);
if (ret < 0) {
    // 挂起失败,可能是设备正忙
    LOG_ERR("Failed to suspend device: %d", ret);
}

// 恢复设备
ret = pm_device_action_run(dev, PM_DEVICE_ACTION_RESUME);
if (ret < 0) {
    LOG_ERR("Failed to resume device: %d", ret);
}

挂起的时候,驱动会保存设备的状态寄存器,关闭时钟,切断电源(如果硬件支持)。恢复的时候,反向操作:上电、恢复寄存器、重新初始化。

我曾经在一个项目里遇到个坑:某个传感器挂起后,恢复时没有重新配置采样率,结果数据全乱了。从那以后,我写驱动时都会在恢复函数里把关键配置重新写一遍,而不是只恢复寄存器。安全第一。

注意:挂起操作可能会失败。比如设备正在执行DMA传输,这时候强行挂起会导致数据丢失。Zephyr的框架会返回-EBUSY,你的应用层代码一定要处理这个错误。

4.3 设备电源状态回调函数

回调函数,是设备电源管理的灵魂。每个设备驱动都需要实现一组回调,告诉Zephyr的电源管理框架:我支持哪些状态,怎么切换。

回调函数定义在struct pm_device结构体里。核心是pm_device_action_cb_t这个类型。

static int my_device_pm_action(const struct device *dev,
                               enum pm_device_action action)
{
    switch (action) {
    case PM_DEVICE_ACTION_SUSPEND:
        // 保存状态,关闭外设时钟
        my_device_save_context(dev);
        my_device_disable_clock(dev);
        break;

    case PM_DEVICE_ACTION_RESUME:
        // 开启时钟,恢复状态
        my_device_enable_clock(dev);
        my_device_restore_context(dev);
        break;

    case PM_DEVICE_ACTION_TURN_ON:
        // 完全上电
        my_device_power_on(dev);
        break;

    case PM_DEVICE_ACTION_TURN_OFF:
        // 完全断电
        my_device_power_off(dev);
        break;

    default:
        return -ENOTSUP;
    }

    return 0;
}

这里要注意,回调函数是在中断上下文还是线程上下文执行的?Zephyr的电源管理框架会保证在合适的上下文中调用。但你的回调函数里不能做阻塞操作,比如等待信号量、延时。我见过有人直接在回调里调k_sleep,结果系统直接死锁。

设备驱动注册时,需要把回调函数挂上去:

PM_DEVICE_DEFINE(my_device, my_device_pm_action,
                 PM_DEVICE_STATE_ACTIVE);

这个宏会创建一个pm_device实例,初始状态是PM_DEVICE_STATE_ACTIVE。之后框架会根据系统策略,自动调用回调来切换状态。

核心要点:

  • 回调函数必须快速返回,不能阻塞
  • 状态切换要幂等——多次挂起不会导致问题
  • 保存和恢复的上下文要完整,尤其是中断使能位

4.4 三种机制的协同工作

RPM、挂起/恢复、回调函数,这三者不是孤立的。它们共同构成了Zephyr的设备电源管理体系。

我画个简单的逻辑图给你看:

机制 触发方式 典型场景
RPM 应用层调用get/put UART等待数据、SPI总线空闲
挂起/恢复 系统电源管理策略触发 进入系统休眠前、从休眠唤醒后
回调函数 驱动内部实现 所有状态切换的实际执行者

实际项目中,RPM负责细粒度的功耗控制,挂起/恢复负责粗粒度的状态切换。回调函数是底层执行者。你想想看,如果没有回调函数,框架怎么知道你的设备该怎么省电?所以写驱动时,回调函数是必须实现的。

最后说一句:调试设备电源管理时,我建议先用CONFIG_PM_DEVICE_RUNTIME_LOG_LEVEL_DBG打开调试日志。这样你能看到每个设备的get/put调用情况。我曾经靠这个日志,抓到一个设备引用计数泄漏的bug——某个驱动在错误路径上忘了调用put,导致设备永远无法休眠。日志里一看,引用计数一直不为零,问题就找到了。

嗯,设备电源管理这部分就讲到这里。下一节我们聊聊系统级的电源策略,怎么把设备管理跟系统休眠结合起来。