GoupOS时钟管理

GoupOS时钟管理

1. 时钟管理

时间是非常重要的概念?和朋友出去游玩需要约定时间,完成任务也需要花费时,生活离不开时间。操作系统也一样需要通过时间来规范其任务的执行。操作系统中最小的时间单位是时钟节拍(OS Tick)。时钟节拍和基于时钟节拍的定时器。

1. 时钟节拍

任何操作系统都需要提供一个时钟节拍,以供系统处理所有与时间有关的事件, 如线程的延时、线程的时间片轮转凋度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以被看作系统心跳,中断之间的时间间隔取决于不同的应用,一般是1~100ms。时钟节拍率越快,系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。
GoupOS中,时钟节拍的长度可以根据OS_SYSTICK_MS的定义来调整,每秒跳动次数等于1/OS_SYSTICK_MS秒。

1. 时钟节拍的实现方式

时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次void tTaskSwitch(void),通知操作系统已经过去一个系统时钟。不同硬件定时器的中断实现都不同,下面的中断函数以STM32定时器作为示例。

tcpu.h
1
2
3
4
5
6
7
8
9
/** 
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
tTaskSystemTickHandler();
}
os_tTask.h
1
2
3
4
5
6
7
8
9
10
11
void tTaskSystemTickHandler(void)
{
//...
//code
#if (GOUPOS_ENABLE_TIMER == 1)
/*<!定时器模块通知操作*/
TimerModuleTickNotify();
#endif
//...
//code
}

2. 定时器管理

定时器的功能是从指定的时刻开始,经过指定时间后触发一个事件,例如定个时间提醒第二天能够按时起床。定时器有硬件定时器和软件定时器两种。
(1)硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别, 并且是中断触发方式。
(2)软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。
GoupOS操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是OS Tick的整数倍,例如一个OS Tick是10ms, 那么上层软件定时器只能是10ms、20ms、100ms等,而不能定时为15ms。GoupOS的定时器也提供了基于时钟节拍整数倍的定时能力。

1. GoupOS定时器介绍

GoupOS定时器提供两类定时器机制:分别是一次性定时器任务和周期性定时器;一次性定时器在启动后只会触发一次定时器事件,然后定时器自动停止。周期触发定时器,这类定时器会周期性地触发定时器事件,直到用户手动停止,否则将永远执行下去。
另外,根据超时函数执行时所处的上下文环境,GoupOS的定时器可以分为OS_STARTHARD_TIMER模式与OS_STARTSOFT_TIMER模式,如下图所示。
定时器.png

1. OS_STARTHARD_TIMER模式

OS_STARTHARD_TIMER模式的定时器超时函数在中断上下文环境中执行,可以在初始化/创建定时器时使用TIMER_CONFIG_TYPE_HARD参数来指定。
在中断上下文环境中执行时,对于定时器函数的要求与中断服务例程的要求相同:执行时间应该尽掀短,执行时不应导致当前上下文挂起、等待,例如在中断上下文中执行的超时函数不应该试图去申请动态内存、释放动态内存等。
GoupOS定时器,即定时器定时到达后,定时函数是在系统时钟中断的上下文环境中运行的。在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间。

2. OS_STARTSOFT_TIMER模式

TIMER_CONFIG_TYPE_SOFT模式可配置,通过宏定义OS_STARTSOFT_TIMER来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个TimerTask线程,然后OS_STARTSOFT_TIMER模式的定时器函数会在TimerTask线程的上下文环境中执行。可以在初始化/创建定时器时使用参数TIMER_CONFIG_TYPE_SOFT来设置OS_STARTSOFT_TIMER模式。

2. 定时器的工作机制

下面以一个例子来说明GoupOS定时器的工作机制。在GoupOS定时器模块中维护着两个重要的全局变量:
(1)当前系统经过OS tick时间tickCount(当硬件定时器中断来临时,它将加1)。
(2)定时器链表tTimerSoftList。系统新创建并激活的定时器都会以超时时间排序的方式插入tTimerSoftList链表中。
如下图所示,系统当前tickCount值为20, 在当前系统中已经创建并启动了三个定时器,分别是定时时间为10个tick的timer1、20个tick的timer2和30个tick的timer3, 这三个定时器分别加上系统当前时间tickCount=20, 按从小到大的顺序链接在tTimerSoftList链表中,形成如下图所示的定时器链表结构。
定时器链表结构

而tickCount随着硬件定时器的触发一直在增长(每一次硬件定时器中断来临,tickCount变量就会加1), 10个tick以后,tickCount从20增长到30, timerl的延时值会等于0,这时会触发timer1定时器相关联的函数,同理,40个tick 和50个tick过去后,与timer2和timer3定时器相关联的函数会被触发。
如果系统当前定时器状态在10个tick以后(tickCount=30)有一个任务新创建了一个tick值为15的timer4定时器,由于timer4定时器delaytime= tickCount +15=45,因此它将被插入到timer3后,形成如下图所示的链表结构。
定时器链表结构

1. 定时器控制块

在GoupOS操作系统中,定时器控制块是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息,例如初始延时数、循环延时数,也包含定时器与定时器之间连接用的链表结构、延时到达回调函数等。
定时器控制块由tTimer结构体定义并形成定时器内核对象, linkNode成员则用于把一个激活的(已经启动的)定时器链接到tTimerSoftListtTimerHardList链表中。

