Zephyr电源管理-系统电源管理

Creative Commons
本作品采用知识共享署名

本文描述Zephyr系统电源管理的实现

概述

Zephyr系统电源管理可以看作是比Tickless Idle更进一步的省电手段,在满足一定的条件下让整个系统进入更深层次的睡眠以达到更省电的目的。更深层次的睡眠方式一般是由SOC定义,不同的SOC会有不同的方式,本文主要是介绍系统电源管理的架构,分析Zephyr框架是如何调用到SOC系统级别的省电功能,但不会深入分析具体SOC的省电方式。

系统电源状态

和大多数做法一样Zephyr将电源状态分为:激活,睡眠,深度睡眠3种,并对这3种状态进行的定义,同时支持扩展了睡眠和深度睡眠的种类,以支持SOC更细粒度的电源控制能力,在include/power/power.h的enum power_states中可以看到zephyr定义的电源状态。

激活状态(Active State)

系统正常运行的状态,CPU和各外设时钟都上电,代码中对应SYS_POWER_STATE_ACTIVE

睡眠状态(Sleep State)

Zephyr要支援睡眠状态需要配置CONFIG_SYS_POWER_SLEEP_STATES
CPU停止工作,内存保留上下文. 一些Soc时钟被关闭,外设配置被保留。唤醒时可以恢复上下文。 具体哪些Soc时钟会被关闭,取决于Soc的具体实现,一些SOC会有比较细粒度的睡眠控制,因此Zephyr的框架允许多个睡眠状态,最多可以支持3种睡眠状态

1
2
3
SYS_POWER_STATE_SLEEP_1
SYS_POWER_STATE_SLEEP_2
SYS_POWER_STATE_SLEEP_3

深度睡眠状态(Deep Sleep State)

Zephyr要支援睡眠状态需要配置CONFIG_SYS_POWER_DEEP_SLEEP_STATES
CPU掉电,丢失上下文,恢复将重新执行启动代码或者是有引导程序确定的恢复点开始执行, 相当于是进行了reset,只是一些RAM的特定的区域会被保留。和睡眠状态一样,一些SOC会有比较细粒度的深度睡眠控制,因此Zephyr的框架允许多个升读睡眠状态,最多可以支持3种睡眠状态

1
2
3
SYS_POWER_STATE_DEEP_SLEEP_1
SYS_POWER_STATE_DEEP_SLEEP_2
SYS_POWER_STATE_DEEP_SLEEP_3

电源管理策略

电源管理策略的代码在subsys/power/policy下面, 由sys_pm_policy_next_state函数根据要休眠的tick数来决定进入那种电源状态,Zephry支持3种系统电源策略,只能选择某一种作为系统电源管理的策略,如下

Dummy

当配置CONFIG_SYS_PM_POLICY_DUMMY时会启用dummy策略,该策略会让系统在几个电源状态下轮流切换,一般只用于测试。例如加了一种电源状态的实现可以用dummy的轮流切换机制去测试到这种状态。
可以在文件policy_dummy.c中的sys_pm_policy_next_state的实现,不再做详细说明

Application

当配置CONFIG_SYS_PM_POLICY_APP时会启用Application策略,该策略下需要由app来实现sys_pm_policy_next_state,实现者可以根据实际情况在sys_pm_policy_next_state中决定系统进入何种电源状态,这里无需分析。

Residency

当配置CONFIG_SYS_PM_POLICY_RESIDENCY时会启用Residency策略,对于每种电源状态需要配置一个最小Residency时间, 当系统idle的时间大于预定的Residency时间就进入对应的电源状态。下面分析是如何实现该策略的:

Residency配置

