快速回顾
μC/OS-Ⅱ中的多任务
μC/OS-Ⅱ源码学习(1)---多任务系统的实现
μC/OS-Ⅱ源码学习(2)---多任务系统的实现(下)
μC/OS-Ⅱ源码学习(3)---事件模型
μC/OS-Ⅱ源码学习(4)---信号量
μC/OS-Ⅱ源码学习(5)---消息队列
μC/OS-Ⅱ源码学习(6)---事件标志组
本文进一步解析μC/OS-Ⅱ中,软件定时器的函数源码。
μC/OSⅡ中的定时器模型
软件定时器并不属于事件系统,是一类特殊的模型,有自己独特的执行逻辑。更具体来说,除了手动创建、启动和销毁,软件定时器的其它生命周期过程无需用户操作,更不与其它任务相关联,它和定时器任务共同构成了软件定时器系统。在μC/OSⅡ中,软件定时器的运行方式是“车轮”式的,将时间线周期性地均匀缠绕在“车轮”上,好比钟表周期性走时一样,每一根车辐都对应一个时间刻度,进而可以将不同的定时器通过到期时间归属到对应的时刻上,来减少对大量定时器的遍历,提高效率。
相关结构和类型
和软件定时器相关的变量如下:
//ucos_ii.h
OS_EXT INT16U OSTmrFree; /* 剩余可用的空白定时器 */
OS_EXT INT16U OSTmrUsed; /* 已使用的定时器数量 */
OS_EXT INT32U OSTmrTime; /* 当前时间 */OS_EXT OS_EVENT *OSTmrSem; /* 操作定时器的权限 */
OS_EXT OS_EVENT *OSTmrSemSignal; /* 当定时器更新(到期)时,用于提醒定时器任务运行的信号量 */OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; /* 软件定时器数组 */
OS_EXT OS_TMR *OSTmrFreeList; /* 空白软件定时器链表 */
OS_EXT OS_STK OSTmrTaskStk[OS_TASK_TMR_STK_SIZE]; //软件定时器堆栈大小OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]; //时间车轮数组,每一个成员代表一个时间刻度
OS_TMR_CFG_MAX表示定时器的最大数量,OS_TMR_CFG_WHEEL_SIZE表示时间车轮的车辐数,也即时间刻度数,下文中的“车辐”和“刻度”表示一个意思。
两个信号量,OSTmrSem是获取定时器操作权限的信号量,OSTmrSemSignal是滴答中断用来提醒定时器任务执行的信号量。
这里面比较重要的类型就是定时器OS_TMR和时间车轮OS_TMR_WHEEL:
OS_TMR
与任务、事件类似,定时器也是以链表的形式进行遍历检索的(除了存储定时器的原始数组),且是双向链表的形式,方便在链表中进行插入和删除元素。
//ucos_ii.h
typedef struct os_tmr {INT8U OSTmrType; /* 初始化后被设置位OS_TMR_TYPE(其它类型无效) */OS_TMR_CALLBACK OSTmrCallback; /* 定时器回调函数 */void *OSTmrCallbackArg; /* 需要传给回调函数的参数 */void *OSTmrNext; /* 和OSTmrPrev共同构成双向链表指针 */void *OSTmrPrev;INT32U OSTmrMatch; /* 到期时间,即当OSTmrTime=OSTmrMatch时定时器到期 */INT32U OSTmrDly; /* 开启定时器前的延时 */INT32U OSTmrPeriod; /* 定时器周期 */
#if OS_TMR_CFG_NAME_EN > 0uINT8U *OSTmrName; /* 定时器名称 */
#endifINT8U OSTmrOpt; /* 选项(如OS_TMR_OPT_xxx) */INT8U OSTmrState; /* 定时器状态,有四种 */
} OS_TMR;
里面有两个成员可以进一步解析,一个是选项OSTmrOpt:
//ucos_ii.h
#define OS_TMR_OPT_NONE 0u /* 无选项 */#define OS_TMR_OPT_ONE_SHOT 1u /* 单次定时器,不会自动重启 */
#define OS_TMR_OPT_PERIODIC 2u /* 周期性定时器,到期后自动重启 */#define OS_TMR_OPT_CALLBACK 3u /* OSTmrStop() option to call 'callback' w/ timer arg. */
#define OS_TMR_OPT_CALLBACK_ARG 4u /* OSTmrStop() option to call 'callback' w/ new arg. */
另一个是定时器状态OSTmrState:
//ucos_ii.h
#define OS_TMR_STATE_UNUSED 0u //未使用的(未初始化)
#define OS_TMR_STATE_STOPPED 1u //已初始化但未启动的定时器
#define OS_TMR_STATE_COMPLETED 2u //已经结束的定时器
#define OS_TMR_STATE_RUNNING 3u //正在运行的定时器
OS_TMR_WHEEL
//ucos_ii.h
typedef struct os_tmr_wheel {OS_TMR *OSTmrFirst; /* 指向定时器的指针 */INT16U OSTmrEntries; //该车轮片剩余的定时器
} OS_TMR_WHEEL;
OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]包含了所有的刻度(详见上一节的图解模型),里面的每个对象都对应一个刻度(OS_TMR_WHEEL类型),刻度上会挂载用户所需的定时器(OSTmrFirst链表)。系统会根据定时器到期时间自动挂载到对应的刻度上。
软件定时器初始化
在OSInit()中,对系统的软件定时器进行了结构初始化OSTmr_Init():将原始的定时器数组按顺序相连,形成一条空白链表,同时进行白板初始化。
//os_tmr.c
void OSTmr_Init (void)
{
#if OS_EVENT_NAME_EN > 0uINT8U err;
#endifINT16U ix;INT16U ix_next;OS_TMR *ptmr1;OS_TMR *ptmr2;OS_MemClr((INT8U *)&OSTmrTbl[0], sizeof(OSTmrTbl)); /* 清除定时器数组 */OS_MemClr((INT8U *)&OSTmrWheelTbl[0], sizeof(OSTmrWheelTbl)); /* Clear the timer wheel */for (ix = 0u; ix < (OS_TMR_CFG_MAX - 1u); ix++) { /* 遍历定时器数组对每个定时器进行初始化,并用指针连成链表 */ix_next = ix + 1u;ptmr1 = &OSTmrTbl[ix];ptmr2 = &OSTmrTbl[ix_next];ptmr1->OSTmrType = OS_TMR_TYPE;ptmr1->OSTmrState = OS_TMR_STATE_UNUSED;ptmr1->OSTmrNext = (void *)ptmr2; /* OSTmrNext指针指向下一个定时器 */
#if OS_TMR_CFG_NAME_EN > 0uptmr1->OSTmrName = (INT8U *)(void *)"?";
#endif}ptmr1 = &OSTmrTbl[ix];ptmr1->OSTmrType = OS_TMR_TYPE;ptmr1->OSTmrState = OS_TMR_STATE_UNUSED;ptmr1->OSTmrNext = (void *)0; /* 最后一个定时器的下一个为空 */
#if OS_TMR_CFG_NAME_EN > 0uptmr1->OSTmrName = (INT8U *)(void *)"?";
#endifOSTmrTime = 0u;OSTmrUsed = 0u;OSTmrFree = OS_TMR_CFG_MAX;OSTmrFreeList = &OSTmrTbl[0]; //链表头指向数组首,正式形成OSTmrFreeList链表OSTmrSem = OSSemCreate(1u); //创建定时器操作权限信号量,初始值为1(第一次操作必定要成功)OSTmrSemSignal = OSSemCreate(0u); //创建定时器提醒信号量,初始值为0(等待Tick中段释放)#if OS_EVENT_NAME_EN > 0uOSEventNameSet(OSTmrSem, (INT8U *)(void *)"uC/OS-II TmrLock", &err);OSEventNameSet(OSTmrSemSignal, (INT8U *)(void *)"uC/OS-II TmrSignal", &err);
#endifOSTmr_InitTask(); //初始化定时器任务
}
软件定时器任务
在μC/OSⅡ中,软件定时器的运行离不开定时器任务的管理,这是一个特殊的任务,需要使用时,还要明确指定定时器任务的优先级,不能与其它任务优先级相冲突:
//用户自定义定时器任务优先级
#define OS_TASK_TMR_PRIO 10
在上一节定时器初始化函数的最后,还对定时器任务进行了初始化OSTmr_InitTask():
//os_tmr.c
static void OSTmr_InitTask (void)
{
#if OS_TASK_NAME_EN > 0uINT8U err;
#endif#if OS_TASK_CREATE_EXT_EN > 0u //使用OSTaskCreateExt创建任务#if OS_STK_GROWTH == 1u //堆栈增长方向,由高到地(void)OSTaskCreateExt(OSTmr_Task,(void *)0, /* No arguments passed to OSTmrTask() */&OSTmrTaskStk[OS_TASK_TMR_STK_SIZE - 1u], /* Set Top-Of-Stack */OS_TASK_TMR_PRIO,OS_TASK_TMR_ID,&OSTmrTaskStk[0], /* Set Bottom-Of-Stack */OS_TASK_TMR_STK_SIZE,(void *)0, /* No TCB extension */OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); /* Enable stack checking + clear stack */#else(void)OSTaskCreateExt(OSTmr_Task,(void *)0, /* No arguments passed to OSTmrTask() */&OSTmrTaskStk[0], /* Set Top-Of-Stack */OS_TASK_TMR_PRIO,OS_TASK_TMR_ID,&OSTmrTaskStk[OS_TASK_TMR_STK_SIZE - 1u], /* Set Bottom-Of-Stack */OS_TASK_TMR_STK_SIZE,(void *)0, /* No TCB extension */OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); /* Enable stack checking + clear stack */#endif
#else#if OS_STK_GROWTH == 1u(void)OSTaskCreate(OSTmr_Task,(void *)0,&OSTmrTaskStk[OS_TASK_TMR_STK_SIZE - 1u],OS_TASK_TMR_PRIO);#else(void)OSTaskCreate(OSTmr_Task,(void *)0,&OSTmrTaskStk[0],OS_TASK_TMR_PRIO);#endif
#endif#if OS_TASK_NAME_EN > 0uOSTaskNameSet(OS_TASK_TMR_PRIO, (INT8U *)(void *)"uC/OS-II Tmr", &err);
#endif
}
直接看定时器任务OSTmr_Task():该任务会周期性获得OSTmrSemSignal信号量并执行,主要功能是检索当前所在时间车轮的刻度,并对该刻度上挂载的软件定时器对象进行遍历,检查是否到期,如果到期就执行对应的回调函数,并根据定时器配置决定是否需要再次挂载。
//os_tmr.c
static void OSTmr_Task (void *p_arg)
{INT8U err;OS_TMR *ptmr;OS_TMR *ptmr_next;OS_TMR_CALLBACK pfnct;OS_TMR_WHEEL *pspoke;INT16U spoke;p_arg = p_arg; /* 引用一下传参防止编译告警 */for (;;) {OSSemPend(OSTmrSemSignal, 0u, &err); /* 等待定时器信号量提醒时间到期 */OSSchedLock(); //给调度器上锁OSTmrTime++; /* 当前时间+1 */spoke = (INT16U)(OSTmrTime % OS_TMR_CFG_WHEEL_SIZE); /* 定位当前时间所处的车轮片 */pspoke = &OSTmrWheelTbl[spoke]; //取出该片ptmr = pspoke->OSTmrFirst;while (ptmr != (OS_TMR *)0) {ptmr_next = (OS_TMR *)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; /* 标记定时器已完成 */}pfnct = ptmr->OSTmrCallback; /* 获取当前定时器绑定的回调函数 */if (pfnct != (OS_TMR_CALLBACK)0) {(*pfnct)((void *)ptmr, ptmr->OSTmrCallbackArg); //执行回调函数}}ptmr = ptmr_next;}OSSchedUnlock();}
}
其中涉及到两个关键函数:OSTmr_Unlink()和OSTmr_Link(),先看前者:
//os_tmr.c
static void OSTmr_Unlink (OS_TMR *ptmr)
{OS_TMR *ptmr1;OS_TMR *ptmr2;OS_TMR_WHEEL *pspoke;INT16U spoke;spoke = (INT16U)(ptmr->OSTmrMatch % OS_TMR_CFG_WHEEL_SIZE); //查看到期时间属于哪个片段pspoke = &OSTmrWheelTbl[spoke]; //取出该片if (pspoke->OSTmrFirst == ptmr) { /* 目标定时器处于片内首位 */ptmr1 = (OS_TMR *)ptmr->OSTmrNext;pspoke->OSTmrFirst = (OS_TMR *)ptmr1;if (ptmr1 != (OS_TMR *)0) {ptmr1->OSTmrPrev = (void *)0;}} else {ptmr1 = (OS_TMR *)ptmr->OSTmrPrev; /* 目标定时器位于中间某个位置 */ptmr2 = (OS_TMR *)ptmr->OSTmrNext;ptmr1->OSTmrNext = ptmr2;if (ptmr2 != (OS_TMR *)0) {ptmr2->OSTmrPrev = (void *)ptmr1;}}ptmr->OSTmrState = OS_TMR_STATE_STOPPED; //设置为已停止状态ptmr->OSTmrNext = (void *)0;ptmr->OSTmrPrev = (void *)0;pspoke->OSTmrEntries--;
}
再看重新链接函数OSTmr_Link():
//os_tmr.c
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;}}spoke = (INT16U)(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;
}
软件定时器的创建
创建定时器的函数为:
OS_TMR *OSTmrCreate (INT32U dly, INT32U period, INT8U opt, OS_TMR_CALLBACK callback, void *callback_arg, INT8U *pname, INT8U *perr);
配置信息很丰富,dly表示定时器首次调用的延时,period表示周期性定时器的执行周期,可选项opt包括:
//ucos_ii.h
#define OS_TMR_OPT_ONE_SHOT 1u /* 单次定时器,不会自动重启 */
#define OS_TMR_OPT_PERIODIC 2u /* 周期定时器,到期后会自动重启 */
接着来看源码:
//os_tmr.c
OS_TMR *OSTmrCreate (INT32U dly,INT32U period,INT8U opt,OS_TMR_CALLBACK callback,void *callback_arg,INT8U *pname,INT8U *perr)
{OS_TMR *ptmr;#ifdef OS_SAFETY_CRITICALif (perr == (INT8U *)0) {OS_SAFETY_CRITICAL_EXCEPTION();}
#endif#ifdef OS_SAFETY_CRITICAL_IEC61508if (OSSafetyCriticalStartFlag == OS_TRUE) {OS_SAFETY_CRITICAL_EXCEPTION();}
#endif#if OS_ARG_CHK_EN > 0uswitch (opt) { /* 校验选项参数 */case OS_TMR_OPT_PERIODIC:if (period == 0u) { //周期性定时器的周期不能为0*perr = OS_ERR_TMR_INVALID_PERIOD;return ((OS_TMR *)0);}break;case OS_TMR_OPT_ONE_SHOT:if (dly == 0u) { //单次定时器的初次调用延时不能为0(否则就是立即执行,没有意义,不如直接调用函数)*perr = OS_ERR_TMR_INVALID_DLY;return ((OS_TMR *)0);}break;default: //非法选项,返回空*perr = OS_ERR_TMR_INVALID_OPT;return ((OS_TMR *)0);}
#endifif (OSIntNesting > 0u) { /* 不能在中断内调用创建函数 */*perr = OS_ERR_TMR_ISR;return ((OS_TMR *)0);}OSSchedLock(); //给调度器上锁ptmr = OSTmr_Alloc(); /* 从空白定时器链表中取一个定时器 */if (ptmr == (OS_TMR *)0) { //如无可用定时器,则标记错误并返回空OSSchedUnlock();*perr = OS_ERR_TMR_NON_AVAIL;return ((OS_TMR *)0);}/* 填装定时器 */ptmr->OSTmrState = OS_TMR_STATE_STOPPED; /* 未运行状态 */ptmr->OSTmrDly = dly;ptmr->OSTmrPeriod = period;ptmr->OSTmrOpt = opt;ptmr->OSTmrCallback = callback;ptmr->OSTmrCallbackArg = callback_arg;
#if OS_TMR_CFG_NAME_EN > 0uptmr->OSTmrName = pname;
#endifOSSchedUnlock(); //解锁调度器*perr = OS_ERR_NONE;return (ptmr); //返回创建好的定时器对象指针
}
发现了一个盲点,只有定时器任务相关的函数会使用OSSchedLock()和OSSchedUnLock()函数进行调度器上锁解锁操作,其它的源码中是不存在这样的操作的,取而代之直接使用临界区OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()囊括一整段代码,那么是为什么呢?
我们知道进入临界区本质上就是关闭中断(除了NMI和硬件FAULT):
//os_cpu_a.asm
OS_CPU_SR_SaveMRS R0, PRIMASK ;读取PRIMASK到R0,R0为返回值 CPSID I ;PRIMASK=1,关中断(NMI和硬件FAULT可以响应)BX LR ;返回OS_CPU_SR_RestoreMSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数BX LR ;返回
当关闭中断后,相当于执行连续、不可打断的代码。而上锁调度器仅仅是无法切换任务,其使用条件是不如临界区严格的。
//os_core.c
void OSSchedLock (void)
{
#if OS_CRITICAL_METHOD == 3u /* 初始化临界区变量 */OS_CPU_SR cpu_sr = 0u;
#endifif (OSRunning == OS_TRUE) { /* 系统正在运行 */OS_ENTER_CRITICAL();if (OSIntNesting == 0u) { /* 不能在中断中调用 */if (OSLockNesting < 255u) { /* 防止调度器锁上溢 */OSLockNesting++; /* 调度器锁+1 */}}OS_EXIT_CRITICAL();}
}
笔者的理解是,有些对事件和任务的操作是可以在中断中进行的(典型的如Post操作),因此在不希望发生这些事情的场合(如Pend操作),就进入临界区,防止中断内相关操作的干扰。
而定时器的生态链是比较封闭的,不与其它任务和事件相关联,无需担心意外的操作导致出错。此外,定时器有实时性要求(比如0延时立即执行),不希望被切换到其它大型任务,这样就无法保证设定的延时,所以在进行操作前都会对调度器上锁。
软件定时器的运行
使用OSTmrStart()运行对应的定时器,直接分析源码:
//os_tmr.c
BOOLEAN OSTmrStart (OS_TMR *ptmr,INT8U *perr)
{
#ifdef OS_SAFETY_CRITICALif (perr == (INT8U *)0) {OS_SAFETY_CRITICAL_EXCEPTION();}
#endif#if OS_ARG_CHK_EN > 0uif (ptmr == (OS_TMR *)0) {*perr = OS_ERR_TMR_INVALID;return (OS_FALSE);}
#endifif (ptmr->OSTmrType != OS_TMR_TYPE) { /* 验证定时器类型是否正确 */*perr = OS_ERR_TMR_INVALID_TYPE;return (OS_FALSE);}if (OSIntNesting > 0u) { /* 不能在中断中调用 */*perr = OS_ERR_TMR_ISR;return (OS_FALSE);}OSSchedLock();switch (ptmr->OSTmrState) {case OS_TMR_STATE_RUNNING: /* 本来就在运行状态,先脱离再重新链接重启 */OSTmr_Unlink(ptmr); /* 脱离时间车轮 */OSTmr_Link(ptmr, OS_TMR_LINK_DLY); /* 重新链接时间车轮 */OSSchedUnlock();*perr = OS_ERR_NONE;return (OS_TRUE);case OS_TMR_STATE_STOPPED:case OS_TMR_STATE_COMPLETED: /* 原来处于停止或完成状态,直接链接上并重启 */OSTmr_Link(ptmr, OS_TMR_LINK_DLY); /* 链接到时间车轮上 */OSSchedUnlock();*perr = OS_ERR_NONE;return (OS_TRUE);case OS_TMR_STATE_UNUSED: /* 未初始化的定时器,标记错误并返回 */OSSchedUnlock();*perr = OS_ERR_TMR_INACTIVE;return (OS_FALSE);default: //非法选项,标记错误并返回OSSchedUnlock();*perr = OS_ERR_TMR_INVALID_STATE;return (OS_FALSE);}
}
软件定时器的到期
在开启后,如何保证时间到期后能触发呢?这就涉及到滴答中断了。在中断中会OSTimeTickHook()钩子函数,该函数会释放信号量OSTmrSemSignal以便定时器任务OSTmr_Task()执行(回到前面的定时器任务一节)。
//os_cpu_c.c
void OSTimeTickHook (void)
{
#if OS_APP_HOOKS_EN > 0App_TimeTickHook(); //用户定义的回调函数
#endif#if OS_TMR_EN > 0OSTmrCtr++; //系统滴答和定时器检测频率是不同的,用一个计数器来进行同步if (OSTmrCtr >= (OS_TICKS_PER_SEC / OS_TMR_CFG_TICKS_PER_SEC)) {OSTmrCtr = 0;OSTmrSignal(); //释放OSTmrSemSignal信号量,可以继续执行定时器任务}
#endif
}
其中OS_TICKS_PER_SEC表示滴答中断的频率,比如200表示一秒内中断200次。而OS_TMR_CFG_TICKS_PER_SEC表示定时器任务检测的频率,比如40表示一秒内检测40次。二者做个除法,表示滴答中断5次,才进行一次定时器任务,这也符合宏定义本身的定义。