前言
非 Tickless 模式下,设定系统硬件 Timer 中断周期性的发生,如 timer_irq_handler() 每 1ms 中断一次。假设定时器总线频率 32.768KHz,计数器为 16 位计数器,最大计数值 (1 << 16 - 1) = 0xFFFF = 65535;1s 要完成 32768 个 counter,1ms 中断一次产生一次 overflow 则计数 32.768 个 counter;一个简单的软件定时器可能像这样:
// 假设软件定时器,利用 count 进行软件计数
struct timer {
int count;
int period;
void (*callback)(void);
};
struct timer timer[2];
timer[0].count = timer[0].period = 1000;
timer[1].count = timer[1].period = 500;
// 1ms 周期性中断
void timer_irq_handler() {
for (int i = 0; i < 2; ++i) {
if (0 == timer[i].count--) {
timer[i].count = timer[i].period;
timer[i].callback();
}
}
}
systick_handler 只有在 500ms,和 1000ms 时执行实际的回调函数,其他情况下基本是无用的中断,中断时产生的上下文切换就是无必要的开销。
Tickless 模式下,Timer 中断不是周期性的产生,只在需要的时候产生。通过设置 Timer 的溢出中断时间点(计数器计数顶点),可以让 Timer 在下一次 Timeout 的时候再产生中断。stm32 平台下通过设置 Timer 寄存器的自动重装载值 autoreload(如果使用 Systick Timer 为 Systick->LOAD),来改变中断产生的时间。如上述两个定时器中,定时发生应从小到大,先设定 500ms 的中断执行 timer[1].callback(),此时已经走过了500ms,只需要再设定500ms中断 timer[0] 完成 1s 的中断回调,同时 timer[1] 的 callback 再次发生。zephyr 通过 timeout 模块去管理这个过程。
timeout 模块
timeout 链表管理
timeout 是一个双向链表,节点实现如下,timeout 以 ticks 为单位。
typedef void (*_timeout_func_t)(struct _timeout *t);
struct _timeout {
sys_dnode_t node;
_timeout_func_t fn;
int32_t dticks;
#endif
};
zephyr/kernel/timeout.c,timeout 模块的管理实现如下图:
链表的 timeout_list, 链表内等待 timeout 的节点已等待的 ticks 数头到尾从小到大排序,每次都只取头节点处理。每个节点的定时溢出时间就 = 头节点累加到本身的 ticks:
T1等待的时间是T1->dticks,
T2等待的时间是T2->dticks+T1->dticks
T3等待的时间是T3->dticks+T2->dticks+T1->dticks
这样每次只需要对头节点的 ticks 做处理,就同时于对所有节点的定时周期做处理。T3 未在 timeout_list 时,timeout_list 按:T1.dticks 1 --> T2.dticks 20 --> T4.dticks 29 --> NULL,读取头节点。将一个 36ms 超时的节点插入时(未插入时 dticks = 36),会从头开始遍历链表,刨除比自己小的节点的 dticks(刨除 1 得到 35 > 20,刨除 20 得到 15 < 29,所以插入 T2 后),然后让自己的后续节点刨除该节点要等待的tick数(图中红色部分, 29 - 15 = 14),最后:T1.dticks 1 --> T2.dticks 20 --> T3.dticks 15 --> T4.dticks 14 --> NULL,移除 T3 时只需要将原本 T3 需要等待的 dticks 加到下一个节点 T4 即可:
void z_add_timeout(struct _timeout *to, _timeout_func_t fn,
k_timeout_t timeout)
{
if (K_TIMEOUT_EQ(timeout, K_FOREVER)) {
return;
}
__ASSERT(!sys_dnode_is_linked(&to->node), "");
to->fn = fn;
K_SPINLOCK(&timeout_lock) {
struct _timeout *t;
to->dticks = timeout.ticks + 1 + elapsed();
for (t = first(); t != NULL; t = next(t)) {
if (t->dticks > to->dticks) {
/* 更新插入节点后一个节点的等待 ticks 数 */
t->dticks -= to->dticks;
sys_dlist_insert(&t->node, &to->node);
break;
}
/* 减去前面节点的tick数 */
to->dticks -= t->dticks;
}
/* 第一个节点直接加入链表 */
if (t == NULL) {
sys_dlist_append(&timeout_list, &to->node);
}
/* 非 Tickless 模式无效,Tickless 模式下计算下一次中断溢出时间 */
if (to == first()) {
sys_clock_set_timeout(next_timeout(), false);
}
}
}
timeout 节点更新
每次tick中断发生时,就会调用 sys_clock_announce 对链表头节点的 dtick 数进行更新并检查,如果发现节点超时将移除节点并进行 callback。先以简单的非 Tickless 说明:设 1ms 定时中断周期, 中断一次 = 1 个 ticks,则中断处理函数中:
void sys_clock_isr(void *arg) {
ARG_UNUSED(arg);
sys_clock_announce(1);
}
void sys_clock_announce(int32_t ticks)
{
k_spinlock_key_t key = k_spin_lock(&timeout_lock);
/* announce_remaining 用于记录通知系统到达下一次中断剩余的滴答数,非 Tickless 模式,为 1 */
announce_remaining = ticks;
struct _timeout *t;
/*
* 检查头节点是否超时,超时移除头节点,执行节点回调函数,如果存在相同超时节点
* (t->dticks <= announce_remaining)
* 如果是 T1
*/
for (t = first();
(t != NULL) && (t->dticks <= announce_remaining);
t = first()) {
int dt = t->dticks;
/* 当前系统全局 curr_tick 计数累加,用于其他辅助的 API 本文不用理会 */
curr_tick += dt;
/* 移除超时节点 */
t->dticks = 0;
remove_timeout(t);
/* 执行节点回调函数 */
k_spin_unlock(&timeout_lock, key);
t->fn(t);
key = k_spin_lock(&timeout_lock);
/* announce_remaining 清 0 */
announce_remaining -= dt;
}
/*
* 循环结束 t 依然指向 first(),如果之前有节点超时,announce_remaining = 0,
* 此处不起作用。非则对节点 dticks 更新,非 Tickless 模式,每次 -1。Tickless 模式
* 下减去已走过的 ticks 参数。
*/
if (t != NULL) {
t->dticks -= announce_remaining;
}
/* 当前系统全局 curr_tick 计数累加,用于其他辅助的 API 本文不用理会 */
curr_tick += announce_remaining;
announce_remaining = 0;
/* 非 Tickless 模式,此处会不生效 */
sys_clock_set_timeout(next_timeout(), false);
k_spin_unlock(&timeout_lock, key);
}
Tickless 关键函数分析
关键变量
初始化时 SysTick->LOAD = last_load - 1;SysTick->VAL = 0;
sys_clock_announce
zephyr 中一个关键宏:CONFIG_SYS_CLOCK_TICKS_PER_SEC,由用户定义每秒需要计数多个 ticks。通常让它等于 CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC,定时器时钟频率。如 stm32nucleo-411re 主频率为 100MHz,如果使用 SysTick 定时器,定时器频率与主频一致,CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC = 100000000,即每秒 100,000,000 个 cycle,如果:
#define CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC 100000000
#define CONFIG_SYS_CLOCK_TICKS_PER_SEC 100000000
#define CYC_PER_TICK (CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / CONFIG_SYS_CLOCK_TICKS_PER_SEC)
则每个 tick 就有 1 个硬件 cycle(CYC_PER_TICK = 1),以下分析假设定时 1s 的 timeout,就需要 dticks = 100000000,cycle = 100000000,即 SysTick->VAL 需要计数 100000000 次后触发 timeoout 超时,而 Systick 的计数器为 24 位,计数最大值为 1 << 24 - 1 = 0x00FFFFFF = 16,777,215,Systick 最多每 1 / (100,000,000 / 16,777,215) ≈ 0.168s 中断一次,所以传入 sys_clock_announce(int32_t ticks),的 ticks 参数可能如下变化:
void sys_clock_announce(int32_t ticks)
{
k_spinlock_key_t key = k_spin_lock(&timeout_lock);
// 第1次中断 announce_remaining = 16,777,215
// 第2次中断 announce_remaining = 16,777,215
// 第3次中断 announce_remaining = 16,777,215
// 第4次中断 announce_remaining = 16,777,215
// 第5次中断 announce_remaining = 16,777,215
// 第6次中断 announce_remaining = 16,113,925
announce_remaining = ticks;
struct _timeout *t;
// 第1次中断 first()->dticks = 100,000,000 !<= 16,777,215
// 第2次中断 first()->dticks = 83,222,785 !<= 16,777,215
// 第3次中断 first()->dticks = 66,445,570 !<= 16,777,215
// 第4次中断 first()->dticks = 49,668,355 !<= 16,777,215
// 第5次中断 first()->dticks = 32,891,140 !<= 16,777,215
// 第6次中断 first()->dticks = 16,113,925 <= 16,113,925 进入循环
for (t = first();
(t != NULL) && (t->dticks <= announce_remaining);
t = first()) {
int dt = t->dticks;
/* 当前系统全局 curr_tick 计数累加,用于其他辅助的 API 本文不用理会 */
curr_tick += dt;
/* 移除超时节点 */
t->dticks = 0;
remove_timeout(t);
/* 执行节点回调函数 */
k_spin_unlock(&timeout_lock, key);
t->fn(t);
key = k_spin_lock(&timeout_lock);
/* announce_remaining 清 0 */
announce_remaining -= dt;
}
// 第1次中断 first()->dticks = 100,000,000 - 16,777,215 = 83,222,785
// 第2次中断 first()->dticks = 83,222,785 - 16,777,215 = 66,445,570
// 第3次中断 first()->dticks = 66,445,570 - 16,777,215 = 49,668,355
// 第4次中断 first()->dticks = 49,668,355 - 16,777,215 = 32,891,140
// 第5次中断 first()->dticks = 32,891,140 - 16,777,215 = 16,113,925
// 第6次中断 如果还有节点,first()->dticks - 0
if (t != NULL) {
t->dticks -= announce_remaining;
}
/* 当前系统全局 curr_tick 计数累加,用于其他辅助的 API 本文不用理会 */
curr_tick += announce_remaining;
announce_remaining = 0;
// 第1次中断 next_timeout() = 83,222,785
// 第2次中断 next_timeout() = 66,445,570
// 第3次中断 next_timeout() = 49,668,355
// 第4次中断 next_timeout() = 32,891,140
// 第5次中断 next_timeout() = 16,113,925
// 第6次中断 如果还有节点,next_timeout() = first()->dticks
sys_clock_set_timeout(next_timeout(), false);
k_spin_unlock(&timeout_lock, key);
}
由于定时器最大溢出设置为0x00FFFFFF,且定时器主频较大,所以仍然需要几次中断才拿到 timeout 的执行,为了解决该问题可以选择计数位数更多 timer 或者降低硬件Timer的时钟,以及有一些 soc 提供了低功耗的 timer。
elapsed
elapsed用于获取最近一次设置 LOAD 寄存器到当前执行了的 cycle 数量。SysTick 为递减计数,流逝的 cycle = last_load - 当前读取的 val + 上一次到期本次中断到期的溢出 cycle。
执行elapsed有两种情况:
- 在isr中执行,此时是timer到期,因此必定有COUNTFLAG标记
- 在thread中执行,在执行elapsed前都会锁中断,执行过程中及时timer到期也不会进入isr,timer到期有几种情况:
– 在A前到期:必定有COUNTFLAG标记
– 在A和B之间到期:必定有COUNTFLAG标记,且val1 < val2
– 在B和C之间到期:没有COUNTFLAG标记,但val1 < val2,到期将在下一次执行时判断
static uint32_t elapsed(void)
{
uint32_t val1 = SysTick->VAL; /* A */
uint32_t ctrl = SysTick->CTRL; /* B */
uint32_t val2 = SysTick->VAL; /* C */
// 如果在中断中调用 (ctrl & SysTick_CTRL_COUNTFLAG_Msk) 每次满足,
// 如果是多线程系统在 thread 中执行,在执行elapsed前都会锁中断,需要判断
// (ctrl & SysTick_CTRL_COUNTFLAG_Msk) 是否满足。
// 如果发生到期,需要将上一次的计时 last_load(SysTick->LOAD)加入到overflow_cyc中
if ((ctrl & SysTick_CTRL_COUNTFLAG_Msk)
|| (val1 < val2)) {
overflow_cyc += last_load;
/* We know there was a wrap, but we might not have
* seen it in CTRL, so clear it. */
(void)SysTick->CTRL;
}
// 返回上次到期到现在流逝掉的cycle
return (last_load - val2) + overflow_cyc;
}
// next_timeout 调用的接口, 将 cycle 换算成 ticks 返回
uint32_t sys_clock_elapsed(void)
{
if (!TICKLESS) {
return 0;
}
k_spinlock_key_t key = k_spin_lock(&lock);
uint32_t unannounced = cycle_count - announced_cycles;
uint32_t cyc = elapsed() + unannounced;
k_spin_unlock(&lock, key);
return cyc / CYC_PER_TICK;
}
sys_clock_isr
void sys_clock_isr(void) {
uint32_t dcycles;
uint32_t dticks;
// 更新 overflow_cyc,在elapsed内更新,即每次 中断溢出时 的cycle
elapsed();
// 累加经过的硬件周期数,
cycle_count += overflow_cyc;
// 清除记录
overflow_cyc = 0;
// 用总共流逝的 cycle_count 减去 本次中断之前共用掉的 announced_cycles
// 得到本次中断拿来用的 dcycles
dcycles = cycle_count - announced_cycles;
// CYC_PER_TICK = 1,dticks 与 dcycles 对等
dticks = dcycles / CYC_PER_TICK;
// 累加记录通知内核已经用掉的 cycles 计数,就是 sys_clock_announce 用掉了的 cycles
announced_cycles += dcycles;
// 更新 timeout 节点的 dticks
sys_clock_announce(dticks);
}
sys_clock_set_timeout
void sys_clock_set_timeout(int32_t ticks, bool idle)
{
// SysTick 是无法关闭的,始终都会在计数,这里如果开启了idle,将计数溢出设为最大
// 以保证 “Tickless”,低功耗定时器或其他定时器,在这里将定时器关闭。
if (IS_ENABLED(CONFIG_TICKLESS_KERNEL) && idle && ticks == K_TICKS_FOREVER) {
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
last_load = TIMER_STOPPED;
return;
}
#if defined(CONFIG_TICKLESS_KERNEL)
uint32_t delay;
uint32_t val1, val2;
// 记录之前设置的 SysTick->LOAD
uint32_t last_load_ = last_load;
// #define COUNTER_MAX 0xFFFFFF
// #define MAX_TICKS ((COUNTER_MAX / CYC_PER_TICK) - 1)
// CYC_PER_TICK = 1 时,一次中断最多只能处理 16777215 个 tick
ticks = (ticks == K_TICKS_FOREVER) ? MAX_TICKS : ticks;
ticks = CLAMP(ticks - 1, 0, (int32_t)MAX_TICKS);
k_spinlock_key_t key = k_spin_lock(&lock);
// 从上一次中断到当前走过多少硬件 cycle
uint32_t pending = elapsed();
// 获取当前 SysTick 计数器的值,cycle_count = 0
val1 = SysTick->VAL;
// 从上一次中断到当前走过的硬件 cycle 添加到 cycle_count 中,一直在累加同样会出现溢出循环,
cycle_count += pending;
// 将溢出计数 overflow_cyc 清零
overflow_cyc = 0U;
// 从上一次中断到当前走了多少个cycle,本来就是前面的 pending,这样做是为了检查 cycle_count 溢出循环
uint32_t unannounced = cycle_count - announced_cycles;
if ((int32_t)unannounced < 0) {
// #define MIN_DELAY MAX(1024U, ((uint32_t)CYC_PER_TICK/16U))
// 如果发生了溢出,设置定时器定时一小段时间,让 announced_cycles 也溢出循环,方便计算
// 将 last_load 设置为 MIN_DELAY,确保至少有一个最小的延迟时间
last_load = MIN_DELAY;
} else {
// 如果 CYC_PER_TICK = 1,ticks = delay = 0xFFFFFF,
// 正好是硬件计数器的最大计数值,否则需要将 ticks 换算成硬件 cycle,
delay = ticks * CYC_PER_TICK;
// 因为每次中断都是按tick处理,而 unannounced 可能不是 tick 对齐的,所以这里加入unannounced 进行对齐就算
// 将尚未通知的周期数加到延迟时间中
delay += unannounced;
// 将延迟时间向上舍入到下一个时钟滴答周期的整数倍
delay = DIV_ROUND_UP(delay, CYC_PER_TICK) * CYC_PER_TICK;
// 从延迟时间中减去尚未通知的周期数
delay -= unannounced;
// 将延迟时间限制在 MIN_DELAY 和 MAX_CYCLES 之间。
delay = MAX(delay, MIN_DELAY);
// #define MAX_CYCLES (MAX_TICKS * CYC_PER_TICK)
// CYC_PER_TICK = 1,MAX_CYCLES 就是 MAX_TICKS = 0xFFFFFE
if (delay > MAX_CYCLES) {
last_load = MAX_CYCLES;
} else {
last_load = delay;
}
}
// 记录当前 SysTick->VAL
val2 = SysTick->VAL;
// 设置下一次溢出时间
SysTick->LOAD = last_load - 1;
SysTick->VAL = 0;
// 将 val1 到 val2 之间计算下一次溢出时间时所流逝的时间加到全局 cycle_count 中
// SysTick 是递减的计数器,通常情况下 val1 >= val2,如果 val1 = 0x0,
// 到 val2 就回环了,等于从 0xffffff 开始的一个值。
if (val1 < val2) {
cycle_count += (val1 + (last_load_ - val2));
} else {
cycle_count += (val1 - val2);
}
k_spin_unlock(&lock, key);
#endif
}
思考
使用普通定时器或者低功耗定时器,配置为向上计数,吧定时器频率设定的比较低,关键需要修改 elapsed()、sys_clock_isr()、sys_clock_set_timeout(), 这三个函数。这是一个无 os 的移植测试框架,欢迎指导。