uCOS-ii 软件定时器
最近学习嵌入式操作系统,见过了很多RTOS之后,最本质的东西也就那点东西。无论是FreeRTOS还是μC/OS-II-III、鸿蒙、RT_Thread等等,内核层面的实现机制大同小异。想从最基本的底层原理学习一个OS的设计思想和实现原理。μC/OS-II是最好的学习对象。
μC/OS-II 嵌入式操作系统属于微内核的RTOS,1992年由美国人推出。To date, μC/OS-III已经出现了。无论各种RTOS时怎么变,一些设计思想和实现方法都不会变的。我一直认为OS是一种很有技术和智慧的软件产品,OS主要功能是管理硬件、提供服务。实现机制无非就是一些数据结构和算法。
本文只对μC/OS-II 的软件定时器功能进行讲解,因为有项目用到,后续会对每个部分的功能进行学习。主要通过博客记录一些自己学习的历程和经验,在CSDN分享自己的学习经验和历程。
功能介绍
软件定时器属于OS提供的一种服务和功能,其作用就是定时,到特定的时间时触发相应的操作(一般以回调函数的方式实现)。
μC/OS-II 参考说明如下:
Timers are down counters that perform an when the counter reaches zero. The user action provides the action through a function (or simply ). A callback is a user-declared function that will be called when the timer expires.
Timers are useful in protocol stacks (re-transmission timers, for example), and can also be used to poll I/O devices at predefined intervals.
总觉得英文原版对于理解会更加切意一点,建议大家多去读英文技术手册原版。
定时器的状态切换主要靠内核API实现,这些函数在 os_tmr.c 中可以找的到。
实现原理
Timer属于内核对象,内核以 OS_TMR 表示一个软件定时器,又可叫做定时器控制块。其数据结构如下:
// An highlighted block
// 定时器控制块
typedef struct os_tmr {
INT8U OSTmrType; /* Should be set to OS_TMR_TYPE */
OS_TMR_CALLBACK OSTmrCallback; /* Function to call when timer expires */
void *OSTmrCallbackArg; /* Argument to pass to function when timer expires */
void *OSTmrNext; /* Double link list pointers */
void *OSTmrPrev;
INT32U OSTmrMatch; /* Timer expires when OSTmrTime == OSTmrMatch */
INT32U OSTmrDly; /* Delay time before periodic update starts */
INT32U OSTmrPeriod; /* Period to repeat timer */
#if OS_TMR_CFG_NAME_EN > 0u
INT8U *OSTmrName; /* Name to give the timer */
#endif
INT8U OSTmrOpt; /* Options (see OS_TMR_OPT_xxx) */
INT8U OSTmrState; /* Indicates the state of the timer: */
/* OS_TMR_STATE_UNUSED
/* OS_TMR_STATE_RUNNING */
/* OS_TMR_STATE_STOPPED */
}OS_TMR;
一个定时器大体由 3 部分组成:定时时间,回调函数和属性。当定时时间到了的话,就进行一次回调函数的处理,定时器属性说明定时器是周期性的定时还是只做一次定时。如果用户使能了 OS_TMR_EN,ucosii 会在内部创建一个定时器任务,负责处理各个定时器。这个任务一般应该由硬件定时器的中断函数中调用 OSTmrSignal()去激活。所以从本质上说 os_tmr.c 中的定时器是由一个硬件定时器分化出来的。默认情况下是由 SysTick 中断里通过 OSTimeTickHook()去激活定时器任务的。
μC/OS-II 的软件定时器算法分析
μC/OS-II使用三种数据结构来管理软件定时器,也就是两个结构体数组,一个结构体指针
// μC/OS-II 软件// 以数组的形式静态分配定时器控制块所需的 RAM 空间,并存储所有已建立的定时器控制块。定时器中实现了 3 类链表的维护:
OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; /* Table containing pool of timers /
OS_EXT OS_TMR * OSTmrFreeList; / Pointer to free list of timers */
OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];
typedef struct os_tmr_wheel {
OS_TMR *OSTmrFirst; /* Pointer to first timer in linked list */
INT16U OSTmrEntries;
} OS_TMR_WHEEL;
宏 OS_TMR_CFG_WHEEL_SIZE 定义了 OSTmr-WheelTbl[]数组的大小,同时这个值也
是定时器分组的依据。按照定时器到时值与 OS_TMR_CFG_WHEEL_SIZE 相除的余数进行分组:不同余数的定时器放在不同分组中;相同余数的定时器处在同一组中,由双向链表连接。这样,余数值为 0~OS_TMR_CFG_WHEEL_SIZE-1 的不同定时器控制块,正好分别对应了数组元素 OSTmr-WheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE-1]的不同分组。每次时钟节拍到来时,时钟数 OSTmrTime 值加 1,然后也进行求余操作,只有余数相同的那组定时器才有可能到时,所以只对该组定时器进行判断。这种方法比循环判断所有定时器更高效。随着时钟数的累加,处理的分组也由 0~OS_TMR_CFG_WHE EL_SIZE-1循环。
信号量唤醒定时器管理任务,计算出当前所要处理的分组后,程序遍历该分组中的所有
控制块,将当前 OSTmr-Time 值与定时器控制块中的到时值相比较。若相等(即到时),则调用该定时器到时回调函数;若不相等,则判断该组中下一个定时器控制块。如此操作,直到该分组链表的结尾。定时器管理任务的流程如下图所示。 OS_TMR_CFG_WHEEL_SIZE 的取值推荐为 2 的 N 次方,以便采用移位操作计算余数,缩短处理时间。
仔细理解上图表示的意思, OSTmrWheelTbl[index]表示已经存在的定时器,free指向空闲定时器可用于生成定时器,OSTmrTbl[SIZE]是整个定时器池。
代码实现
// 定时器管理任务
static void OSTmr_Task (void *p_arg)
{
// 0. 开始 临时变量的定义
INT8U err;
for(; ; )
{
// 1.OSTmrSemSignal 触发释放信号量
OSSemPend(OSTmrSemSignal, 0, &err);
// 2.系统调度上锁
OSSchedLock();
// 3.系统节拍计数器加1
OSTmrTime++;
// 4.确定本次到时时要处理的分组
spoke = OSTmrTime % OS_TMR_CFG_WHEEL_SIZE ;
// 5.选定分组
OS_TMR_WHEEL *pspoke = &OSTmrWheelTbl[spoke];
// 6.获得分组要处理的第一个定时器
OS_TMR *ptmr = pspoke->OSTmrFirst;
while (ptmr != (OS_TMR *) 0) //指针有效
{
OS_TMR *ptmr_next = ptmr->OSTmrNext; // 获得下一个定时器
if( OSTmrTime == ptmr->OSTmrMatch) // 判断此定时器是否到时
{
// 移除此定时器
OSTmr_Unlink(ptmr);
if (ptmr->OSTmrOpt == OS_TMR_OPT_PERIODIC) //重新执行
{
OSTmr_Link(ptmr, OS_TMR_LINK_PERIODIC);
}
else
{
ptmr->OSTmrState = OS_TMR_STATE_COMPLETED
}
// 执行到时回调函数
OS_TMR_CALLBACK pfnct = ptmr->OSTmrCallback;
if (pfnct != (OS_TMR_CALLBACK)0)
{
(*pfnct)((void *)ptmr, ptmr->OSTmrCallbackArg);
}
}
ptmr = ptmr_next; //
}
OSSchedUnlock();
}
}
内核是如何管理定时器控制块的?双向链表。对应着就存在链表的几种基本的操作:插入、删除、初始化。链表的插入和删除,前项指针和后向指针的移动,总让人混乱。
// 定时器链表的插入
// 定时器下次到时的 OSTmrTime 值=定时器定时值+当前 OSTmrTime 值新的分组=定时器下次到时的 OSTmrTime 值%OS_TMR_CFG_WHEEL_SIZE
static void OSTmr_Link(OS_TMR *ptmr, INT8U type)
{
/*
插入说明次定时器需要去定时超时处理
*/
OS_TMR *ptmr1;
OS_TMR_WHEEL *pspoke;
INT16U spoke;
ptmr->OSTmrState = OS_TMR_STATE_RUNNING;
if(type == OS_TMR_LINK_PERIODIC) // 周期定时
{
ptmr->OSTmrMatch = ptmr->OSTmrPeriod + OSTmrTime
}
else
{
if(ptmr->OSTmrDly == 0u)
{
ptmr->OSTmrMatch = ptmr->OSTmrPeriod + OSTmrTime;
}
else
{
ptmr->OSTmrMatch = ptmr->OSTmrDly + OSTmrTime;
}
}
// 1.获得届满定时器所在的分组
spoke = ptmr->OSTmrMatch % OS_TMR_CFG_WHEEL_SIZE;
pspoke = &OSTmrWheelTbl[spoke];
if(pspoke->OSTmrFirst == (OS_TMR *)0) // 次分组空
{
pspoke->OSTmrFirst = ptmr;
ptmr->OSTmrNext = (OS_TMR *)0;
pspoke->OSTmrEntries = 1u;
}
else // 链表指针移动
{
ptmr1 = pspoke->OSTmrFirst;
pspoke->OSTmrFirst = ptmr;
ptmr->OSTmrNext = (void *)ptmr1;
ptmr1->OSTmrPrev = (void *)ptmr;
pspoke->OSTmrEntries++;
}
ptmr->OSTmrPrev = (void *)0; // ptmr处于当前最新的第一个位置
}
定时器链表的删除
// 定时器链表的删除
static void OSTmr_Unlink (OS_TMR *ptmr)
{
/*
ptmr1->OSTmrPrev =
ptmr1->OSTmrNext = ptmr->prev;
ptmr2->OSTmrPrev = ptmr->next
ptmr2->Next =
*/
OS_TMR *ptmr1;
OS_TMR *ptmr2;
OS_TMR_WHEEL *pspoke;
INT16U spoke;
spoke = (INT16U)(ptmr->OSTmrMatch % OS_TMR_CFG_WHEEL_SIZE);
pspoke = &OSTmrWheelTbl[spoke]; // 已经存在的定时器轮子pool中处理
if(pspoke->OSTmrFirst == ptmr) // 运气好,第一个就是要删除的
{
ptmr1 = ptmr->OSTmrNext; // 第二个定时器
pspoke->OSTmrFirst = (OS_TMR *)ptmr1;
if (ptmr1 != (OS_TMR *)0)
{
ptmr1->OSTmrPrev = (void *)0; // 此时,之前的第二个定时器变为第一个
}
}
else // 运气不好,ptmr在中间
{
// 1.先断开保存
ptmr1 = (OS_TMR *)ptmr->OSTmrPrev;
ptmr2 = (OS_TMR *)ptmr->OSTmrNext;
// 2.链接
ptmr1->OSTmrNext = ptmr2; // 将 N-1 与 N+1 链接
if(ptmr2 != (OS_TMR *)0)
{
ptmr2->OSTmrPrev = (void *) ptmr1;
}
}
// 更新 ptmr 的状态信息
ptmr->OSTmrState = OS_TMR_STATE_STOPPED;
ptmr->OSTmrNext = (void *)0;
ptmr->OSTmrPrev = (void *)0;
pspoke->OSTmrEntries--;
}
总结
OS提供的服务和功能一般以API函数的形式提供给外界用户,其实现的方式一般就是数据结构和算法的方法。问题是如何构造这些合适的、恰当的数据结构,效率高简便的算法是很重要的方面。每一个软件产品和软件模块都应该有它自己的实现方案和设计思想,代码背后的一些东西才是学习需要探索和挖掘的。
最近在工作中一些项目功能和µC/OS-II提供的功能很相似,就想着能不能用OS来实现,顺便重新学习了一下RTOS。把基本原理搞懂,工作、做项目才能安心的使用。