合理使用定时器

定时器执行上下文

RT-Thread的定时器与其他实时操作系统的定时器实现稍微有些不同(特别是RT-Thread早期版本的实现中),因为RT-Thread里定时器默认的方式是HARD_TIMER定时器,即定时器超时后,超时函数是在系统时钟中断的上下文环境中运行的。在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间。

另外,在第二章第三节线程控制块中,你是否有留意到每个线程控制块中都包含了一个定时器:thread_timer。这个thread_timer也是一个HARD_TIMER定时器。它被用于当线程需要执行一些带时间特性的系统调用中,例如带超时特性的试图持有信号量,接收事件、接收消息等,而当相应的条件不能够被满足时线程就将被挂起,在线程挂起前,这个内置的定时器将会被激活并启动(超时函数设定为rt_thread_timeout)。当线程定时器超时时,这个线程依然还未被唤醒, rt_thread_timeout函数仍将继续被调用,接着设置线程的error代码为-RT_ETIMEOUT,接着唤醒这个线程。所以从某个意义上说,在线程中执行rt_thread_sleep/rt_thread_delay函数,也可以算是另一种意义的超时。

回到上一段对HARD_TIMER定时器描述中来,可以看到HARD_TIMER定时器超时函数工作于中断的上下文环境中,这种在中断中执行的方式显得非常麻烦,因此开发人员需要时刻关心超时函数究竟执行了哪些操作;相反如果定时器超时函数是在线程中执行,显然会好很多,如果有更高优先级的线程就绪,依然可以抢占这个定时器执行线程从而获得优先处理权。如果是想要使用rt_thread_sleep/rt_thread_delay的方式实现定时器超时操作,那么可以使用如下图的方式:

线程定时器

在上面的例子中,timer_thread是一个线程入口函数,在线程中执行rt_thread_delay(n)后,可以实现n个OS tick的定时,当执行rt_thread_delay时,线程的内置定时器将会被激活并启动;当线程定时器超时时,这个线程将被唤醒,并接着rt_thread_delay运行后续的代码。

上述描述的都是HARD_TIMER的特性。另外,在RT-Thread中,我们也可以在创建定时器时,把定时器指定成SOFT_TIMER的方式,这样可以使得定时器超时函数完全运行在timer系统线程上下文环境中。如果系统在初始化时需要使用SOFT_TIMER特性,需要在系统配置中打开RT_USING_TIMER_SOFT宏定义,那么调用rt_system_timer_thread_init函数就可以启动timer系统线程。这里值得注意的是,SOFT_TIMER定时器的精度由RT_TIMER_TICK_PER_SECOND定义的值所决定(每秒触发的timer tick次数是多少),这个值必须是OS tick的整数倍。

OS tick与定时器精度

系统中HARD_TIMER定时器的最小精度是由系统时钟节拍所决定的(1 OS tick = 1/RT_TICK _PER_SECOND秒,RT_TICK_PER_SECOND值在rtconfig.h文件中定义),定时器设定的时间必须是OS tick的整数倍。当需要实现更短时间长度的系统定时时,例如OS tick是10ms,而程序需要实现1ms的定时或延时,这种时候操作系统定时器将不能够满足要求,只能通过读取系统某个硬件定时器的计数器或直接使用硬件定时器的方式。

在Cortex-M3中,SysTick已经被RT-Thread用于作为OS tick使用,它被配置成1/RTTICK PER_SECOND秒后触发一次中断的方式,中断处理函数使用Cortex-M3默认的SysTick_Handler名字。在Cortex-M3的CMSIS(Cortex Microcontroller Software Interface Standard)规范中规定了SystemCoreClock代表芯片的主频,所以基于SysTick以及SystemCoreClock,我们能够使用SysTick获得一个精确的延时函数,如下例所示,Cortex-M3上的基于SysTick的精确延时(需要系统在使能SysTick后使用):

高精度延时 的例程如下所示

  1. #include <board.h>
  2. void rt_hw_us_delay(rt_uint32_t us)
  3. {
  4. rt_uint32_t delta;
  5.  
  6. /* 获得延时经过的tick数 */
  7. us = us * (SysTick->LOAD/(1000000/RT_TICK_PER_SECOND));
  8.  
  9. /* 获得当前时间 */
  10. delta = SysTick->VAL;
  11.  
  12. /* 循环获得当前时间,直到达到指定的时间后退出循环 */
  13. while (delta - SysTick->VAL< us);
  14. }

其中入口参数us指示出需要延时的微秒数目,这个函数只能支持低于1 OS tick的延时,否则SysTick会出现溢出而不能够获得指定的延时时间。