Zephyr - timeout 模块和 Tickless 代码分析

前言

        非 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有两种情况:

  1. 在isr中执行,此时是timer到期,因此必定有COUNTFLAG标记
  2. 在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 的移植测试框架,欢迎指导。

Hal coder 大佬博客

zephyr documentation

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

__蚩尤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值