rt-thread优化系列(三)软定时器的定时漂移问题分析

描述

软定时器

所谓软定时器,是由一个线程运行维护的定时器列表。由线程调用定时器回调函数。
相对硬定时器,是由中断(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 优化系列(二) 之 同步和消息关中断分析

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分