subsys/power/policy/policy_residency.c中pm_min_residency保存了每个睡眠状态的residency时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static const unsigned int pm_min_residency[] = {
#ifdef CONFIG_SYS_POWER_SLEEP_STATES
#ifdef CONFIG_HAS_SYS_POWER_STATE_SLEEP_1
CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_1 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif

#ifdef CONFIG_HAS_SYS_POWER_STATE_SLEEP_2
CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_2 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif

#ifdef CONFIG_HAS_SYS_POWER_STATE_SLEEP_3
CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_3 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif
#endif /* CONFIG_SYS_POWER_SLEEP_STATES */

#ifdef CONFIG_SYS_POWER_DEEP_SLEEP_STATES
#ifdef CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_1
CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_1 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif

#ifdef CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_2
CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_2 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif

#ifdef CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_3
CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_3 * SECS_TO_TICKS / MSEC_PER_SEC,
#endif
#endif /* CONFIG_SYS_POWER_DEEP_SLEEP_STATES */
};

该residency时间需要在配置支援的睡眠状态是一起配置,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CONFIG_SYS_POWER_SLEEP_STATES=y
CONFIG_HAS_SYS_POWER_STATE_SLEEP_1=y
CONFIG_HAS_SYS_POWER_STATE_SLEEP_2=y
CONFIG_HAS_SYS_POWER_STATE_SLEEP_3=y

CONFIG_SYS_POWER_DEEP_SLEEP_STATES=y
CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_1=y
CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_2=y
CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_3=y


CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_1=30
CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_2=50
CONFIG_SYS_PM_MIN_RESIDENCY_SLEEP_3=80

CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_1=300
CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_2=500
CONFIG_SYS_PM_MIN_RESIDENCY_DEEP_SLEEP_3=800

以上的状态可以选其中的1种或者几种,但配置residency时间一定要满足DEEP_SLEEP_3>DEEP_SLEEP_2>DEEP_SLEEP_1>SLEEP_3>SLEEP_2>SLEEP_1, 这和策略的实现有关,下面会分析到。这里可以确认的是pm_min_residency数组内的元素是从小到大排列的

Residency匹配

再来看实际的sys_pm_policy_next_state策略函数,从前面的说明可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum power_states sys_pm_policy_next_state(s32_t ticks)
{
int i;

//到下一次调度idle的ticks比最小的residency都小(pm_min_residency[0]),说明不会进入系统电源省电状态
if ((ticks != K_FOREVER) && (ticks < pm_min_residency[0])) {
LOG_DBG("Not enough time for PM operations: %d", ticks);
return SYS_POWER_STATE_ACTIVE;
}

//将ticks和pm_min_residency内保存的时间,从大到小的匹配
for (i = ARRAY_SIZE(pm_min_residency) - 1; i >= 0; i--) {

//查看对应的系统电源状态是否被disable
if (!sys_pm_ctrl_is_state_enabled((enum power_states)(i))) {
continue;
}

//挡ticks满足首先匹配到的residency条件,就返回对应的电源状态
if ((ticks == K_FOREVER) ||
(ticks >= pm_min_residency[i])) {
LOG_DBG("Selected power state %d "
"(ticks: %d, min_residency: %u)",
i, ticks, pm_min_residency[i]);
return (enum power_states)(i);
}
}

//没有匹配到,系统电源任然处于激活状态
LOG_DBG("No suitable power state found!");
return SYS_POWER_STATE_ACTIVE;
}

从上面的代码可以看到,idle的ticks是在pm_min_residency内从后向前匹配的,因此pm_min_residency内的residency一定要从小到大放置。例如定义两个睡眠状态SLEEP_1=30和DEEP_SLEEP_1=300:

1
2
3
4
static const unsigned int pm_min_residency[] = {
30,
300
}

当要idle的ticks为400时,就要进入DEEP_SLEEP_1,ticks为50时就要进入SLEEP_1,当ticks为20时进行Tickless Idle。

系统电源控制

Zephyr的系统电源在编译的时候会根据配置项决定支持哪些系统电源省电状态,在运行时也可以使用下面的函数动态的开启和关闭系统电源状态

