软定时器
所谓软定时器,是由一个线程运行维护的定时器列表。由线程调用定时器回调函数。
相对硬定时器,是由中断(SysTick)维护的定时器列表,并在中断中调用定时器回调函数。
另外,还有一种*硬件定时器*,这个和单片机里的定时器是一个概念,由外设定时器实现定时。和 rt-thread 提供的硬定时器是两个不同概念。
对硬定时器回调函数有严格的执行时间要求,而且不能调用任何在中断中不能调用的函数。总之不能有任何不能在中断中执行的操作。
那么,软定时器呢,要求需要这么严格吗?
比如有个处理执行时间 10ms,比如在定时器中断函数里发送个等待 10ms 的消息...
无论是硬定时器还是软定时器,它们各自有一个定时器列表。列表中的定时器根据定时时间长短排序,定时时间短的在前。扫描这个列表中所有定时器,直到结尾或者出现第一个定时时间未到的定时器节点。当判断出定时时间到的时候,调用定时器回调函数。
假如,某次扫描这个列表中多于一个定时器到达定时时间,也就是需要执行两个以上的定时器回调函数。并且前一个扫描到的定时器的回调函数执行时间比较长,出现上面设想的某一种使用情况。这时候会出现什么效果?
因为硬定时器的种种硬性要求,以下讨论只针对软定时器。
从对 `rt_soft_timer_check` 的几个疑问讲起
先摆出官方的 rt_soft_timer_check 函数,实现。这个函数是来扫描定时器列表中到达定时时间的定时器,并调用定时器回调函数的。
```
void rt_soft_timer_check(void)
{
rt_tick_t current_tick;
struct rt_timer *t;
register rt_base_t level;
rt_list_t list;
rt_list_init(&list);
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check enter\n"));
/* disable interrupt */
level = rt_hw_interrupt_disable();
while (!rt_list_isempty(&rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
{
t = rt_list_entry(rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);
current_tick = rt_tick_get();
/*
* It supposes that the new tick shall less than the half duration of
* tick max.
*/
if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
{
RT_OBJECT_HOOK_CALL(rt_timer_enter_hook, (t));
/* remove timer from timer list firstly */
_rt_timer_remove(t);
if (!(t->parent.flag & RT_TIMER_FLAG_PERIODIC))
{
t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
}
/* add timer to temporary list */
rt_list_insert_after(&list, &(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
soft_timer_status = RT_SOFT_TIMER_BUSY;
/* enable interrupt */
rt_hw_interrupt_enable(level);
/* call timeout function */
t->timeout_func(t->parameter);
RT_OBJECT_HOOK_CALL(rt_timer_exit_hook, (t));
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));
/* disable interrupt */
level = rt_hw_interrupt_disable();
soft_timer_status = RT_SOFT_TIMER_IDLE;
/* Check whether the timer object is detached or started again */
if (rt_list_isempty(&list))
{
continue;
}
rt_list_remove(&(t->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&
(t->parent.flag & RT_TIMER_FLAG_ACTIVATED))
{
/* start it */
t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
rt_timer_start(t);
}
}
else break; /* not check anymore */
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("software timer check leave\n"));
}
疑问一 `rt_list_t list` 这个临时中间变量的作用是什么?
- 进入 `rt_soft_timer_check` 函数后,先初始化 list 变量。
- 扫描定时器列表,当有到时的定时器,把这个定时器从软定时器列表 `rt_soft_timer_list` 移除,插入到这个 list 临时存放。
- 开中断,调用定时器回调函数,关中断
- 把 list 中存放的定时器移除
- 判断定时器的周期定时器还是一次性定时器
- 继续扫描定时器列表
list 是个局部变量,仅仅起临时存放当前这个定时器的作用。看似是一种稳妥的做法。
但是,这样做的初衷是什么?
为什么这个定时器一定要放到某个列表里?
如果从软定时器列表 `rt_soft_timer_list` 移除后,不插入任何列表会有什么影响?
因为退出 `rt_soft_timer_check` 函数后,list 列表不复存在了,应该不是退出 `rt_soft_timer_check` 函数后的需求,那么插入 list 和 从 list 取出之间有哪些情况需要我们注意,需要用一个临时列表将软定时器暂存?
定时器回调函数里可能发生哪些操作?
- 修改定时器设置
- 停止,重启定时器
- 删除定时器
- 发生中断,在中断里执行上述三种操作
修改定时器设置,可能只涉及到定时器的定时时间间隔和定时周期特性。这两个参数设置需要定时器必须在某个列表中吗?
停止,重启定时器,必然导致修改定时器所在列表指针,这里就涉及到双向列表的操作了。
> 简短介绍一下双向列表,
- rt-thread 使用的双向列表,每一个节点有一个 prev 和 一个 next 指针,分别指向双向列表中的前一个节点和后一个节点。
- 一个空列表 l 仅有一个不含数据的节点,此节点的 prev next 指针均指向它自己。
- 任何一个带数据的列表节点必须进行初始化,使得它的 prev next 分别指向它自己,这一点和空列表 l 完全雷同!换句话说,***任何一个双向列表节点均有作为链表的潜质***。从操作上讲,你可以定义两个定时器,然后这两个定时器之间构建一个含有两个节点的双向列表,当然,这种做法没有多少实用意义。
- 从链表中移除的节点,**必须**使得它的 prev next 指针指向它自己。
- **无论一个节点是否在某个双向链表中,或者仅仅是一个独立节点,对它进行删除操作,效果是完全一样的!**
- 更多的操作详见 rtservice.c 文件中相关函数,`rt_list_init, rt_list_insert_after, rt_list_insert_before, rt_list_remove` 等等。
停止定时器会把当前定时器从定时器列表删除,无论这个定时器有没有在某个定时器列表中,或者只是一个独立的定时器节点,删除操作的结果都是一样的,使用 list 这个临时列表可能不能保护它不被删除。
重启定时器会把它先从前一个列表中删除,然后插入软定时器列表 `rt_soft_timer_list` 。list 这个临时列表也阻止不了重启定时器操作。
**至此,可以看出,`rt_list_t list` 这个临时列表无任何存在意义**
疑问二 `soft_timer_status` 指示的是什么状态?
这是一个全局静态变量,它的使用也很简单,只在四个地方使用了,上面的源码函数里有两处,其它两个地方分别是初始化声明
/* soft timer status */
static rt_uint8_t soft_timer_status = RT_SOFT_TIMER_IDLE;
和——以下摘自 `rt_timer_start` 函数
#ifdef RT_USING_TIMER_SOFT
if (timer->parent.flag & RT_TIMER_FLAG_SOFT_TIMER)
{
/* check whether timer thread is ready */
if ((soft_timer_status == RT_SOFT_TIMER_IDLE) &&
((timer_thread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND))
{
/* resume timer thread to check soft timer */
rt_thread_resume(&timer_thread);
rt_schedule();
}
}
#endif /* RT_USING_TIMER_SOFT */
将这四个地方联系起来看,意思好像是调用定时器回调函数前修改软定时器为 busy 状态,返回回调函数后恢复为 idle 状态,而如果是在定时器回调函数里调用 `rt_timer_start` ,可以达到不进行任务调度的目的。好像是起了双保险作用,真是这样吗?
我们分析一下上面这段 `rt_timer_start` 函数片段。
首先判断定时器是不是软定时器,只有软定时器启动时才有进行任务调度的可能。
其次,判断 `soft_timer_status` 是否空闲,以及软定时器线程是否***挂起态***。
以上仨条件均满足,进行任务调度。
我们重点关注“其次”,一个定时器线程调用的定时器回调函数,这个线程会是挂起态吗?答案是肯定不是。它在运行着,肯定是 `RT_THREAD_RUNNING` 的。那么这个 `soft_timer_status` “双保险”了吗?
疑问三 开篇提到的假想
开篇提到了一种假想,假想软定时器回调函数占用 cpu 时间有点儿长,会产生什么影响,引起什么后果。
讨论这个问题仍然离不开 `rt_soft_timer_check` 函数工作原理,我们再梳理一下 `rt_soft_timer_check` 函数的操作。(以下分析忽略 list 以及 soft_timer_status 相关操作)
- 关中断
- 扫描列表是否有节点
- 取出第一个节点
- ***获取当前系统 tick***
- 检查定时器是否定时时间到,如果到时
- 先从软定时器列表 `rt_soft_timer_list` 移除定时器。非周期定时器,取消激活态
- 开中断
- 执行定时器回调函数。(这里可能存在长时间操作)
- 关中断
- 对于周期性定时器,重启定时器;非周期定时器,前面已经做了取消激活态操作。
- 继续扫描列表,取出第一个节点,***获取当前系统 tick*** ,检查定时器是否定时时间到。。。
假设 RT_TICK_PER_SECOND = 1000,有两个周期性定时器 t0 t1 ,定时间隔不同,同时启动,各自的定时器回调函数执行时间 t0 500us,t1 5 ms。
经过一段时间以后,总是可能会出现定时间隔公倍数时刻 Tn ,它们俩同时达到定时时间。
如果 t1 先被处理,那么 t1 重启的时候系统 tick 已经是 Tn + 5;t0 的重启时间有 50% 的可能是 Tn + 5, 50%的可能是 Tn + 6。
如果 t0 先被处理,t0 的重启时间有 50% 的可能是 Tn, 50%的可能是 Tn + 1;t1 重启时间是 Tn + 5 或 Tn + 6。
即便不考虑 t0 不考虑它对外的影响,也不考虑它受到的影响,仅仅分析 t1 自己对自己的影响,也可以看出来,随着时间的推移,它的定时间隔不是初始设定的 Inv ,而是 Inv + 5。
优化后的 `rt_soft_timer_check` 流程
- ***获取当前系统 tick***
- 关中断
- 扫描列表是否有节点
- 取出第一个节点
- 检查定时器是否定时时间到,如果到时
- 先从软定时器列表 `rt_soft_timer_list` 移除定时器。
- 对于周期性定时器,***重启定时器***;非周期定时器,取消激活态。
- 开中断
- 执行定时器回调函数。(这里可能存在长时间操作)
- 关中断
- 继续扫描列表,取出第一个节点,检查定时器是否定时时间到。。。
其中,优化后的重启定时器不能使用原来的接口。需要使用如下原型函数接口
static rt_err_t _rt_timer_start(rt_timer_t timer, rt_tick_t current_tick)
第二个参数是进入 `rt_soft_timer_check` 函数,第一次关中断前获取的当前系统 `tick` 值,无论下面扫描出多少个到达时间的定时器,启动时间都是同一个 `tick` 值。
而且无论其中某个定时器回调函数执行时间有多长,或者多个回调函数累积执行时间有多长,它们的启动时间都是相同的!
注:由此,引起的另一个弊端的,期间某个定时器定时时间到了,但是会被判定为未到,下次调用 `rt_soft_timer_check` 时才会被处理。
附测试程序
static void led1_timeout(void *parameter)
{
rt_tick_t current_tick;
int pin = rt_pin_read(LED1_PIN);
rt_pin_write(LED1_PIN, !pin);
current_tick = rt_tick_get();
rt_hw_us_delay(50000);
}
void led_tick_thread(void *parameter)
{
rt_timer_t led1_timer;
led1_timer = rt_timer_create("ledtim1", led1_timeout,
RT_NULL, 1000,
RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
if (led1_timer != RT_NULL) {
rt_timer_start(led1_timer);
}
while (1) {
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
}
作为对比,两个 led 一个用软定时器控制亮灭频率,一个用 mdelay 延时控制亮灭频率。
如果 timeout 没有延迟,两个灯一直是同步的;有延迟后,过一段时间两个灯亮灭变不同步了。
总结
肯定有很多人反对说,定时器回调函数不要有长时间操作。发消息,信号,邮箱...交给其它线程操作云云。
软定时器本身就是一个线程,通过某种技术手段,在这个线程中可以完成的工作,一定要使用消息机制交给另外一下线程完成吗?
如何抉择,请君深思
本优化系列所有提到的更改已经提交到 gitee ,欢迎大家测试
https://gitee.com/thewon/rt_thread_repo
相关文章:
rt-thread 优化系列(0) SysTick 优化分析
rt-thread 优化系列(一) 之 过多关中断
rt-thread 优化系列(二) 之 同步和消息关中断分析
审核编辑:汤梓红
-
定时器
+关注
关注
23文章
3246浏览量
114739 -
函数
+关注
关注
3文章
4329浏览量
62575 -
RT-Thread
+关注
关注
31文章
1285浏览量
40093
发布评论请先 登录
相关推荐
评论