os_tTimer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief 定时器结构体
*/
typedef struct _tTimer
{
char Timername[OS_TIMERNAME_LENGTH]; /*<!定时器名称*/
uint16_t Timername_length; /*<!定时器名称长度*/
tNode linkNode; /*<!链表节点*/
uint32_t startDelayTicks; /*<!初次(开始)启动延后的ticks数*/
uint32_t durationTicks; /*<!周期定时时的周期ticks数(周期执行的时间)(持续时间滴答)*/
uint32_t delayTicks; /*<!当前定时递减计数值*/
void (*timerFunc)(void *arg); /*<!定时回调函数*/
void * arg; /*<!传递给回调函数的参数*/
uint32_t config; /*<!定时器配置参数*/
tTimerState state; /*<!定时器状态*/
}tTimer;

3. 定时器的管理方式

介绍了GoupOS定时器并对定时器的工作机制进行了概念上的讲解,本节深入介绍定时器的各个接口,帮助读者在代码层次上理解GoupOS定时器。
在系统启动时需要初始化定时器管理系统。可以通过下面的函数接口完成:

os_tTimer.h
1
2
void OS_TimerTaskInit(void)
void OS_TimerModule_Init(void);

定时器控制块中含有定时器相关的重要参数,它们在定时器各种状态间起到纽带的作用。定时器的相关操作如图5-4所示,包括:创建/初始化定时器、启动定时器、停止/控制定时器、删除/脱离定时器。一次性定时器在定时到达后会从定时器链表中被移除,而周期性定时器会一直在定时器链表,这与定时器参数设置相关。在每次的操作系统时钟中断发生时,都会更改已经延时到达的定时器状态参数。
定时器的相关操

1. 创建和删除定时器

创建定时器时,可利用tTimerInit接口来初始化该定时器,函数接口如下:

os_tTimer.h
1
void tTimerInit(char *Timername, uint16_t Timername_len,tTimer * timer,uint32_t delayTicks,uint32_t durationTicks,void (*timerFunc)(void *arg),void *arg,uint32_t config)

使用该函数接口时会初始化相应的定时器控制块、定时器名称、定时器到达函数等,其中的各参数和返回值说明见下表

线程状态转换的基本函数
参数 描述
Timername 定时器的名称
Timername_len 定时器名称长度,当名称长度超过OS_TIMERNAME_LENGTH多余的字段会被截断;
timer 定时器结构体
delayTicks 定时器初始启动时延时值
durationTicks 周期定时时的周期ticks数(周期执行的时间)(持续时间滴答)
void (*timerFunc)(void *arg) 定时器延时到达函数指针(当定时器到达时,系统会调用这个函数)
arg 定时器延时到达函数的入口参数(当定时器延时到达时,调用延时到达回调函数会把这个参数作为入口参数传递给延时到达函数)
config 通过该配置配置该定时器为软定时器还是硬件定时器配置参数:
#define TIMER_CONFIG_TYPE_HARD (1<<0) 在中断服务函数中处理
#define TIMER_CONFIG_TYPE_SOFT (0<<0) 在任务中处理

当一个定时器不需要再使用时,可以使用下面的函数接口:

os_tTimer.h
1
void tTimerDestroy(tTimer * timer); //定时器删除函数

删除定时器时,但是定时器对象所占有的内存不会被释放,其中的各参数和返回值说明见下表

tTimerDestroy()的输入参数
参数 描述
timer 定时器结构体

2. 启动和停止定时器

当定时器被创建或者初始化以后,并不会被立即启动,必须在调用启动定时器函数接口后才开始工作,启动定时器函数接口如下:

os_tTimer.h
1
void tTimerStart(tTimer *timer);

调用定时器启动函数接口后,定时器的状态将更改为激活状态(tTimerStarted),并按照配置硬件或者软件定时器插入不同队列链表中,其中的各参数和返回值说明见下表

tTimerStart()的输入参数
参数 描述
timer 定时器结构体

启动定时器以后,若想使它停止,可以使用下面的函数接口:

os_tTimer.h
1
void tTimerStop(tTimer *timer);  //定时器停止函数

调用定时器停止函数接口后,定时器状态将更改为停止状态,并从软件tTimerSoftList链表或者硬件tTimerHardList链表中脱离出来,不参与定时器延时检查。其中的各参数和返回值说明见表

tTimerStop()的输入参数
参数 描述
timer 定时器结构体

3. 定时器应用示例

下面是一个创建定时器的例子,该例程会创建两个定时器,一个是单次定时, 一个是周期定时并让周期性定时器运行一段时间后停止,如代码清单如下所示

创建定时器例程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "GoupOSinclude.h"
#include "app.h"


tTimer timer1;
tTimer timer2;

uint16_t timer1_num = 0;
uint16_t timer2_num = 0;
/*定时器1*/
void timer1func(void *arg)
{
timer1_num++;
}

/*定时器2*/
void timer2func(void *arg)
{
/*运行第10次,停止周期性定时器*/
if(++timer2_num >= 10)
{
tTimerStop(&timer2);
}
}

/*用户任务初始化程序*/
void AppTaskInit(void)
{
/*定时器初始化函数*/
tTimerInit("timer1",sizeof("timer1"),&timer1,10,0,timer1func,(void * )0,TIMER_CONFIG_TYPE_SOFT);
tTimerInit("timer2",sizeof("timer2"),&timer2,15,10,timer2func,(void * )0,TIMER_CONFIG_TYPE_SOFT);
/*定时器启动函数*/
tTimerStart(&timer1);
tTimerStart(&timer2);

}

运行结果:前面写的钩子函数,和任务1和任务的函数还是在正常继续运行

定时器例程

周期性定时器启动时先延时15个OS Tick启动运行1次,然后延时函数每10个OS Tick运行1次,共运行10次(10次后调用tTimerStop使定时器1停止运行);单次定时器的超时函数在第10个OSTick时运行一次。