前言
在 RTOS 中,时间相关的功能非常常见,比如任务延时、超时等待、周期执行等。
如果所有地方都用忙等延时,不仅浪费 CPU,还会破坏多任务系统的意义。因此 RTOS 一般会基于系统 tick 实现一套软件定时器机制,让任务可以在等待期间进入阻塞状态,把 CPU 让给其它任务。
这篇文章记录一下 Yuan RTOS 中软件定时器的实现。
完整代码请访问我的Github仓库,网址为:yuan-rtos。
值得注意是本文的示例代码在你阅读时可能已经进行过修改了,具体实现以仓库中代码为主。
系统 tick
软件定时器首先需要一个稳定的时间基准。在 Yuan RTOS 中,这个时间基准就是系统 tick:
volatile yr_uint32_t yr_current_ticks = 0;
每次硬件定时中断到来时,需要调用:
void yr_tick_update(void)
对于 Cortex-M ,一般在 SysTick_Handler() 中调用它。
yr_tick_update() 做的事情主要有三件:
- 递增全局 tick 计数
- 处理当前任务的时间片
- 检查是否有软件定时器超时
相关逻辑如下:
++yr_current_ticks;
--current_task->remaining_ticks;
if( current_task->remaining_ticks == 0 ) {
current_task->remaining_ticks = current_task->init_ticks;
yr_sched_yield();
}
yr_timer_check();
也就是说,任务时间片和软件定时器都依赖同一个系统 tick。
定时器对象
Yuan RTOS 中的软件定时器结构体如下:
typedef struct yr_timer_t {
yr_list_t list_node;
yr_timer_func_t timeout_func;
void *param;
yr_uint32_t init_ticks;
yr_uint32_t timeout_ticks;
} yr_timer_t;
其中:
list_node:用于挂入定时器链表timeout_func:超时回调函数param:传给超时回调的参数init_ticks:定时器启动后等待多少 ticktimeout_ticks:实际超时的系统 tick 值
定时器初始化时只会设置这些基础信息,并不会自动启动:
yr_err_t yr_timer_init( yr_timer_t *timer,
yr_timer_func_t func,
void *param,
yr_uint32_t ticks)
{
yr_list_init( &timer->list_node);
timer->timeout_func = func;
timer->param = param;
timer->init_ticks = ticks;
timer->timeout_ticks = 0;
return YR_OK;
}
有序链表
Yuan RTOS 使用一个全局链表管理所有已经启动的软件定时器:
yr_list_head_t yr_timer_list;
这个链表并不是随便插入的,而是按照超时时间从小到大排序。启动定时器时,会先计算它的绝对超时时刻:
timer->timeout_ticks = yr_current_ticks + timer->init_ticks;
然后遍历定时器链表,找到第一个超时时间比自己更晚的节点,把当前定时器插到它前面:
for( timer_node = yr_timer_list.next;
timer_node != &yr_timer_list;
timer_node = timer_node->next ) {
yr_timer_t *ptimer = YR_LIST_ENTRY( timer_node,
yr_timer_t,
list_node);
if( (yr_int32_t)( ptimer->timeout_ticks -
timer->timeout_ticks ) > 0 )
break;
}
yr_list_insert_before( timer_node, &timer->list_node);
这样做的好处是检查超时时,只需要从链表头开始检查。只要遇到第一个没超时的定时器,就可以直接停止遍历,因为后面的定时器一定更晚超时。
超时检查
定时器检查函数的核心逻辑如下:
while(!yr_list_isempty(&yr_timer_list)) {
timer_node = yr_timer_list.next;
ptimer = YR_LIST_ENTRY( timer_node, yr_timer_t, list_node);
if( (yr_int32_t)( yr_current_ticks -
ptimer->timeout_ticks ) >= 0 ) {
yr_list_delete_self( timer_node);
yr_list_insert_before( &timeout_list, timer_node);
} else {
break;
}
}
这里有两个值得注意的地方。
第一,判断超时使用的是强制转换后的有符号减法:
(yr_int32_t)(yr_current_ticks - ptimer->timeout_ticks) >= 0
这种写法可以在一定范围内处理 tick 溢出的情况,比直接比较两个无符号数更稳妥。
第二,超时的定时器不是立刻执行回调,而是先移动到一个临时链表 timeout_list 中,之后再统一执行回调:
while (!yr_list_isempty(&timeout_list)) {
timer_node = timeout_list.next;
ptimer = YR_LIST_ENTRY(timer_node, yr_timer_t, list_node);
yr_list_delete_self(timer_node);
if (ptimer->timeout_func != NULL)
ptimer->timeout_func(ptimer->param);
}
这样做是为了避免回调函数中再次操作定时器链表时破坏当前遍历过程。例如回调中可能会重新启动当前定时器,用于实现周期软件定时器。
任务延时
软件定时器在 Yuan RTOS 中最直接的用途就是任务延时。每个任务控制块中都有一个定时器:
yr_timer_t timer;
当任务调用 yr_task_sleep_ticks() 时,当前任务会被移出就绪队列,并启动自己的定时器:
yr_sched_remove_task( current_task);
current_task->status = YR_TASK_STATUS_BLOCKED;
yr_timer_stop( ¤t_task->timer);
yr_timer_set_ticks( ¤t_task->timer, ticks);
yr_timer_start( ¤t_task->timer);
yr_sched_switch();
此时任务不再参与调度,直到定时器超时。
任务定时器使用的默认回调是:
void yr_timeout_default_func(void *param)
这个函数会把阻塞任务重新放回就绪队列:
task->status = YR_TASK_STATUS_READY;
yr_task_set_msg( task,
NULL,
NULL,
YR_TASK_MR_NONE,
YR_TASK_MN_WAIT_TIMEOUT);
yr_sched_insert_task(task);
如果被唤醒任务的优先级高于当前任务,还会触发一次任务切换。
周期延时
除了相对延时,Yuan RTOS 还实现了 yr_task_sleep_until(),用于周期任务:
yr_err_t yr_task_sleep_until(yr_uint32_t *pre_ticks,
yr_uint32_t inc_ticks)
{
*pre_ticks += inc_ticks;
current_ticks = yr_get_current_ticks();
if ((yr_int32_t)(*pre_ticks - current_ticks) <= 0)
return YR_OK;
sleep_ticks = *pre_ticks - current_ticks;
yr_task_sleep_ticks(sleep_ticks);
return YR_OK;
}
普通的相对延时大概是“从现在开始延时多少 tick”。而周期延时是“下一次应该在某个绝对 tick 醒来”。
这样可以减少任务执行时间带来的累计误差,更适合周期采样、周期打印、周期控制等场景。
IPC 超时
软件定时器还有一个重要用途:IPC 等待超时。
例如任务获取信号量失败时,如果 wait_ticks 不是 0,任务会进入 IPC 阻塞队列。如果 wait_ticks 也不是 YR_WAIT_FOREVER,则会启动当前任务的定时器:
yr_ipc_block_task( &sem->ipc_base, current_task);
if( wait_ticks != YR_WAIT_FOREVER ) {
yr_timer_set_ticks( ¤t_task->timer, wait_ticks);
yr_timer_start( ¤t_task->timer);
}
如果在超时前资源可用,则 IPC 模块会停止这个定时器,并把任务唤醒。
如果先发生超时,则 yr_timeout_default_func() 会把任务从 IPC 的阻塞链表中删除,并设置通知状态为 YR_TASK_MN_WAIT_TIMEOUT。任务恢复运行后,就可以根据这个状态判断等待是否成功。
总结
Yuan RTOS 的软件定时器实现可以概括为:
- 使用系统 tick 作为时间基准
- 所有已启动定时器挂入一个按超时时间排序的链表
- 每次 tick 后检查链表头部已经超时的定时器
- 使用临时链表执行超时回调,避免遍历过程中被回调破坏链表
- 任务延时和 IPC 超时都复用了同一套定时器机制
这种实现的优点是结构简单,代码量较少,并且便于理解。缺点是启动定时器时需要遍历链表,定时器数量很多时效率会下降。不过对于轻量化 RTOS 和小型 MCU 项目来说,这个实现已经比较够用了。