Zephyr电源管理-Tickless Idle

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

本文基于cortex-m arch分析Zephyr 如何实现Tickless Idle省电。

大多数边缘节点OS都会将Tickless Idle作为省电的手段,Zephyr也不例外。

原理

一般情况下OS的thread有效工作时间是不能占满CPU工作时间的,在其等待的时候有一个idle thread占用CPU,Tickless idle就是将idle thread占用CPU的运行时间转为CPU idle,达到省电的目的。
tickless
上图示例了一个跑了3个thread: Ta,Tb,Tc。这三个thread根据实际的应用情况调度,在t1,t2,t3这三个时间段这3个thread都不需要工作,在Normal的状态下,Idle thread将占用这3个时间段的CPU,让CPU空转。当引入Tickless Idle后,一旦3个thread不工作转到idle时,idle thread会自动检测下一次thread工作的时间,设置timer,然后让CPU进入idle,当timer超时就唤醒CPU,继续调度工作。以t1为例:当Tc调度结束后进入idle thread, idle thread计算到下一次Tb被调度的时间间隔t1, 以t1设置timer,然后让CPU进入idle,t1时间到后timer中断产生唤醒CPU,切换到Tb开始运行

实现

Idle

虽然在Tickless Idle的情况下Idle thread不再长时间占用CPU,但还是需要用来检测Idle时间设置timer和控制CPU进入idle.

创建Idle thread

在系统启动时创建Idle thread:
zephyr/kernel/init.c
z_cstart->prepare_multithreading->init_idle_thread

1
2
3
4
5
6
7
8
static void init_idle_thread(struct k_thread *thr, k_thread_stack_t *stack)
{
//创建idle thread
z_setup_new_thread(thr, stack,
CONFIG_IDLE_STACK_SIZE, idle, NULL, NULL, NULL,
K_LOWEST_THREAD_PRIO, K_ESSENTIAL, IDLE_THREAD_NAME);
z_mark_thread_as_started(thr);
}

进入Idle

从idle thread可以看到主要是在sys_power_save_idle进行处理
zephyr/kernel/idle.c

1
2
3
4
5
6
7
8
void idle(void *unused1, void *unused2, void *unused3)
{
while (true) {
(void)z_arch_irq_lock();
sys_power_save_idle();
IDLE_YIELD_IF_COOP();
}
}

这里只提取和Tickless Idle相关的代码

1
2
3
4
5
6
7
8
9
10
11
static void sys_power_save_idle(void)
{
s32_t ticks = z_get_next_timeout_expiry(); //计算到下一次调度的时间

#ifdef CONFIG_SYS_CLOCK_EXISTS
z_set_timeout_expiry((ticks < IDLE_THRESH) ? 1 : ticks, true); //设置tick timer,该timer将在ticks后超时产生中断
#endif

k_cpu_idle(); //让CPU进入idle状态

}

在zephyr/include/kernel.h中可以看到实际执行idle的函数是z_arch_cpu_idle

1
2
3
4
static inline void k_cpu_idle(void)
{
z_arch_cpu_idle();
}

不同的arch有不同的z_arch_cpu_idle实现,对于arm来说实现在zephyr/arch/arm/core/cpu_idle.S中,这里只列出Armv7架构下的代码

1
2
3
4
5
6
7
SECTION_FUNC(TEXT, z_arch_cpu_idle)
eors.n r0, r0
msr BASEPRI, r0

wfi

bx lr

可以看到是用wfi指令让ARM CPU进入idle, wfi 进入idle状态的具体行为并未在架构文档中定义,只是说明关闭总线活动和停止执行CPU指令,实际idle的行为是由soc设计者具体实现。ARM®v7-M Architecture RM原文如下


A common implementation practice is to complete any entry into powerdown routines with a WFI instruction.
Typically, the WFI instruction:

  1. Forces the suspension of execution, and of all associated bus activity.
  2. Ceases to execute instructions from the processor.

The control logic required to do this typically tracks the activity of the bus interfaces of the processor. This means
it can signal to an external power controller that there is no ongoing bus activity.
The exact nature of this interface is IMPLEMENTATION DEFINED, but the use of Wait For Interrupt as the only architecturally-defined mechanism that completely suspends execution makes it very suitable as the preferred powerdown entry mechanism.


唤醒

当tick timer超时,中断产生时CPU从idle状态中唤醒,tick timer的中断处理函数定义在中断向量表zephyr/arch/arm/core/cortex_m/vector_table.S中

1
2
3
4
5
#if defined(CONFIG_SYS_CLOCK_EXISTS)
.word z_clock_isr
#else
.word z_arm_reserved
#endif

实现在zephyr/drivers/timer/cortex_m_systick.c中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void z_clock_isr(void *arg)
{
//重置timer
cycle_count += last_load;
dticks = (cycle_count - announced_cycles) / CYC_PER_TICK;
announced_cycles += dticks * CYC_PER_TICK;

overflow_cyc = SysTick->CTRL; /* Reset overflow flag */
overflow_cyc = 0U;

z_clock_announce(TICKLESS ? dticks : 1);

//恢复调度
z_arm_exc_exit();
}

z_arm_exc_exit定义在zephyr/arch/arm/core/exc_exit.S中,下面只列出关键代码移除了一些无关的代码,显示了恢复调度的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SECTION_SUBSEC_FUNC(TEXT, _HandlerModeExit, z_arm_exc_exit)
ldr r0, =_kernel

ldr r1, [r0, #_kernel_offset_to_current]

ldr r0, [r0, #_kernel_offset_to_ready_q_cache]
cmp r0, r1
beq _EXIT_EXC //r1中装入的是当前thread, r0中装入的是ready q中将要调度的thread,如果相等就直接恢复到idle thread中,不等就进入的pendsv中切换到将要调度的thread的上下文中

ldr r1, =_SCS_ICSR
ldr r2, =_SCS_ICSR_PENDSV
str r2, [r1] //进入到pendsv重新调度到其它thread

_EXIT_EXC:
bx lr //恢复到idle thread中执行后重新调度

参考

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