时间是非常重要的概念,和朋友出去游玩需要约定时间,完成任务也需要花费时间,生活离不开时间。
操作系统也一样,需要通过时间来规范其任务的执行,操作系统中最小时间单位是时钟节拍(OS Tick)。
任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超市等。
时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是1ms~100ms,时钟节拍率越快,系统的实时响应越快,但是系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。
RT-Thread中,时钟节拍的长度可以根据RT_TICK_PER_SECOND的定义来调整,等于1/RT_TICK_PER_SECOND秒。
时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次:void rt_tick_increase(void),通知操作系统已经过去一个系统时钟;不同硬件定时器中断实现都不同。
void SysTick_Handler(void)
{
/* 进入中断 */
rt_interrupt_enter();
……
rt_tick_increase();
/* 退出中断 */
rt_interrupt_leave();
}
void rt_tick_increase(void)
{
struct rt_thread *thread;
/* 全局变量rt_tick自加 */
++rt_tick;
/* 检查时间片 */
thread = rt_thread_self();
--thread->remaining_tick;
if(thread->remaining_tick == 0)
{
thread->remaining_tick = thread->init_tick;
rt_thread_yield();
}
rt_timer_check();
}
可以看到全局变量rt_tick在每经过一个时钟节拍时,值就会加1,rt_tick的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。
此外,每经过一个时钟节拍时,都会检查当前线程的时间片是否用完,以及是否有定时器超时。
rt_timer_check()用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。
由于全局变量rt_tick在每经过一个时钟节拍时,值就会加1,通过调用rt_tick_get会返回当前rt_tick的值,即可以获取当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的时间。
rt_tick_t rt_tick_get(void);
定时器,是指从指定的时刻开始,经过一定的指定时间后触发一个事件,例如定个时间提醒第二天能够按时起床。
定时器有硬件定时器和软件定时器之分:
RT-Thread操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是OS Tick的整数倍,例如一个OS Tick是10ms,那么上层软件定时器只能是10ms,20ms,100ms等,而不能定时为15ms。
RT-Thread的定时器也基于系统的节拍,提供了基于节拍整数倍的定时能力。
RT-Thread的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。
另外,根据超时函数执行时所处的上下文环境,RT-Thread的定时器可以分为HARD_TIMER模式与SOFT_TIMER模式。
HARD_TIMER模式的定时器,超时函数在中断上下文环境中执行,可以在初始化/创建定时器时使用参数RT_TIMER_FLAG_HARD_TIMER来指定。
在中断上下文环境中执行时,对于超时函数的要求与中断服务例程的要求相同:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等。
RT-Thread定时器默认的方式是HARD_TIMER模式,即定时器超时后,超时函数是在系统时钟中断的上下文环境中运行的。在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其它中断的响应时间加长或抢占了其他线程执行的时间。
SOFT_TIMER模式可配置,通过宏定义RT_USING_TIMER_SOFT来决定是否启用该模式。
该模式被启用后,系统会在初始化时创建一个timer线程,然后SOFT_TIMER模式的定时器超时函数在timer线程的上下文环境中执行。
在RT-Thread定时器模块中维护着两个重要的全局变量:
如下图所示:系统当前tick值为20,在当前系统中已经创建并启动了三个定时器,分别是定时时间为 50 个 tick 的 Timer1、100 个 tick 的 Timer2 和 500 个 tick 的 Timer3,这三个定时器分别加上系统当前时间rt_tick=20,从小到大排序链接在rt_timer_list链表中。
而rt_tick随着硬件定时器的触发一直在增长(每一次硬件定时器中断来临,rt_tick变量会加1),50个tick以后,rt_tick从20增长到70,与Timer1的timeout值相等,这时会触发与Timer1定时器相关联的超时函数,同时将Timer1从rt_timer_list链表上删除。
同理,100 个 tick 和 500 个 tick 过去后,与 Timer2 和 Timer3 定时器相关联的超时函数会被触发,接着将 Timer2 和 Timer3 定时器从 rt_timer_list 链表中删除。
如果系统当前定时器状态在 10 个 tick 以后(rt_tick=30)有一个任务新创建了一个 tick 值为 300 的 Timer4 定时器,由于 Timer4 定时器的 timeout=rt_tick+300=330, 因此它将被插入到 Timer2 和 Timer3 定时器中间,形成如下图所示链表结构:
在RT-Thread操作系统中,定时器控制块由结构体struct rt_timer定义并形成定时器内核对象,再链接到内核对象容器中进行管理。
它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息,例如初始节拍数,超时的节拍数,也包含定时器与定时器之间连接用的链表结构,超时回调函数等。
struct rt_timer
{
struct rt_object parent;
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
void (*timeout_func)(void *parameter);
void *parameter;
rt_tick_t init_tick;
rt_tick_t timeout_tick;
}
typedef struct rt_timer *rt_timer_t;
定时器控制块由struct rt_timer结构体定义并形成定时器内核对象,再链接到内核对象容器中进行管理,list成员则用于把一个激活的定时器链接到rt_timer_list链表中。
系统新创建并激活的定时器都会按照以超时时间排序的方式插入到rt_timer_list链表中,也就是说这个链表是一个有序链表,RT-Thread中使用了跳表算法来加快搜索链表元素的速度。
跳表是一种基于并联链表的数据结构,实现简单,插入,删除、查找的时间复杂度均为O(log n)。跳表是链表的一种,但它在链表的基础上增加了**“跳跃”功能**,正是这个功能,使得在查找元素时,跳表能够提供O(log n)的时间复杂度,举例如下:
一个有序的链表,如下图所示,从该有序链表中搜索元素 {13, 39},需要比较的次数分别为 {3, 5},总共比较的次数为 3 + 5 = 8 次。
使用跳表算法后可以采用类似二叉搜索树的方法,把一些节点提取出来作为索引,得到如下图所示的结构:
在这个结构里把{3,18,77}提取出来作为一级索引,这样搜索的时候就可以减少比较次数了,例如在搜索 39 时仅比较了 3 次(通过比较 3,18,39)。当然我们还可以再从一级索引提取一些元素出来,作为二级索引,这样更能加快元素搜索。
所以,定时器跳表可以通过上层的索引,在搜索的时候就减少比较次数,提升查找的效率,这是一种通过“空间来换取时间”的算法,在RT-Thread中通过宏定义RT_TIMER_SKIP_LIST_LEVEL来配置跳表的层数,默认为1,表示采用一级有序链表图的有序链表算法,每增加一,表示在原链表基础上增加一级索引。
在系统启动时需要初始化定时器管理系统。可以通过下面的函数接口完成:
void rt_system_timer_init(void);
如果需要使用SOFT_TIMER,则系统初始化时,应该调用下面这个函数接口:
void rt_system_timer_thread_init(void);
定时器控制块中含有定时器相关的重要参数,在定时器各种状态间起到纽带的作用。定时器的相关操作如下图所示,对定时器的操作包含:创建 / 初始化定时器、启动定时器、运行定时器、删除 / 脱离定时器,所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表,这与定时器参数设置相关。在每次的操作系统时钟中断发生时,都会对已经超时的定时器状态参数做改变。
#define RT_TIMER_FLAG_ONE_SHOT 0x0 /* 单次定时 */
#define RT_TIMER_FLAG_PERIODIC 0x2 /* 周期定时 */
#define RT_TIMER_FLAG_HARD_TIMER 0x0 /* 硬件定时器 */
#define RT_TIMER_FLAG_SOFT_TIMER 0x4 /* 软件定时器 */
当为HARD_TIMER时,如果定时器超时,定时器的回调函数将在时钟中断的服务例程上下文中被调用;当指定为SOFT_TIMER时,如果定时器超时,定时器的回调函数将在系统时钟timer线程的上下文中被调用。
当定时器被创建或者初始化以后,并不会被立即启动,必须在调用启动定时器函数接口后,才开始工作,启动定时器函数接口如下:
rt_err_t rt_timer_start(rt_timer_t timer);
调用定时器启动函数接口后,定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED),并按照超时顺序插入到 rt_timer_list 队列链表中。
启动定时器以后,若想使它停止,可以使用下面的函数接口:
rt_err_t rt_timer_stop(rt_timer_t timer);
调用定时器停止函数接口后,定时器状态将更改为停止状态,并从 rt_timer_list 链表中脱离出来不参与定时器超时检查。当一个(周期性)定时器超时时,也可以调用这个函数接口停止这个(周期性)定时器本身。
RT-Thread 定时器的最小精度是由系统时钟节拍所决定的(1 OS Tick = 1/RT_TICK_PER_SECOND 秒,RT_TICK_PER_SECOND 值在 rtconfig.h 文件中定义),定时器设定的时间必须是 OS Tick 的整数倍。当需要实现更短时间长度的系统定时时,例如 OS Tick 是 10ms,而程序需要实现 1ms 的定时或延时,这种时候操作系统定时器将不能够满足要求,只能通过读取系统某个硬件定时器的计数器或直接使用硬件定时器的方式。
在 Cortex-M 系列中,SysTick 已经被 RT-Thread 用于作为 OS Tick 使用,它被配置成 1/RT_TICK_PER_SECOND 秒后触发一次中断的方式,中断处理函数使用 Cortex-M3 默认的 SysTick_Handler 名字。
在Cortex-M3的CMSIS规范中规定了SystemCoreClock代表芯片的主频,所以基于SysTick以及SystemCoreClock,我们能够使用SysTick获得一个精确的延时函数。
#include
void rt_hw_us_delay(rt_uint32_t us)
{
rt_uint32_t ticks;
rt_uint32_t told, tnow, tcnt = 0;
rt_uint32_t reload = SysTick->LOAD;
/* 获得延时经过的 tick 数 */
ticks = us * reload / (1000000 / RT_TICK_PER_SECOND);
/* 获得当前时间 */
told = SysTick->VAL;
while (1)
{
/* 循环获得当前时间,直到达到指定的时间后退出循环 */
tnow = SysTick->VAL;
if (tnow != told)
{
if (tnow < told)
{
tcnt += told - tnow;
}
else
{
tcnt += reload - tnow + told;
}
told = tnow;
if (tcnt >= ticks)
{
break;
}
}
}
}