电源管理

XWOS的电源管理

概述

XWOS有两套电源管理框架,分别用于MP系统和UP系统。

XWOS的电源管理框架只提供了基本流程,最终SOC如何休眠由BSP中的回调函数实现: 用户可以自行决定是否保持SDRAM的刷新、是否保持某些GPIO的输出、或则是否进入最低功耗的待机等等。

XWOS多核系统中,CPU不唯一,每个CPU的休眠与唤醒流程相同且相互独立运行。 XWOS单核系统中,单一CPU的休眠与唤醒流程与多核系统任意CPU的流程相同。

流程

每个CPU的电源管理流程
Photo: xwos.tech / CC-BY-SA-4.0

图中,左列为休眠流程,右列为唤醒流程,箭头代表了可以进行 电源管理阶段 转换的方向。 电源管理领域 将电源管理分为四个阶段(图中绿色的框):

正在运行 ( XWOS_PM_STAGE_RUNNING )

所有CPU正常运行,可以使用下面CAPI进入休眠的流程:

xwer_t xwos_pm_suspend(void);

此CAPI是操作系统抽象层 OSAL 中的CAPI,实际调用的是:

  • 多核系统: xwer_t xwmp_pm_suspend(void);
  • 单核系统: xwer_t xwup_pm_suspend(void);
冻结线程 —— 解冻线程 ( XWOS_PM_STAGE_FREEZING - XWOS_PM_STAGE_THAWING )

一旦开始休眠流程,内核会中断所有线程的 等待睡眠 态,并为所有线程设置 可冻结 标志, 然后依次调度每个线程,直到它们运行到 冻结点 进行冻结。

这个过程耗时比较长,在执行过程中如果遇到唤醒事件,内核可在唤醒事件中断中切换为解冻流程: 解冻已冻结的线程,并取消未冻结的线程的 可冻结 标志。

全部线程已冻结 ( XWOS_PM_STAGE_ALLFRZ )

这是一个比较短暂的状态。当最后一个线程完成冻结时,CPU处于调度器服务中断中,内核会将滴答定时器关闭。 从此时开始,内核会将上下文(Context)切换为 电源管理 (XWOS_SKD_CONTEXT_PWRMNT)

如果此刻出现唤醒事件,待CPU从调度器服务中断中退出后,会立即进入唤醒事件中断。 休眠流程切换为唤醒流程,并重新打开滴答定时器。 按照 调度器的中断 中的约束,唤醒事件中断的优先级低于调度器服务中断, 因此唤醒事件中断一定会在调度器服务中断退出后才开始执行, 滴答定时器一定会先关闭,然后再打开,顺序不会错乱。

若未出现唤醒中断,CPU从调度器服务中断中退出后,会切换回线程上下文,此时因所有线程已经冻结, CPU只可能运行在 空闲任务 中。

正在暂停 —— 正在恢复 ( XWOS_PM_STAGE_SUSPENDING - XWOS_PM_STAGE_RESUMING )

内核会在 空闲任务 中执行用户的 suspend() 回调函数。 此时,如果出现唤醒事件,系统会在唤醒事件中断中将状态从 正在暂停 切换为 正在恢复 , 并执行用户的 resume() 回调函数。

  • suspend() 回调函数用于在休眠之前让用户关闭设备、配置SDRAM刷新模式、配置GPIO等;
  • resume() 回调函数用于在唤醒之前执行与 suspend() 回调函数相反的操作;

这两个回调函数是每CPU独立的,每个CPU只能执行自己的回调函数。 执行这两个函数时,内核会关闭CPU总中断开关。 如果正在执行 suspend() 回调函数时出现了唤醒事件,唤醒事件中断会挂起,等待 suspend() 返回, 内核才会打开CPU总中断开关,然后执行唤醒事件的中断函数。

示例,WeActMiniStm32H750的 resume()suspend() 回调函数:

void stm32hal_resume(void)
{
        xwds_pm_resume(&stm32xwds); /* 恢复所有设备 */
}

void xwosac_pmcb_resume(void * arg)
{
        xwsq_t ctx;
        xwirq_t irq;

        XWOS_UNUSED(arg);
        xwos_skd_get_context_lc(&ctx, &irq); /* 获取上下文以及中断号,用于调试 */
        stm32hal_resume();
}