1
2
3
void sys_pm_ctrl_disable_state(enum power_states state)     //关闭指定的系统电源状态
void sys_pm_ctrl_enable_state(enum power_states state) //开启指定的系统电源状态
bool sys_pm_ctrl_is_state_enabled(enum power_states state) //获取指定的系统电源状态

上一节的sys_pm_policy_next_state中可以看到会使用sys_pm_ctrl_is_state_enabled,开启和关闭的API会被应用或者其他组件调用。具体实现的原理是就是设置和读取power_state_disable_count数组变量,代码很简单这里贴出来说明,可以参看pm_ctrl.c

系统电源管理工作流程

代码如下,和Tickless Idle一样的框架流程,都是在idle thread内sys_power_save_idle完成,但有了sleep和deep_sleep的代码最后可以简化为如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void sys_power_save_idle(void)
{
s32_t ticks = z_get_next_timeout_expiry();

z_set_timeout_expiry((ticks < IDLE_THRESH) ? 1 : ticks, true);


set_kernel_idle_time_in_ticks(ticks);
sys_pm_idle_exit_notify = 1U;

//_sys_suspend根据要idle的ticks判断是否要进sleep/deep sleep
//如果进入了sleep/deep sleep将不会再从这里返回
if (_sys_suspend(ticks) == SYS_POWER_STATE_ACTIVE) {
sys_pm_idle_exit_notify = 0U;

//不进sleep/deep sleep继续走Tickless Idle流程
k_cpu_idle();
}

}

进入系统电源状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
enum power_states _sys_suspend(s32_t ticks)
{
bool deep_sleep;

//由策略计算函数获取将要进入的系统电源状态
pm_state = (forced_pm_state == SYS_POWER_STATE_AUTO) ?
sys_pm_policy_next_state(ticks) : forced_pm_state;

//如果计算出要进入的系统电源状态还是activie,直接返回
if (pm_state == SYS_POWER_STATE_ACTIVE) {
LOG_DBG("No PM operations done.");
return pm_state;
}

//检查是否为深度睡眠状态
deep_sleep = IS_ENABLED(CONFIG_SYS_POWER_DEEP_SLEEP_STATES) ?
sys_pm_is_deep_sleep_state(pm_state) : 0;

post_ops_done = 0;

//sys_pm_notify_power_state_entry是个weak符号,可以由应用重写,由应用决定进入系统电源状态要做的事情
sys_pm_notify_power_state_entry(pm_state);

if (deep_sleep) {
//对于深度睡眠,在有device电源管理的情况下,还要让一些device进入低功耗状态,这里展开说明,之后在device电源管理的文章中说明
#if CONFIG_DEVICE_POWER_MANAGEMENT
/* Suspend peripherals. */
if (sys_pm_suspend_devices()) {
LOG_ERR("System level device suspend failed!");
sys_pm_notify_power_state_exit(pm_state);
pm_state = SYS_POWER_STATE_ACTIVE;
return pm_state;
}
#endif
//由于进入深度睡眠后再唤醒,系统需要重走初始化流程,因此设置标志,无需执行唤醒的nodify
_sys_pm_idle_exit_notification_disable();
}

//进入系统电源状态,sys_set_power_state函数会被不同的soc根据实际情况实现
//注意如果在这里进入SOC深度睡眠后,将重周初始化流程,之后的代码将不会执行
sys_set_power_state(pm_state);

#if CONFIG_DEVICE_POWER_MANAGEMENT
//对于深度睡眠,在有device电源管理的情况下device要退出深度睡眠
if (deep_sleep) {
/* Turn on peripherals and restore device states as necessary */
sys_pm_resume_devices();
}
#endif

//只有非深度睡眠才会走这里,理论上后面的z_sys_power_save_idle_exit->_sys_resume在中断中会做这件事,这里的流程应该是不必要的
if (!post_ops_done) {
post_ops_done = 1;
//sys_pm_notify_power_state_exit是个weak符号,可以由应用重写,由应用决定退出系统电源状态要做的事情
sys_pm_notify_power_state_exit(pm_state);
//退出当前系统电源状态,_sys_pm_power_state_exit_post_ops函数会被不同的soc根据实际情况实现
_sys_pm_power_state_exit_post_ops(pm_state);
}

return pm_state;
}

