(学习日记)2024.04.13:UCOSIII第四十一节:软件定时器实验
写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.04.13:UCOSIII第四十一节:软件定时器实验
- 五十五、UCOSIII:软件定时器实验
- 1、软件定时器任务
- 2、软件定时器实验
- 3、软件定时器实验现象
- 4、总结
五十五、UCOSIII:软件定时器实验
1、软件定时器任务
软件定时器的回调函数的上下文是在任务中,所以系统中必须要一个任务来管理所有的软件定时器, 等到定时时间到达后就调用定时器对应的回调函数。
那么软件定时器任务又是一个什么东西呢,它是在系统初始化的时候系统就帮我们创建的一个任务
void OS_TmrInit (OS_ERR *p_err) { OS_TMR_SPOKE_IX i; OS_TMR_SPOKE *p_spoke; #ifdef OS_SAFETY_CRITICAL if (p_err == (OS_ERR *)0) { OS_SAFETY_CRITICAL_EXCEPTION(); return; } #endif #if OS_CFG_DBG_EN > 0u OSTmrDbgListPtr = (OS_TMR *)0; #endif if (OSCfg_TmrTaskRate_Hz > (OS_RATE_HZ)0) (1) { OSTmrUpdateCnt = OSCfg_TickRate_Hz / OSCfg_TmrTaskRate_Hz; } else (2) { OSTmrUpdateCnt = OSCfg_TickRate_Hz / (OS_RATE_HZ)10; } OSTmrUpdateCtr = OSTmrUpdateCnt; OSTmrTickCtr = (OS_TICK)0; OSTmrTaskTimeMax = (CPU_TS)0; for (i = 0u; i NbrEntries = (OS_OBJ_QTY)0; p_spoke->NbrEntriesMax = (OS_OBJ_QTY)0; p_spoke->FirstPtr = (OS_TMR *)0; } /* ---------------- CREATE THE TIMER TASK --------------- */ if (OSCfg_TmrTaskStkBasePtr == (CPU_STK*)0) { *p_err = OS_ERR_TMR_STK_INVALID; return; } if (OSCfg_TmrTaskStkSize = (OS_CFG_PRIO_MAX - 1u)) { *p_err = OS_ERR_TMR_PRIO_INVALID; return; } OSTaskCreate((OS_TCB *)&OSTmrTaskTCB, (CPU_CHAR *)((void *)"μC/OS-III Timer Task"), (OS_TASK_PTR )OS_TmrTask, (void *)0, (OS_PRIO )OSCfg_TmrTaskPrio, (CPU_STK *)OSCfg_TmrTaskStkBasePtr, (CPU_STK_SIZE)OSCfg_TmrTaskStkLimit, (CPU_STK_SIZE)OSCfg_TmrTaskStkSize, (OS_MSG_QTY )0, (OS_TICK )0, (void *)0, (OS_OPT)(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR|OS_OPT_TASK_NO_TLS), (OS_ERR *)p_err); (4) }
- (1):正常来说定时器任务的执行频率OSCfg_TmrTaskRate_Hz 是大于 0 的, 并且能被 OSCfg_TickRate_Hz 整除,才能比较准确得到定时器任务运行的频率。 如果OSCfg_TmrTaskRate_Hz设置为大于0,就配置定时器任务的频率。
- (2):否则就配置为系统时钟频率的十分之一(1/10)。 不过当设定的定时器的频率大于时钟节拍的执行频率的时候,定时器运行就会出错,但是这里没有进行判断,我们自己在写代码的时候注意一下即可。
举个例子,系统的OSCfg_TickRate_Hz 是 1000,OSCfg_TmrTaskRate_Hz 是 10, 那么计算得到OSTmrUpdateCnt 就是 100,开始的时候OSTmrUpdateCtr 是跟 OSTmrUpdateCnt 一样大的, 都是100,每当时钟节拍到来的时候OSTmrUpdateCtr 就减 1,减到 0 的话就运行定时器任务, 这样子就实现了从时间节拍中分频得到定时器任务频率。如果 OSCfg_TmrTaskRate_Hz 不能被 OSCfg_TickRate_Hz 整除, 比如 OSCfg_TickRate_Hz 设置为1000,OSCfg_TmrTaskRate_Hz 设置为 300, 这样子设置是想要定时器任务执行频率是 300Hz, 但是 OSTmrUpdateCnt 计算出来是 3,这样子定时器任务的执行频率大约就是 330Hz,定时的单位本来想设置为 3.3ms, 可实际运行的单位却是3ms,这样子肯定导致定时器不是很精确的,这些处理还是需要我们根据实际情况进行调整的。
- (3):利用for循环初始化定时器列表。
- (4):创建OS_TmrTask任务。
我们来看看定时器任务是在做什么事情,OS_TmrTask()源码具体如下:
void OS_TmrTask (void *p_arg) { CPU_BOOLEAN done; OS_ERR err; OS_TMR_CALLBACK_PTR p_fnct; OS_TMR_SPOKE *p_spoke; OS_TMR *p_tmr; OS_TMR *p_tmr_next; OS_TMR_SPOKE_IX spoke; CPU_TS ts; CPU_TS ts_start; CPU_TS ts_end; p_arg = p_arg;/* Not using 'p_arg', prevent compiler warning */ while (DEF_ON) { /* 等待信号指示更新定时器的时间*/ (void)OSTaskSemPend((OS_TICK )0, (OS_OPT )OS_OPT_PEND_BLOCKING, (CPU_TS *)&ts, (OS_ERR *)&err); (1) OSSchedLock(&err); ts_start = OS_TS_GET(); /* 增加当前定时器时间*/ OSTmrTickCtr++; (2) /* 通过哈希算法找到对应时间唤醒的列表 */ spoke = (OS_TMR_SPOKE_IX)(OSTmrTickCtr % OSCfg_TmrWheelSize); p_spoke = &OSCfg_TmrWheel[spoke]; (3) /* 获取列表头部的定时器 */ p_tmr = p_spoke->FirstPtr; (4) done = DEF_FALSE; while (done == DEF_FALSE) { if (p_tmr != (OS_TMR *)0) (5) { /* 指向下一个定时器以进行更新, 因为可能当前定时器到时了会从列表中移除 */ p_tmr_next = (OS_TMR *)p_tmr->NextPtr; /* 确认是定时时间到达 */ if (OSTmrTickCtr == p_tmr->Match) (6) { /* 先移除定时器 */ OS_TmrUnlink(p_tmr); /* 如果是周期定时器 */ if (p_tmr->Opt == OS_OPT_TMR_PERIODIC) { /* 重新按照唤醒时间插入定时器列表 */ OS_TmrLink(p_tmr, OS_OPT_LINK_PERIODIC);(7) } else { /* 定时器状态设置为已完成 */ p_tmr->State = OS_TMR_STATE_COMPLETED;(8) } /* 执行回调函数(如果可用)*/ p_fnct = p_tmr->CallbackPtr; if (p_fnct != (OS_TMR_CALLBACK_PTR)0) { (*p_fnct)((void *)p_tmr, p_tmr->CallbackPtrArg); (9) } /* 看看下一个定时器是否匹配 */ p_tmr = p_tmr_next; (10) } else { done = DEF_TRUE; } } else { done = DEF_TRUE; } } /* 测量定时器任务的执行时间*/ ts_end = OS_TS_GET() - ts_start; (11) OSSchedUnlock(&err); if (OSTmrTaskTimeMax
- (1):调用OSTaskSemPend()函数在一直等待定时器节拍的信号量, 等待到了才运行,那定时器节拍是怎么样运行的呢。
系统的时钟节拍是基于SysTick定时器上的,μC/OS采用Tick任务(OS_TickTask)管理系统的时间节拍, 而我们定时器节拍是由系统节拍分频而来,那么其发送信号量的地方当然也是在SysTick中断服务函数中但是μC/OS支持采用中断延迟,如果使用了中断延迟, 那么发送任务信号量的地方就会在中断发布任务中(OS_IntQTask),从代码中,我们可以看到当OSTmrUpdateCtr减到0的时候才会发送一次信号量, 这也就是为什么我们的定时器节拍是基于系统时钟节拍分频而来的原因,具体如下
/***************************在SysTick中断服务函数中***********************/ #if OS_CFG_TMR_EN > 0u //如果启用(默认启用)了软件定时器 OSTmrUpdateCtr--; //软件定时器计数器自减 if (OSTmrUpdateCtr == (OS_CTR)0u) //如果软件定时器计数器减至0 { OSTmrUpdateCtr = OSTmrUpdateCnt; //重载软件定时器计数器 //发送信号量给软件定时器任务OS_TmrTask() OSTaskSemPost((OS_TCB *)&OSTmrTaskTCB, (OS_OPT ) OS_OPT_POST_NONE, (OS_ERR *)&err); } #endif /*********************在中断发布任务中**********************************/ #if OS_CFG_TMR_EN > 0u OSTmrUpdateCtr--; if (OSTmrUpdateCtr == (OS_CTR)0u) { OSTmrUpdateCtr = OSTmrUpdateCnt; ts = OS_TS_GET(); /* 获取时间戳 */ /* 发送信号量给软件定时器任务OS_TmrTask()*/ (void)OS_TaskSemPost((OS_TCB *)&OSTmrTaskTCB, (OS_OPT ) OS_OPT_POST_NONE, (CPU_TS ) ts, (OS_ERR *)&err); } #endif
- (2):当任务获取到信号量的时候,任务开始运行,增加当前定时器时间记录OSTmrTickCtr。
- (3):通过哈希算法找到对应时间唤醒的列表,比如按照我们前面添加的定时器1与定时器2, 当OSTmrTickCtr到达13的时候,通过哈希算法的运算之后, 我们能得到spoke等于4,这样子就直接找到我们插入的定时器列表了。
- (4):获取列表头部的定时器。
- (5):如果定时器列表中有定时器的话, 就将p_tmr_next变量指向下一个定时器以准备进行更新,因为当前定时器可能到时了,就会从列表中移除。
- (6):如果当前定时器时间(OSTmrTickCtr)与定时器中的匹配时间(Match)是一样的, 那么确认是定时时间已经到达。
- (7):不管三七二十一调用OS_TmrUnlink()函数移除定时器, 如果该定时器是周期定时器,那么就调用OS_TmrLink()函数按照唤醒时间重新插入定时器列表。
- (8):否则就是单次定时器,那么将定时器状态设置为定时已完成。
- (9):如果回调函数存在,执行回调函数。
- (10):看看下一个定时器的定时时间是否也到达了,如果是就需要唤醒。
- (11):测量定时器任务的执行时间。
当定时器任务被执行,它首先递增 OSTmrTickCtr变量,然后通过哈希算法决定哪个定时器列表需被更新。
如果这个定时器列表中存在定时器(FirstPtr不为NULL), 系统会检查定时器中的匹配时间Match是否与当前定时器时间OSTmrTickCtr相等,如果相等,这个定时器会被移出该定时器, 然后调用这个定时器的回调函数(假定这个定时器被创建时有回调函数),再根据定时器的工作模式决定是否重新插入定时器列表中。 然后遍历该定时器列表直到没有定时器的Match值与OSTmrTickCtr匹配。
注意:
当定时器被唤醒后,定时器列表会被重新排序,定时器也不一定插入原本的定时器列表中。
OS_TmrTask()任务的大部分工作都是在锁调度器的状态下进行的。然而,因为定时器列表会被重新分配(依次排序),所以遍历这个定时器列表的时间会非常短的,也就是临界段会非常短的。
2、软件定时器实验
软件定时器实验是在μC/OS中创建了一个应用任务 AppTaskTmr,在该任务中创建一个软件定时器, 周期性定时 1s,每次定时完成切换 LED1 的亮灭状态, 并且打印时间戳的计时,检验定时的精准度,具体如下:
#include 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); //初始化 μC/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); //启动多任务管理(交由μC/OS-III控制) } static void AppTaskStart (void *p_arg) { CPU_INT32U cpu_clk_freq; CPU_INT32U cnts; OS_ERR err; (void)p_arg; //板级初始化 BSP_Init(); //初始化 CPU 组件(时间戳、关中断时间测量和主机名) CPU_Init(); //获取 CPU 内核时钟频率(SysTick 工作时钟) cpu_clk_freq = BSP_CPU_ClkFreq(); //根据用户设定的时钟节拍频率计算 SysTick 定时器的计数值 cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; //调用 SysTick 初始化函数,设置定时器计数值和启动定时器 OS_CPU_SysTickInit(cnts); //初始化内存管理组件(堆内存池和内存池表) Mem_Init(); #if OS_CFG_STAT_TASK_EN > 0u OSStatTaskCPUUsageInit(&err); #endif CPU_IntDisMeasMaxCurReset(); //复位(清零)当前最大关中断时间 /* 创建 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 ); //删除起始任务本身,该任务不再运行 } //软件定时器MyTmr的回调函数 void TmrCallback (OS_TMR *p_tmr, void *p_arg) { CPU_INT32U cpu_clk_freq; //使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和定义一 //个局部变量,用于保存关中断前的 CPU 状态寄存器 // SR(临界段关中断只需保存SR),开中断时将该值还原。 CPU_SR_ALLOC(); printf ( "%s", ( char * ) p_arg ); cpu_clk_freq = BSP_CPU_ClkFreq(); //获取CPU时钟,时间戳是以该时钟计数 macLED1_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 ); //不断阻塞该任务 } }
3、软件定时器实验现象
在串口调试助手中可以看到运行结果我们可以看到,每1S时间到的时候, 软件定时器就会触发一次回调函数,具体见图
4、总结
从一开始的定时器相关函数的使用和分析到后面定时器运作机制的分析,想必大家对定时器整个运作有了更深的了解。
定时器的创建、删除、启动、 停止这些操作无非就是在操作定时器列表的双向列表和根据不同的设置进行定时器状态的转化以及相关的处理。
至于检测定时器到期,系统将时间节拍进行分频得到定时器任务执行的频率, 在定时器任务中,系统采用了哈希算法进行快速检测有没有定时器到期,然后执行其对应的回调函数等操作。
软件定时器最核心的一点是底层的一个硬件定时器(SysTick内核定时器)上进行软件分频,这也是μC/OS写的很好的一点,大家也可以学习它的这种编程思想。
μC/OS允许用户建立任意数量的定时器(只限制于处理器的RAM大小)。
回调函数是在定时器任务中被调用,所以回调函数的上下文环境是在任务中,并且运行回调函数时调度器处于被锁状态。
一般我们编写的回调函数越简短越好, 并且不能在回调函数中等待消息队列、信号量、事件等操作,否则定时器任务会被挂起,导致定时器任务崩溃,这是绝对不允许的。
此外我们还需要注意几点:
- 回调函数是在定时器任务中被执行的,这意味着定时器任务需要有足够的栈空间供回调函数去执行。
- 回调函数是在根据定时器队列中依次存放的,所以在定时器时间到达后回调函数是依次被执行的。
- 定时器任务的执行时间决定于:有多少个定时器期满,执行定时器中的回调函数需多少时间。 因为回调函数是由用户提供,它可能很大程度上影响了定时器任务的执行时间。
- 回调函数被执行时会锁调度器,所以我们必须让回调函数尽可能地短,以便其他任务能正常运行。
- (1):调用OSTaskSemPend()函数在一直等待定时器节拍的信号量, 等待到了才运行,那定时器节拍是怎么样运行的呢。