void stm32hal_suspend(void)
{
        xwds_pm_suspend(&stm32xwds); /* 暂停所有设备 */

        /* 设置休眠方式为STOP模式:
           STOP模式下寄存器与内部RAM数据不丢失,
           因此休眠方式为SuspendToRAM,唤醒后运行状态可恢复。*/
        LL_PWR_SetRegulModeDS(LL_PWR_REGU_DSMODE_LOW_POWER);
        LL_PWR_EnableFlashPowerDown();
        LL_PWR_CPU_SetD1PowerMode(LL_PWR_CPU_MODE_D1STOP);
        LL_PWR_CPU_SetD2PowerMode(LL_PWR_CPU_MODE_D2STOP);
        LL_PWR_CPU_SetD3PowerMode(LL_PWR_CPU_MODE_D3STOP);
        LL_LPM_EnableDeepSleep();
}

void xwosac_pmcb_suspend(void * arg)
{
        xwsq_t ctx;
        xwirq_t irq;

        XWOS_UNUSED(arg);
        xwos_skd_get_context_lc(&ctx, &irq); /* 获取上下文以及中断号,用于调试 */
        stm32hal_suspend();
}
已经暂停 ( XWOS_PM_STAGE_SUSPENDED )

此阶段为休眠流程的最后一个阶段,也是唤醒流程的第一个阶段。此阶段提供给用户的回调函数:

  • sleep() :休眠SOC
  • wakeup() :唤醒SOC

当最后一个CPU的 空闲任务 执行完上一阶段的 suspend() 回调函数后, 内核将电源管理步骤切换到此阶段,并执行 sleep() 回调函数。 SOC系统在 sleep() 回调函数内部进入低功耗状态, 此时时钟停止,代码停止运行, sleep() 回调函数不会返回。

XWOS将 sleep() 回调函数设计在空闲任务中执行,是因为中断优先级的问题有可能导致系统无法唤醒, 例如,ARM Cortex-M的内核单片机。 如果在一个高优先级的中断中执行了休眠指令( WFI ),低优先级的唤醒中断无法把系统唤醒。

当唤醒事件出现, wakeup() 回调函数在唤醒事件中断中执行。

回调函数 sleep()wakeup() 之间没有锁保护, 因此 sleep() 回调函数需要设计成能被 wakeup() 回调函数打断。

示例,WeActMiniStm32H750的 wakeup()sleep() 回调函数:

void stm32hal_wakeup(void)
{
        LL_LPM_EnableSleep(); /* 清除DEEPSLEEP位 */
        SystemClock_Config(); /* 从STOP模式恢复后,需要重新配置时钟 */
}

void xwosac_pmcb_wakeup(void * arg)
{
        XWOS_UNUSED(arg);
        stm32hal_wakeup();
}

void stm32hal_sleep(void)
{
}

void xwosac_pmcb_sleep(void * arg)
{
        XWOS_UNUSED(arg);
        stm32hal_sleep();
        /* 位置1 */
        armv7m_wfi(); /* 通过 WFI 指令进入STOP模式 */
        /* 位置2 */
}

分为两种情况讨论:

  • xwosac_pmcb_sleep() 中的 WFI 指令执行后出现唤醒事件: 唤醒流程执行完毕后,代码会回到 xwosac_pmcb_sleep()位置2
  • xwosac_pmcb_sleep() 中的 WFI 指令执行之前出现唤醒事件: 唤醒事件中断会打断 xwosac_pmcb_sleep() 函数, 然后进入唤醒事件中断函数中,随着唤醒流程执行,会调用到 xwosac_pmcb_wakeup() 函数, 函数 LL_LPM_EnableSleep() 会清除Cortex-M的DEEPSLEEP位,接下来的唤醒流程也会将滴答定时器重新打开, 当再次回到 xwosac_pmcb_sleep()位置1 时, WFI 指令只会使CPU进入ARMv7-m的SLEEP模式, 只是短暂地暂停了一下CPU时钟,即将到来的滴答定时器中断可使系统恢复正常。

CAPI参考