退出系统电源状态

对于深度睡眠进行唤醒后会重走启动流程,所以不会走到这一步。该流程一般只针对退出睡眠状态.
一般情况下中断会将SOC从睡眠状态中唤醒,因此在中断warpper函数中调用z_sys_power_save_idle_exit执行退出动作
在软中断向量表中设置中断warpper函数
arch/common/isr_tables.c

1
2
3
u32_t __irq_vector_table _irq_vector_table[IRQ_TABLE_SIZE] = {
[0 ...(IRQ_TABLE_SIZE - 1)] = (u32_t)&_isr_wrapper,
};

arch\arm\core\isr_wrapper.S 实现代码可以看到调用了睡眠退出函数z_sys_power_save_idle_exit

1
2
3
4
5
6
7
SECTION_FUNC(TEXT, _isr_wrapper)
push {r0,lr}
ittt ne
movne r1, #0
/* clear kernel idle state */
strne r1, [r2, #_kernel_offset_to_idle] //清掉_kernel->idle
blne z_sys_power_save_idle_exit //调用睡眠退出函数

退出函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void z_sys_power_save_idle_exit(s32_t ticks)
{
#if defined(CONFIG_SYS_POWER_SLEEP_STATES)
//退出睡眠
if (sys_pm_idle_exit_notify) {
_sys_resume();
}
#endif

//退出Tickless idle,重设tick clock
z_clock_idle_exit();
}

void _sys_resume(void)
{
if (!post_ops_done) {
post_ops_done = 1;
//sys_pm_notify_power_state_exit是个weak符号,可以由应用重写,由应用决定退出系统电源状态要做的事情
sys_pm_notify_power_state_exit(pm_state);
//退出当前系统电源状态,_sys_pm_power_state_exit_post_ops函数会被不同的soc根据实际情况实现
_sys_pm_power_state_exit_post_ops(pm_state);
}
}

关于z_clock_idle_exit()这里补充说明一下Tickless idle一文没有说明的内容:一般的中断也会唤醒Tickless idle,因此也会走到z_sys_power_save_idle_exit流程来执行z_clock_idle_exit,然后由IRQ恢复调度。

其它

和Tickless的关系

要支援System power manage必须要开启Tickless, 当不满足System power manage的条件时仍然执行Tickless Idle.

Soc省电功能的概览

虽然详细分析SOC的睡眠和深度睡眠省电实现,但还是以nrf为例大致看一下实现,在zephyr/soc/arm/nordic_nrf/nrf52/power.c中可以看到只支持SYS_POWER_STATE_DEEP_SLEEP_1省电,这是明显的soc相关实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void sys_set_power_state(enum power_states state)
{
switch (state) {
#ifdef CONFIG_SYS_POWER_DEEP_SLEEP_STATES
#ifdef CONFIG_HAS_SYS_POWER_STATE_DEEP_SLEEP_1
case SYS_POWER_STATE_DEEP_SLEEP_1:
nrf_power_system_off();
break;
#endif
#endif
default:
LOG_DBG("Unsupported power state %u", state);
break;
}
}

进入DEEP_SLEEP_1省电是nrf_power_system_off,其是在hal_nordic中实现,代码是写SOC的省电功能的寄存器完成的

1
2
3
4
5
__STATIC_INLINE void nrf_power_system_off(void)
{
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
__DSB();
}

具体的行为可以查看Nrf52832的参考手册”System OFF mode”章节

参考

http://docs.zephyrproject.org/latest/reference/power_management/index.html