目录
一、软件定时器的基本概念
1、软件定时器与硬件定时器
2、单次模式和周期模式
二、软件定时器应用场景
三、软件定时器的精度
四、软件定时器控制块
五、软件定时其函数接口
1、创建软件定时器函数 OSTmrCreate()
2、启动软件定时器函数 OSTmrStart()
3、软件定时器列表管理
4、停止定时器函数 OSTmrStop()
5、删除软件定时器函数 OSTmrDel()
六、软件定时器任务
七、实现
一、软件定时器的基本概念
定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。类似生活中的闹钟,我们可以设置闹钟每天什么时候响,还能设置响的次数,是响一次还是每天都响。
1、软件定时器与硬件定时器
定时器有硬件定时器和软件定时器之分:
硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。
使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息;而使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数,为了统一,下文均用回调函数描述),在回调函数中处理信息。
注意:软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。
软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。定时精度与系统时钟的周期有关。一般系统利用 SysTick 作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务), 比如 OSTimeDly()以及其它能阻塞任务运行的函数,两次触发回调函数的时间间隔 period 叫定时器的定时周期。
uCOS 操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。 UCOS 软件定时器功能上支持:
- 裁剪:能通过宏关闭软件定时器功能。
- 软件定时器创建。
- 软件定时器启动。
- 软件定时器停止。
- 软件定时器删除。
2、单次模式和周期模式
uCOS 提供的软件定时器支持单次模式和周期模式, 单次模式和周期模式的定时时间到之后都会调用软件定时器的回调函数,用户可以在回调函数中加入要执行的工程代码。
单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将不再重复执行,当然用户还是可以调用软件定时器启动函数 OSTmrStart()来启动一次软件定时器。
周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。
当然, uCOS 中软件定时器的周期模式也分为两种,一种是有初始化延迟的周期模式,另一种是无初始化延迟的周期模式, 由 OSTmrCreate()中的“dly” 参数设置, 这两种周期模式基本是一致的,但是有个细微的差别。
有初始化延迟的周期模式:在软件定时器创建的时候,其第一个定时周期是由定时器中的 dly 参数决定,然后在运行完第一个周期后,其以后的定时周期均由 period 参数决定。
无初始化延迟的周期模式:该定时器从始至终都按照周期运行。
比如我们创建两个周期定时器,定时器 1 是无初始化延迟的定时器,周期为 100 个tick(时钟节拍) ,定时器 2 是有初始化延迟的定时器,其初始化延迟的 dly 参数为 150 个tick,周期为 100 个 tick,从 tick 为 0 的时刻就启动了两个软件定时器。定时器 1 从始至终都按照正常的周期运行,但是定时器 2 则在第一个周期中的运行周期为 dly,从第二个运行周期开始按照正常的 100 个 tick 来运行。
uCOS 通过一个 OS_TmrTask 任务(也叫软件定时器任务) 来管理软定时器,它是在系统初始化时(OSInit()函数中) 自动创建的,为了满足用户定时需求。 TmrTask 任务会在定时器节拍到来的时候检查定时器列表,看看是否有定时器时间到了,如果到了就调用其回调函数。
二、软件定时器应用场景
在很多应用中,我们需要一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器代替硬件定时器任务。 但需要注意的是软件定时器的精度是无法和硬件定时器相比的,因为在软件定时器的定时过程中是极有可能被其它中断所打断,因为软件定时器的执行上下文环境是任务。所以,软件定时器更适用于对时间精度要求不高的任务,一些辅助型的任务。
三、软件定时器的精度
在操作系统中,通常软件定时器以系统节拍为计时的时基单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,就类似人的心跳, 1s 能跳动多少下,系统节拍配置为OS_CFG_TICK_RATE_HZ,该宏在 os_app_cfg.h 中有定义,默认是 1000。那么系统的时钟节拍周期就为 1ms(1s 跳动 1000 下,每一下就为 1ms)。
uCOS 软 件 定 时 器 的 精 度 ( 分 辨 率 ) 决 定 于 系 统 时 基 频 率 , 也 就 是 变 量OS_CFG_TMR_TASK_RATE_HZ 的值,它是以 Hz 为单位的。如果软件定时器任务的频率( OS_CFG_TMR_TASK_RATE_HZ) 设置为 10Hz,系统中所有软件定时器的精度为十分之一秒。事实上,这是用于软件定时器的推荐值,因为软件定时器常用于不精确时间尺度的任务。
而且定时器所定时的数值必须是这个定时器任务精度的整数倍,例如,定时器任务的频率为 10HZ,那么上层软件定时器定时数值只能是 100ms, 200ms, 1000ms 等,而不能取值为 150ms。由于系统节拍与软件定时器频率决定了系统中定时器能够分辨的精确度,用户可以根据实际 CPU 的处理能力和实时性需求设置合适的数值, 软件定时器频率的值越大,精度越高,但是系统开销也将越大,因为这代表在 1 秒中系统进入定时器任务的次数也就越多。
注意:定时器任务的频率 OS_CFG_TMR_TASK_RATE_HZ 的值不能大于系统时基频率 OS_CFG_TMR_TASK_RATE_HZ 的值。
四、软件定时器控制块
uCOS 的软件定时器也属于内核对象,是一个可以裁剪的功能模块,同样在系统中由一个控制块管理其相关信息,软件定时器的控制块中包含创建的软件定时器基本信息,在使用定时器前我们需要通过 OSTmrCreate()函数创建一个软件定时器,但是在创建前需要我们定义一个定时器的句柄(控制块)。
五、软件定时其函数接口
1、创建软件定时器函数 OSTmrCreate()
软件定时器也是内核对象,与消息队列、信号量等内核对象一样,都是需要创建之后才能使用的资源,我们在创建的时候需要指定定时器延时初始值 dly、定时器周期、定时器工作模式、回调函数等。每个软件定时器只需少许的 RAM 空间,理论上 uCOS 支持无限多个软件定时器,只要 RAM 足够即可。
2、启动软件定时器函数 OSTmrStart()
我们知道,在系统初始化的时候,系统会帮我们自动创建一个软件定时器任务,在这个任务中,如果暂时没有运行中的定时器,任务会进入阻塞态等待定时器任务节拍的信号量。我们在创建一个软件定时器之后,如果没有启动它,该定时器就不会被添加到软件定时器列表中,那么在定时器任务就不会运行该定时器,而 OSTmrStart()函数就是将已经创建的软件定时器添加到定时器列表中,这样子被创建的定时器就会被系统运行。
3、软件定时器列表管理
有些情况下,当系统中有多个软件定时器的时候, uCOS 可能要维护上百个定时器。使用定时器列表会大大降低更新定时器列表所占用的 CPU 时间,一个一个检测是否到期效率很低,有没有什么办法让系统快速查找到到期的软件定时器? uCOS 对软件定时器列表的管理就跟时间节拍一样,采用哈希算法。
定时器列表中包含了 OS_CFG_TMR_WHEEL_SIZE 条记录, 该值是一个宏定义,由用户指定,在 os_cfg_app.h 文件中。 能记录定时器的多少仅限于处理器的 RAM 空间, 推荐的设置值为定时器数 /4。 定时器列表的每个记录都由 3 部分组成: NbrEntriesMax 表明该记录中有多少个定时器; NbrEntriesMax 表明该记录中最大时存放了多少个定时器; FirstPtr指向当前记录的定时器链表。
4、停止定时器函数 OSTmrStop()
OSTmrStop()函数用于停止一个软件定时器。软件定时器被停掉之后可以调用OSTmrStart()函数重启,但是重启之后定时器是从头计时,而不是接着上次停止的时刻继续计时。
5、删除软件定时器函数 OSTmrDel()
OSTmrDel()用于删除一个已经被创建成功的软件定时器, 删除之后就无法使用该定时器, 并且定时器相应的信息也会被系清空。
六、软件定时器任务
我们知道,软件定时器的回调函数的上下文是在任务中,所有,系统中必须要一个任务来管理所有的软件定时器,等到定时时间到达后就调用定时器对应的回调函数,那么软件定时器任务又是一个什么东西呢,它是在系统初始化的时候系统就帮我们创建的一个任务。
七、实现
#include <includes.h>CPU_TS ts_start; //时间戳变量
CPU_TS ts_end; static OS_TCB AppTaskStartTCB; //任务控制块static OS_TCB AppTaskTmrTCB;static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; //任务堆栈static CPU_STK AppTaskTmrStk [ APP_TASK_TMR_STK_SIZE ];static void AppTaskStart (void *p_arg); //任务函数声明static void AppTaskTmr ( void * p_arg );int main (void)
{OS_ERR err;OSInit(&err); //初始化 uC/OS-III/* 创建起始任务 */OSTaskCreate((OS_TCB *)&AppTaskStartTCB, //任务控制块地址(CPU_CHAR *)"App Task Start", //任务名称(OS_TASK_PTR ) AppTaskStart, //任务函数(void *) 0, //传递给任务函数(形参p_arg)的实参(OS_PRIO ) APP_TASK_START_PRIO, //任务的优先级(CPU_STK *)&AppTaskStartStk[0], //任务堆栈的基地址(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10, //任务堆栈空间剩下1/10时限制其增长(CPU_STK_SIZE) APP_TASK_START_STK_SIZE, //任务堆栈空间(单位:sizeof(CPU_STK))(OS_MSG_QTY ) 5u, //任务可接收的最大消息数(OS_TICK ) 0u, //任务的时间片节拍数(0表默认值OSCfg_TickRate_Hz/10)(void *) 0, //任务扩展(0表不扩展)(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), //任务选项(OS_ERR *)&err); //返回错误类型OSStart(&err); //启动多任务管理(交由uC/OS-III控制)}static void AppTaskStart (void *p_arg)
{CPU_INT32U cpu_clk_freq;CPU_INT32U cnts;OS_ERR err;(void)p_arg;CPU_Init(); //初始化 CPU 组件(时间戳、关中断时间测量和主机名)BSP_Init(); //板级初始化cpu_clk_freq = BSP_CPU_ClkFreq(); //获取 CPU 内核时钟频率(SysTick 工作时钟)cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; //根据用户设定的时钟节拍频率计算 SysTick 定时器的计数值OS_CPU_SysTickInit(cnts); //调用 SysTick 初始化函数,设置定时器计数值和启动定时器Mem_Init(); //初始化内存管理组件(堆内存池和内存池表)#if OS_CFG_STAT_TASK_EN > 0u //如果使能(默认使能)了统计任务OSStatTaskCPUUsageInit(&err); //计算没有应用任务(只有空闲任务)运行时 CPU 的(最大)
#endif //容量(决定 OS_Stat_IdleCtrMax 的值,为后面计算 CPU //使用率使用)。
#ifdef CPU_CFG_INT_DIS_MEAS_ENCPU_IntDisMeasMaxCurReset(); //复位(清零)当前最大关中断时间
#endif/* 创建 AppTaskTmr 任务 */OSTaskCreate((OS_TCB *)&AppTaskTmrTCB, //任务控制块地址(CPU_CHAR *)"App Task Tmr", //任务名称(OS_TASK_PTR ) AppTaskTmr, //任务函数(void *) 0, //传递给任务函数(形参p_arg)的实参(OS_PRIO ) APP_TASK_TMR_PRIO, //任务的优先级(CPU_STK *)&AppTaskTmrStk[0], //任务堆栈的基地址(CPU_STK_SIZE) APP_TASK_TMR_STK_SIZE / 10, //任务堆栈空间剩下1/10时限制其增长(CPU_STK_SIZE) APP_TASK_TMR_STK_SIZE, //任务堆栈空间(单位:sizeof(CPU_STK))(OS_MSG_QTY ) 5u, //任务可接收的最大消息数(OS_TICK ) 0u, //任务的时间片节拍数(0表默认值OSCfg_TickRate_Hz/10)(void *) 0, //任务扩展(0表不扩展)(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), //任务选项(OS_ERR *)&err); //返回错误类型OSTaskDel ( & AppTaskStartTCB, & err ); //删除起始任务本身,该任务不再运行}void TmrCallback (OS_TMR *p_tmr, void *p_arg) //软件定时器MyTmr的回调函数
{CPU_INT32U cpu_clk_freq; CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和定义一个局部变//量,用于保存关中断前的 CPU 状态寄存器 SR(临界段关中断只需保存SR)//,开中断时将该值还原。 printf ( "%s", ( char * ) p_arg );cpu_clk_freq = BSP_CPU_ClkFreq(); //获取CPU时钟,时间戳是以该时钟计数LED1_TOGGLE ; ts_end = OS_TS_GET() - ts_start; //获取定时后的时间戳(以CPU时钟进行计数的一个计数值)//,并计算定时时间。OS_CRITICAL_ENTER(); //进入临界段,不希望下面串口打印遭到中断printf ( "\r\n定时1s,通过时间戳测得定时 %07d us,即 %04d ms。\r\n", ts_end / ( cpu_clk_freq / 1000000 ), //将定时时间折算成 us ts_end / ( cpu_clk_freq / 1000 ) ); //将定时时间折算成 ms OS_CRITICAL_EXIT(); ts_start = OS_TS_GET(); //获取定时前时间戳}static void AppTaskTmr ( void * p_arg )
{OS_ERR err;OS_TMR my_tmr; //声明软件定时器对象(void)p_arg;/* 创建软件定时器 */OSTmrCreate ((OS_TMR *)&my_tmr, //软件定时器对象(CPU_CHAR *)"MySoftTimer", //命名软件定时器(OS_TICK )10, //定时器初始值,依10Hz时基计算,即为1s(OS_TICK )10, //定时器周期重载值,依10Hz时基计算,即为1s(OS_OPT )OS_OPT_TMR_PERIODIC, //周期性定时(OS_TMR_CALLBACK_PTR )TmrCallback, //回调函数(void *)"Timer Over!", //传递实参给回调函数(OS_ERR *)err); //返回错误类型/* 启动软件定时器 */ OSTmrStart ((OS_TMR *)&my_tmr, //软件定时器对象(OS_ERR *)err); //返回错误类型ts_start = OS_TS_GET(); //获取定时前时间戳while (DEF_TRUE) { //任务体,通常写成一个死循环OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err ); //不断阻塞该任务}}