第3章 时间管理
第2章介绍了内核是怎么在时钟节拍到来的时候更新节拍列表的,从第2章的内容可以知道,任务通过哈希算法计算之后分成几组,并且每组的排列顺序是按照到期的时间长短来进行排列的。本章介绍任务插入节拍列表的实现过程,在调用延时函数时也会涉及这个过程。
3.1 实例演示
本书所有例程及其他资料都可以在论坛www.bbsxiaolong.com的μC/OS-III版块中下载。
创建另一个使用延时函数和测量延时时间的任务,关键代码如代码清单3-1所示。
代码清单3-1 测量延时时间任务
1 void Task_TS(void *p_arg) 2 { 3 OS_ERR err; 4 CPU_TS ts_start; 5 CPU_TS ts_end; 6 7 (void)p_arg; 8 9 while (1) { 10 //获取延时之前的时间戳 11 ts_start = OS_TS_GET(); 12 13 //延时阻塞1 s 14 OSTimeDlyHMSM(0, 0,1,0,OS_OPT_TIME_HMSM_STRICT,&err); 15 16 //获取延时之后的时间戳并减去延时之前的时间戳 17 ts_end = OS_TS_GET() - ts_start; 18 19 //打印出时间戳测试延时的长度,时间戳的计数频率是72M,由此推出下面的计算 20 printf("\r\n 延时1 s,时间戳测试:%d us,\ 21 即%d ms",ts_end/72,ts_end/72000); 22 23 } 24 }
分别在延时1s的延时函数前后读取时间戳,然后相减得到总共经历的时间戳个数。时间戳的计数频率是72M,即1s会从0计数到72000000,1ms会从0计数到72000,1µs会从0计数到72,打印时的单位转化即从这里来,如图3-1所示。
图3-1 串口打印延时测试结果
从图3-1所示的串口打印的结果来看,精度还是相当高的,延时时间都是偏少的。存在些许误差的原因主要是在插入的时候可能在两个节拍的任何时刻,退出延时一定是在节拍到来处理节拍任务的时候。如图3-2所示,虽然设置延时节拍的个数是5个,但延时的节拍比大部分会小于5个。图中的小黑块表示节拍任务的处理。
图3-2 延时误差示意图
下面看看延时函数是怎么使用的。
延时函数用来阻塞任务,调用这个函数之后任务会让出CPU,OS执行调度,执行就绪列表中优先级最高的任务。这个函数跟平常自己写的延时函数不一样,它不会占用CPU。以下对话可能让你更理解。
任务:“1s后再来我这干活吧。”
CPU:“好,没什么事那我就先去执行别的任务了,1s后如果你的优先级达到最高我再回来。”
将OS_CFG_TIME_DLY_HMSM_EN置1后才能使用OSTimeDlyHMSM。先不看使能也没有关系,因为没有使能,程序编译会报错说函数没有定义,然后返回查看函数的定义由哪个宏定义导致的,然后修改该宏定义。
时间机关的参数有如下几个。
1)hours:小时数;
2)minutes:分钟数;
3)seconds:秒数;
4)milli:毫秒数。
以上参数设置后总时间会自动相加。
opt为延时模式选项,包含以下几个选项。
1)OS_OPT_TIME_DLY:如果设置这个选项,则延时时间是相对的,比如设置1s后。
2)OS_OPT_TIME_TIMEOUT:同上。
3)OS_OPT_TIME_MATCH:延时时间是绝对的,比如当系统运行1s时,系统就开始调用OSStart。
4)OS_OPT_TIME_PERIODIC:周期性延时。与相对延时差不多,但是相对长时间的周期性延时,该周期性延时更准确一些。
OS_OPT_TIME_HMSM_STRICT选项设定的时间范围如下。
❑hours (0...99)
❑minutes (0...59)
❑seconds (0...59)
❑milliseconds (0...999)
OS_OPT_TIME_HMSM_NON_STRIC时间参数的范围如下。
❑hours (0...999)
❑minutes (0...9999)
❑seconds (0...65535)
❑milliseconds (0...4294967295)
前面四个选项之一可以与后面两个选项之一相与。如果没有设置相关参数,则前者默认是OS_OPT_TIME_DLY,后者默认是OS_OPT_TIME_HMSM_STRICT。
p_err指向返回错误类型的指针,主要包含以下几种类型(这里只列举部分)。
1)OS_ERR_TIME_INVALID_HOURS:小时数设置错误;
2)OS_ERR_TIME_INVALID_MINUTES:分钟数设置错误;
3)OS_ERR_TIME_INVALID_SECONDS:秒数设置错误;
4)OS_ERR_TIME_INVALID_MILLISECONDS:毫秒数设置错误;
5)OS_ERR_TIME_ZERO_DLY:时间和设置为0。
这里读者有没有感受到使用系统的便利?在移植和配置好系统后,即使你不懂系统内部到底是怎么运行的,也可以根据说明调用系统函数实现相应的功能。
同OSTimeDlyHMSM函数一样,OSTimeDly函数也可以进行延时,但是OSTimeDly函数设置延时的单位是节拍数。以我们的例程为例,我们设置的时钟节拍数是1000Hz,1个节拍数就是1ms。
用到的参数包含以下几个。
1)dly:延时的节拍数,具体延时的不仅要看节拍的频率,还要看下面的opt选项延时的模式设置为什么。
2)opt:意义同OSTimeDlyHMSM函数前四个参数。
3)p_err:指向返回错误类型的指针,主要有(这里只列举部分):OS_ERR_TIME_ZERO_DLY,用于设置延时节拍数为0或者延时选项为OS_OPT_TIME_MATCH,要延时的那个时间点已经超过现在的时间。
3.2 任务开始延时
接下来看看上面两个延时函数是怎么工作的。OSTimeDly函数的代码如代码清单3-2所示。
代码清单3-2 OSTimeDly函数
1 void OSTimeDly (OS_TICK dly, 2 OS_OPT opt, 3 OS_ERR *p_err) 4 { 5 CPU_SR_ALLOC(); 6 7 8 9 #ifdef OS_SAFETY_CRITICAL 10 if (p_err == (OS_ERR *)0) { 11 OS_SAFETY_CRITICAL_EXCEPTION(); 12 return; 13 } 14 #endif 15 16 //检查是否在中断中调用延时函数 17 #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u 18 //如果中断嵌套大于0,即在中断中调用 19 if (OSIntNestingCtr > (OS_NESTING_CTR)0u) { 20 *p_err = OS_ERR_TIME_DLY_ISR; 21 return; 22 } 23 #endif 24 25 //延时函数后面要阻塞任务,进行调度,首先检查调度器是否被锁住 26 if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0u) { 27 *p_err = OS_ERR_SCHED_LOCKED; 28 return; 29 } 30 31 //检查选项是否是我们规定的几个 32 switch (opt) { 33 case OS_OPT_TIME_DLY: 34 case OS_OPT_TIME_TIMEOUT: 35 case OS_OPT_TIME_PERIODIC: 36 //延时节拍数不能为0 37 if (dly == (OS_TICK)0u) { 38 *p_err = OS_ERR_TIME_ZERO_DLY; 39 return; 40 } 41 break; 42 43 case OS_OPT_TIME_MATCH: 44 break; 45 46 default: 47 *p_err = OS_ERR_OPT_INVALID; 48 return; 49 } 50 51 //进入临界段 52 OS_CRITICAL_ENTER(); 53 //修改任务的状态为延时状态 54 OSTCBCurPtr->TaskState = OS_TASK_STATE_DLY; 55 //将任务根据参数插入节拍列表 56 OS_TickListInsert(OSTCBCurPtr, 57 dly, 58 opt, 59 p_err); 60 61 //如果插入节拍列表的时候发生了错误,则退出 62 if (*p_err != OS_ERR_NONE) { 63 OS_CRITICAL_EXIT_NO_SCHED(); 64 return; 65 } 66 //将任务从就绪列表中脱离 67 OS_RdyListRemove(OSTCBCurPtr); 68 //退出临界段,进行任务调度 69 OS_CRITICAL_EXIT_NO_SCHED(); 70 OSSched(); 71 *p_err = OS_ERR_NONE; 72 }
第32~49行是检测输入的选项参数opt是否符合要求,而且延时的节拍数也不能为0。真正进入操作的时候,任务锁住了调度器,进入临界段。程序主要调用OS_TickListInsert 将当前任务根据延时的情况插入节拍列表,之后检测插入的过程有没有出现错误,如果出现错误就返回。最后将当前任务脱离就绪列表。
代码清单3-3中的OSTimeDlyHMSM跟OSTimeDly前面的操作有些许的差别,主要体现在计算延时节拍数和参数检测上面。后面插入节拍列表,脱离就绪列表等都是差不多的,如代码清单3-3所示。
代码清单3-3 OSTimeDlyHMSM函数
1 #if OS_CFG_TIME_DLY_HMSM_EN > 0u 2 void OSTimeDlyHMSM (CPU_INT16U hours, 3 CPU_INT16U minutes, 4 CPU_INT16U seconds, 5 CPU_INT32U milli, 6 OS_OPT opt, 7 OS_ERR *p_err) 8 { 9 #if OS_CFG_ARG_CHK_EN > 0u 10 CPU_BOOLEAN opt_invalid; 11 CPU_BOOLEAN opt_non_strict; //选项opt中是否严格检查 12 #endif 13 OS_OPT opt_time; //取出关于时间的选项 14 OS_RATE_HZ tick_rate; //存放时钟节拍的频率 15 OS_TICK ticks; //存放计算过程的时钟节拍 16 CPU_SR_ALLOC(); 17 18 19 20 #ifdef OS_SAFETY_CRITICAL 21 if (p_err == (OS_ERR *)0) { 22 OS_SAFETY_CRITICAL_EXCEPTION(); 23 return; 24 } 25 #endif 26 27 //检测是否在中断中调用了延时函数 28 #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u 29 if (OSIntNestingCtr > (OS_NESTING_CTR)0u) { 30 *p_err = OS_ERR_TIME_DLY_ISR; 31 return; 32 } 33 #endif 34 35 //后面任务延时的时候要进行任务调度,调度器不能锁住 36 if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0u) { 37 *p_err = OS_ERR_SCHED_LOCKED; 38 return; 39 } 40 41 //通过&运算取出延时模式选项,全部选项还有参数范围的 42 opt_time = opt & OS_OPT_TIME_MASK; 43 //判断延时模式是否符合要求 44 switch (opt_time) { 45 case OS_OPT_TIME_DLY: 46 case OS_OPT_TIME_TIMEOUT: 47 case OS_OPT_TIME_PERIODIC: 48 //确保延时时间不为0 49 if (milli == (CPU_INT32U)0u) { 50 if (seconds == (CPU_INT16U)0u) { 51 if (minutes == (CPU_INT16U)0u) { 52 if (hours == (CPU_INT16U)0u) { 53 *p_err = OS_ERR_TIME_ZERO_DLY; 54 return; 55 } 56 } 57 } 58 } 59 break; 60 61 case OS_OPT_TIME_MATCH: 62 break; 63 64 default: 65 *p_err = OS_ERR_OPT_INVALID; 66 return; 67 } 68 69 //参数检测 70 #if OS_CFG_ARG_CHK_EN > 0u 71 //想查看除了选项所有的位外,还有没有被设置的位 72 opt_invalid = DEF_BIT_IS_SET_ANY(opt, ~OS_OPT_TIME_OPTS_MASK); 73 if (opt_invalid == DEF_YES) { 74 *p_err = OS_ERR_OPT_INVALID; 75 return; 76 } 77 //查看参数范围选项的设置 78 opt_non_strict = DEF_BIT_IS_SET(opt, OS_OPT_TIME_HMSM_NON_STRICT); 79 if (opt_non_strict != DEF_YES) { //如果设置为OS_OPT_TIME_HMSM_STRICT 80 if (milli > (CPU_INT32U)999u) { 81 *p_err = OS_ERR_TIME_INVALID_MILLISECONDS; 82 return; 83 } 84 if (seconds > (CPU_INT16U)59u) { 85 *p_err = OS_ERR_TIME_INVALID_SECONDS; 86 return; 87 } 88 if (minutes > (CPU_INT16U)59u) { 89 *p_err = OS_ERR_TIME_INVALID_MINUTES; 90 return; 91 } 92 if (hours > (CPU_INT16U)99u) { 93 *p_err = OS_ERR_TIME_INVALID_HOURS; 94 return; 95 } 96 } else { //如果设置为OS_OPT_TIME_HMSM_NON_STRICT 97 if (minutes > (CPU_INT16U)9999u) { 98 *p_err = OS_ERR_TIME_INVALID_MINUTES; 99 return; 100 } 101 if (hours > (CPU_INT16U)999u) { 102 *p_err = OS_ERR_TIME_INVALID_HOURS; 103 return; 104 } 105 } 106 #endif 107 108 109 110 tick_rate = OSCfg_TickRate_Hz; 111 //计算需要延时的节拍数 112 ticks = ((OS_TICK)hours * (OS_TICK)3600u + (OS_TICK)minutes * (OS_ 113 + (tick_rate * ((OS_TICK)milli + (OS_TICK)500u / tick_rate)) 114 115 //保证节拍数不为0 116 if (ticks > (OS_TICK)0u) { 117 OS_CRITICAL_ENTER(); 118 //任务状态为等待状态 119 OSTCBCurPtr->TaskState = OS_TASK_STATE_DLY; 120 //插入节拍列表 121 OS_TickListInsert(OSTCBCurPtr, 122 ticks, 123 opt_time, 124 p_err); 125 if (*p_err != OS_ERR_NONE) { 126 OS_CRITICAL_EXIT_NO_SCHED(); 127 return; TICK)60u + (OS_TICK)seconds) * tick_rate / (OS_TICK)1000u; 128 } 129 //将任务脱离就绪列表 130 OS_RdyListRemove(OSTCBCurPtr); 131 OS_CRITICAL_EXIT_NO_SCHED(); 132 //找到下一个就绪任务进行调度 133 OSSched(); 134 *p_err = OS_ERR_NONE; 135 } else { 136 *p_err = OS_ERR_TIME_ZERO_DLY; 137 } 138 } 139 #endif
前面我们解释过opt,其有两种不同的选项:一种是延时的方式,一种是参数的范围。第38~53行检查的是延时方式和延时时间,所以第38行进行&运算的时候将设置延时方式的其他选项置0,设置延时方式的位保持不变。宏OS_OPT_TIME_MASK的定义如下,是延时方式全部位都置1。每种延时方式的宏都是只有某位置1,且位都不同。取出位之后判断是否是其中的一项。
1 #define OS_OPT_TIME_MASK ((OS_OPT)(OS_OPT_TIME_DLY | \ 2 OS_OPT_TIME_TIMEOUT | \ 3 OS_OPT_TIME_PERIODIC | \ 4 OS_OPT_TIME_MATCH))
第64行的宏DEF_BIT_IS_SET_ANY(val, mask)是检测val是否至少将mask中置1的位其中一位置1,~OS_OPT_TIME_OPTS_MASK只有除那些不是opt的位才置1,所以第64行就是检测那些不是选项的位有没有被置1,有就是输入参数opt出错了。
OS_OPT_TIME_OPTS_MASK的定义如下:
1 #define OS_OPT_TIME_OPTS_MASK (OS_OPT_TIME_DLY | \ 2 OS_OPT_TIME_TIMEOUT | \ 3 OS_OPT_TIME_PERIODIC | \ 4 OS_OPT_TIME_MATCH | \ 5 OS_OPT_TIME_HMSM_NON_STRICT)
第70行检测的是检测opt参数范围的设置情况,并根据两种不同的参数范围对时间参数进行检测。
注:如果想设置一些默认选项,就将其宏定义的相应位定义为0,比如OS_OPT_TIME_DLY和OS_OPT_TIME_HMSM_STRICT这两个选项都是这样子设置的。
第103行将需要延时的时间转化为需要延时的节拍数,有些人可能会纳闷其中的500是怎么来的,为什么要这样做?比如当节拍频率是100Hz的时候,我们需要延时126ms,这时就要考虑这里的误差,因为周期是10ms,怎么也不可能准确延时126ms,这时要么延时120ms,要么延时130ms,相对准确的应该是130ms。这里的500起的作用就是这样,这有点像我们的四舍五入。500是怎么做到“四舍五入”的呢?需要注意的是,最后这项 ((OS_TICK)milli + (OS_TICK)500u / tick_rate)) 是以ms为单位的,如果是1000/tick_rate,即(1/tick_rate)*1000,tick_rate的单位是Hz,很多人可能就知道1000/tick_rate是1个节拍所需的毫秒数,500/tick_rate就是半个节拍所需的毫秒数。最后计算节拍数的时候,借助半个节拍所需的时间和取余运算,本来换算成的节拍数是小于半个的部分还是不变,本来是大于半个节拍的部分就变成整个。像开始讲到的126ms,在100Hz的情况换算的节拍数是12.6个,那么它跟13个比较接近,126ms加上半个节拍5ms的时间为131ms,再取余就是13。如果开始是122ms,在100Hz的情况换成的节拍数是12.2个,显然延时12个节拍会比较准确,加上半个节拍5ms的时间是127ms,再进行取余运算还是12个。最后换算成节拍数的时候(见下面这一行代码),我们还发现为什么是先乘以tick_rate再对1000取余呢?
(tick_rate * ((OS_TICK)milli + (OS_TICK)500u / tick_rate)) /(OS_TICK)1000u
对1000取余是因为((OS_TICK)milli + (OS_TICK)500u / tick_rate))的单位是ms,乘以tick_rate得到节拍数,tick_rate本来的含义就是1s内的节拍个数。为什么是先乘以tick_rate呢?大家可以先比较5*10/12和5/12*10的区别,注意“/”是取余运算,前者得到的是4,后者得到的是0,那才是我们想要的结果。肯定是前者,所以在进行取余运算的时候一定要注意这点。
3.3 任务插入节拍列表
接下来插入节拍列表和脱离就绪列表的过程几乎跟OSTimeDly一模一样,如代码清单3-4所示。
代码清单3-4 OS_TickListInsert函数
1 void OS_TickListInsert (OS_TCB *p_tcb, 2 OS_TICK time, 3 OS_OPT opt, 4 OS_ERR *p_err) 5 { 6 OS_TICK tick_delta; //存放要延时的时刻和当前节拍数的差值 7 OS_TICK tick_next; //存放下一次任务解除等待状态是什么时候 8 OS_TICK_SPOKE *p_spoke; //指向根据余数算出的OSCfg_TickWheel数组元素指针 9 OS_TCB *p_tcb0; 10 OS_TCB *p_tcb1; //与p_tcb0一起用来调整节拍列表时存放任务控制块的指针 11 OS_TICK_SPOKE_IX spoke; //存放哈希列表中的余数 12 13 //首先根据选项计算任务控制块的元素TickCtrMatch和TickRemain 14 15 //如果选项是OS_OPT_TIME_MATCH 16 if (opt == OS_OPT_TIME_MATCH) { 17 tick_delta = time - OSTickCtr - 1u; 18 //如果两个数相差太大 19 if (tick_delta > OS_TICK_TH_RDY) { 20 p_tcb->TickCtrMatch = (OS_TICK )0u; 21 p_tcb->TickRemain = (OS_TICK )0u; 22 p_tcb->TickSpokePtr = (OS_TICK_SPOKE *)0; 23 *p_err = OS_ERR_TIME_ZERO_DLY; 24 return; 25 } 26 //计算好TickCtrMatch和TickRemain 27 p_tcb->TickCtrMatch = time; 28 p_tcb->TickRemain = tick_delta + 1u; 29 30 } else if (time > (OS_TICK)0u) { 31 //周期性延时 32 if (opt == OS_OPT_TIME_PERIODIC) { 33 tick_next = p_tcb->TickCtrPrev + time; 34 tick_delta = tick_next - OSTickCtr - 1u; 35 //将前面两行的等式代入这个不等式 36 if (tick_delta < time) { 37 p_tcb->TickCtrMatch = tick_next; 38 } else { 39 p_tcb->TickCtrMatch = OSTickCtr + time; 40 } 41 //计算好TickCtrMatch和TickRemain 42 p_tcb->TickRemain = p_tcb->TickCtrMatch - OSTickCtr; 43 p_tcb->TickCtrPrev = p_tcb->TickCtrMatch; 44 45 } else { //如果是相对延时 46 p_tcb->TickCtrMatch = OSTickCtr + time; 47 p_tcb->TickRemain = time; 48 } 49 50 } else { //延时的节拍数小于或等于0,返回 51 p_tcb->TickCtrMatch = (OS_TICK )0u; 52 p_tcb->TickRemain = (OS_TICK )0u; 53 p_tcb->TickSpokePtr = (OS_TICK_SPOKE *)0; 54 *p_err = OS_ERR_TIME_ZERO_DLY; 55 return; 56 } 57 58 //插入的时候也是根据哈希算法先算出余数,取出数组元素 59 spoke = (OS_TICK_SPOKE_IX)(p_tcb->TickCtrMatch % OSCfg_TickWheelSize); 60 p_spoke = &OSCfg_TickWheel[spoke]; 61 62 //整个过程就是双向链表的插入过程,早到期的插入双向链表的前面 63 64 //如果数组元素中之前没有元素 65 if (p_spoke->NbrEntries == (OS_OBJ_QTY)0u) { 66 //双向链表没有下一个也没有前一个 67 p_tcb->TickNextPtr = (OS_TCB *)0; 68 p_tcb->TickPrevPtr = (OS_TCB *)0; 69 //指向双向链表的第一个,即为当前插入的 70 p_spoke->FirstPtr = p_tcb; 71 //双向链表的任务控制块个数是1 72 p_spoke->NbrEntries = (OS_OBJ_QTY)1u; 73 74 } else {//如果双向链表插入之前已经有其他的任务控制块 75 76 //取出双向链表中第一个任务控制块 77 p_tcb1 = p_spoke->FirstPtr; 78 while (p_tcb1 != (OS_TCB *)0) { 79 //计算第一个任务控制块剩下的时钟节拍数 80 p_tcb1->TickRemain = p_tcb1->TickCtrMatch 81 - OSTickCtr; 82 //因为插入的位置是要根据剩下的节拍数从小到大进行排序,所以要进行比较 83 //如果插入的任务控制块剩下的时钟节拍数比较多 84 if (p_tcb->TickRemain > p_tcb1->TickRemain) { 85 //如果双向链表上有下一个任务控制块 86 if (p_tcb1->TickNextPtr != (OS_TCB *)0) { 87 //取出下一个任务控制块进行比较 88 p_tcb1 = p_tcb1->TickNextPtr; 89 } else { 90 /*插入的任务控制块剩下的节拍数最多,而且已经比较到最后一个, 91 直接插入最后*/ 92 p_tcb->TickNextPtr = (OS_TCB *)0; 93 p_tcb->TickPrevPtr = p_tcb1; 94 p_tcb1->TickNextPtr = p_tcb; 95 //退出循环 96 p_tcb1 = (OS_TCB *)0; 97 } 98 } else { //插入的任务控制块剩下的节拍数比较小,准备插入前面 99 //如果要插入第一个的前面 100 if (p_tcb1->TickPrevPtr == (OS_TCB *)0) { 101 p_tcb->TickPrevPtr = (OS_TCB *)0; 102 p_tcb->TickNextPtr = p_tcb1; 103 p_tcb1->TickPrevPtr = p_tcb; 104 p_spoke->FirstPtr = p_tcb; 105 } else { //如果不是要插入第一个的前面 106 p_tcb0 = p_tcb1->TickPrevPtr; 107 p_tcb->TickPrevPtr = p_tcb0; 108 p_tcb->TickNextPtr = p_tcb1; 109 p_tcb0->TickNextPtr = p_tcb; 110 p_tcb1->TickPrevPtr = p_tcb; 111 } 112 //退出循环 113 p_tcb1 = (OS_TCB *)0; 114 } 115 } 116 //更新数组双向链表的任务控制块个数 117 p_spoke->NbrEntries++; 118 } 119 //更新数组元素上的最大任务控制块个数 120 if (p_spoke->NbrEntriesMax < p_spoke->NbrEntries) { 121 p_spoke->NbrEntriesMax = p_spoke->NbrEntries; 122 } 123 //任务控制块也指向数组元素指针 124 p_tcb->TickSpokePtr = p_spoke; 125 *p_err = OS_ERR_NONE; 126 }
如果延时选项是绝对延时OS_OPT_TIME_MATCH,意味着现在开始延时直到OSTickCtr等于传入的延时节拍数的时候。OS_TICK_TH_RDY是一个宏,大小是0xffff0000。tick_delta为延时到的那个节拍数减去当前的节拍数,如果这个数大于0xffff0000,以时钟节拍1000Hz为例,延时时间达到49天,一般来说这是不可能的,这种情况更多的是延时到的那个节拍数小于当前的节拍数,这涉及无符号数相减的相关知识。延时到的那个节拍数小于当前的节拍数,则返回错误。
当延时选项是周期性延时OS_OPT_TIME_PERIODIC的时候,任务控制块的元素TickCtrPrev如果小于当前的节拍数加1,则延时到的节拍数就是TickCtrPrev和要延时的节拍数之和,否则就是当前的节拍数和要延时的节拍数之和。分析第31行不等式的时候,可以将第30行和第29行的等式带入,这决定了在周期性延时中不会因系统负担过重而导致延时时间变长。我们来对比下列代码清单3-5中两段除选项不同之外的延时。
代码清单3-5 周期性延时对比相对延时
1 for (i=0; i<10; i++) 2 { 3 OSTimeDly(2,OS_OPT_TIME_PERIODIC,&err); 4 } 5 6 for (i=0; i<10; i++) 7 { 8 OSTimeDly(2,OS_OPT_TIME_DLY,&err); 9 }
如图3-3所示,当系统负担过重的时候,相对延时可能会相差多个节拍,但是周期性延时会进行相应的调整。
如果是相对延时模式,直接将要延时的节拍数加上当前的节拍数就是要延时到的节拍数。
上面根据不同的延时选项计算好了要延时的时间,下面开始将当前任务插入就绪列表。根据前面讲到的哈希算法,首先计算要延时到的节拍数对常数OSCfg_TickWheelSize,然后根据余数取出OSCfg_TickWheel中相应的数组元素。接下来讲解插入过程,大家最好先回顾图2-1及节拍列表的数据结构内容。
图3-3 对比周期性延时和相对延时
第65~72行表示数组元素开始没有任何延时或者超时检测的任务,先将任务控制块组成双向链表的变量TickPrePtr和TickNextPtr都置0,再将数组元素中指向第一个任务控制块的指针指向当前插入的任务控制块,如图3-4所示。这个过程就是双向链表插入第一个的过程。图3-4中的步骤④对应第117行,对节拍列表插入的任务个数加1;步骤⑤对应第96行。不管任务怎么插入节拍列表,最后任务控制块的元素TickSpokePtr都要指向其插入的数组元素OSCfg_TickWheel[spoke],这样方便在删除节点的时候知道这个任务是插入哪个数组元素中。
图3-4 节拍列表插入第一个元素
第74~111行表示元素之前已经有任务控制块,因为后面插入的任务要按照延时到的节拍数顺序插入,如图3-5所示。越快到期的任务就越排在前面。从前面先到期的任务开始一个个取出来放入p_tcb1变量,每取出一个任务控制块p_tcb1,就跟要插入的任务控制的剩下节拍数TickRemain进行比较。比较结果会出现三种情况。
1)如果当前任务剩下的节拍数大于取出任务,而且取出的任务不是最后一个元素,第88行就继续找出下一个任务在循环里进行比较。
2)如果当前任务剩下的节拍数大于取出的任务,且取出任务是双向列表最后一个元素,那么直接插入后面就可。这个过程很简单,第92~94行分别对应图3-5中的①、②、③操作。
图3-5 任务插入节拍列表最后
3)如果当前任务剩下的节拍数小于或等于当前的,那么直接插入比较的那个任务的前面。这个插入过程还可以分为:如果插入在最前面,那么是第101~104行那样的操作,图3-6分解了插入的整个过程;如果插在中间,如第106~110行,图3-7解析了整个过程。
图3-6 任务插入节拍列表最前面
图3-7 任务插入节拍列表中间
插入过程其实就是双向链表的插入过程,请读者自行查看图和代码进行理解。
3.4 任务取消延时
OSTimeDlyResume函数用于取消其他任务的延时。注意这里是其他任务,因为任务一旦处于延时状态,就被剥夺CPU使用权,无法自己取消延时。就像有人被关进监狱,想要出来,就一定要外面的人采取办法解救。如果任务既被延时又被挂起,即任务处于OS_TASK_STATE_DLY_SUSPENDED的状态,虽然可以恢复被延时的状态,但是仍然被挂起,则要除去挂起后任务才能就绪。这有点像一个犯人犯了两条不同的罪,虽然他的律师帮他洗清了其中一条罪,但犯人还是得进监狱。
参数
(1)p_tcb:要被恢复延时的函数。
(2)p_err:指向返回错误类型的指针,主要有以下几种类型。
❑OS_ERR_NONE 没有错误
❑OS_ERR_STATE_INVALID 任务状态不可用,可能是已经被删除
❑OS_ERR_TIME_DLY_RESUME_ISR 在中断中调用恢复函数
❑OS_ERR_TASK_SUSPENDED 任务解除了延时状态,但是仍然被挂起
❑OS_ERR_TIME_NOT_DLY 任务没有在延时
OSTimeDlyResume函数的具体代码如代码清单3-6所示。
代码清单3-6 OSTimeDlyResume函数
1 #if OS_CFG_TIME_DLY_RESUME_EN > 0u 2 void OSTimeDlyResume (OS_TCB *p_tcb, 3 OS_ERR *p_err) 4 { 5 CPU_SR_ALLOC(); 6 7 8 9 #ifdef OS_SAFETY_CRITICAL 10 if (p_err == (OS_ERR *)0) { 11 OS_SAFETY_CRITICAL_EXCEPTION(); 12 return; 13 } 14 #endif 15 16 //不能在中断中调用中断恢复函数 17 #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u 18 if (OSIntNestingCtr > (OS_NESTING_CTR)0u) { 19 *p_err = OS_ERR_TIME_DLY_RESUME_ISR; 20 return; 21 } 22 #endif 23 24 #if OS_CFG_ARG_CHK_EN > 0u 25 if (p_tcb == (OS_TCB *)0) { 26 *p_err = OS_ERR_TASK_NOT_DLY; 27 return; 28 } 29 #endif 30 31 CPU_CRITICAL_ENTER(); 32 //检查任务状态 33 switch (p_tcb->TaskState) { 34 //任务处于就绪状态,退出 35 case OS_TASK_STATE_RDY: 36 CPU_CRITICAL_EXIT(); 37 *p_err = OS_ERR_TASK_NOT_DLY; 38 break; 39 40 //延时状态 41 case OS_TASK_STATE_DLY: 42 OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT(); 43 p_tcb->TaskState = OS_TASK_STATE_RDY; 44 //脱离节拍列表 45 OS_TickListRemove(p_tcb); 46 //插入就绪列表 47 OS_RdyListInsert(p_tcb); 48 OS_CRITICAL_EXIT_NO_SCHED(); 49 *p_err = OS_ERR_NONE; 50 break; 51 52 //等待状态,退出 53 case OS_TASK_STATE_PEND: 54 CPU_CRITICAL_EXIT(); 55 *p_err = OS_ERR_TASK_NOT_DLY; 56 break; 57 58 //有超时限制的等待状态,退出 59 case OS_TASK_STATE_PEND_TIMEOUT: 60 CPU_CRITICAL_EXIT(); 61 *p_err = OS_ERR_TASK_NOT_DLY; 62 break; 63 64 //挂起状态,退出 65 case OS_TASK_STATE_SUSPENDED: 66 CPU_CRITICAL_EXIT(); 67 *p_err = OS_ERR_TASK_NOT_DLY; 68 break; 69 70 //挂起、延时状态,解除延时状态 71 case OS_TASK_STATE_DLY_SUSPENDED: 72 OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT(); 73 p_tcb->TaskState = OS_TASK_STATE_SUSPENDED; 74 OS_TickListRemove(p_tcb); 75 OS_CRITICAL_EXIT_NO_SCHED(); 76 *p_err = OS_ERR_TASK_SUSPENDED; 77 break; 78 79 //等待、挂起状态 80 case OS_TASK_STATE_PEND_SUSPENDED: 81 CPU_CRITICAL_EXIT(); 82 *p_err = OS_ERR_TASK_NOT_DLY; 83 break; 84 85 //有超时限制的等待、挂起状态 86 case OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED: 87 CPU_CRITICAL_EXIT(); 88 *p_err = OS_ERR_TASK_NOT_DLY; 89 break; 90 91 //其他状态,可能是任务已经被删除 92 default: 93 CPU_CRITICAL_EXIT(); 94 *p_err = OS_ERR_STATE_INVALID; 95 break; 96 } 97 //任务调度 98 OSSched(); 99 } 100 #endif
OSTimeDlyResume函数开始判断任务状态时,处理的任务只有OS_TASK_STATE_DLY和OS_TASK_STATE_DLY_SUSPENDED。前者退出节拍列表后进入就绪列表;后者退出节拍列表后继续被挂起,最后进行任务调度,如果任务已经恢复就绪状态,且优先级足够高,就会获得CPU使用权。
3.5 任务脱离节拍列表
任务脱离节拍列表函数OS_TickListRemove的具体代码如代码清单3-7所示。
代码清单3-7 将任务脱离节拍列表函数OS_TickListRemove
1 void OS_TickListRemove (OS_TCB *p_tcb) 2 { 3 OS_TICK_SPOKE *p_spoke; 4 OS_TCB *p_tcb1; 5 OS_TCB *p_tcb2; 6 7 8 //很方便就找到了任务控制块是哪个元素的双向链表 9 p_spoke = p_tcb->TickSpokePtr; 10 if (p_spoke != (OS_TICK_SPOKE *)0) { 11 //清空剩下的个数 12 p_tcb->TickRemain = (OS_TICK)0u; 13 //查看要移除的任务控制块是否在双向链表的最前面 14 if (p_spoke->FirstPtr == p_tcb) { 15 p_tcb1 = (OS_TCB *)p_tcb->TickNextPtr; 16 p_spoke->FirstPtr = p_tcb1; 17 if (p_tcb1 != (OS_TCB *)0) { 18 p_tcb1->TickPrevPtr = (void *)0; 19 } 20 } else { 21 p_tcb1 = p_tcb->TickPrevPtr; 22 p_tcb2 = p_tcb->TickNextPtr; 23 p_tcb1->TickNextPtr = p_tcb2; 24 if (p_tcb2 != (OS_TCB *)0) { 25 p_tcb2->TickPrevPtr = p_tcb1; 26 } 27 } 28 //任务控制块的相关元素都清零 29 p_tcb->TickNextPtr = (OS_TCB *)0; 30 p_tcb->TickPrevPtr = (OS_TCB *)0; 31 p_tcb->TickSpokePtr = (OS_TICK_SPOKE *)0; 32 p_tcb->TickCtrMatch = (OS_TICK )0u; 33 //双向链表任务控制块个数减1 34 p_spoke->NbrEntries--; 35 } 36 }
函数OS_TickListRemove用于将任务脱离节拍列表,这个过程就是双向链表脱离其中一个节点的过程。首先判断要删除的任务控制块是否是节拍列表的第一个任务控制块:如果是,操作顺序如图3-8所示;如果不是双向链表的第一个任务,操作顺序如图3-9所示。
图3-8 任务从双向链表前面脱离
图3-9 任务从双向链表中间或者后面脱离
脱离节拍列表就是双向链表移除节点的过程。
3.6 获取和设置时钟节拍
获取时钟节拍函数OSTimeGet的源码如代码清单3-8所示。
代码清单3-8 OSTimeGet函数
1 OS_TICK OSTimeGet (OS_ERR *p_err) 2 { 3 OS_TICK ticks; 4 CPU_SR_ALLOC(); 5 6 7 8 #ifdef OS_SAFETY_CRITICAL 9 if (p_err == (OS_ERR *)0) { 10 OS_SAFETY_CRITICAL_EXCEPTION(); 11 return ((OS_TICK)0); 12 } 13 #endif 14 15 CPU_CRITICAL_ENTER(); 16 ticks = OSTickCtr; 17 CPU_CRITICAL_EXIT(); 18 *p_err = OS_ERR_NONE; 19 return (ticks); 20 }
设置时钟节拍函数OSTimeGet的源码如代码清单3-9所示。
代码清单3-9 OSTimeSet函数
1 void OSTimeSet (OS_TICK ticks, 2 OS_ERR *p_err) 3 { 4 CPU_SR_ALLOC(); 5 6 7 8 #ifdef OS_SAFETY_CRITICAL 9 if (p_err == (OS_ERR *)0) { 10 OS_SAFETY_CRITICAL_EXCEPTION(); 11 return; 12 } 13 #endif 14 15 CPU_CRITICAL_ENTER(); 16 OSTickCtr = ticks; 17 CPU_CRITICAL_EXIT(); 18 *p_err = OS_ERR_NONE; 19 }
OSTimeGet和OSTimeSet这两个函数使用起来和理解起来是非常容易的,直接调用得到返回值即是节拍数,输入参数即可设置节拍数。函数就是修改全局变量OSTickCtr的值。平常我们也可以直接操作全局变量OSTickCtr的值,但要和函数一样加上临界段保护才可以。
3.7 μC/OS-III全局变量的定义和声明
本节讲解的内容跟时钟节拍没有多大关系。因为OSTickCtr是一个全局变量,所以这里顺便介绍μC/OS-III全局变量的定义和声明。μC/OS-III由很多全局变量来记录各种事情。不知道大家有没有观察到,μC/OS-III中全局变量的定义和声明没有分开,只用了一处,如下:
OS_EXT 数据类型 全局变量;
如果使用MDK编译器,无论是找到定义还是声明的地方,都是一个地方。常规的办法是,无法做到定义一个就可以在多个C文件使用的全局变量。一个变量想要在其他的C文件中同样适用,需要其他的C文件再次声明这个变量或者要包含这个变量声明的.h文件,才能在其他的C文件中一起使用。
首先在μC/OS-III的OS.H文件中可以看到OS_EXT的定义代码段,如代码清单3-10所示。
代码清单3-10 全局变量相关代码段
1 #ifdef OS_GLOBALS 2 #define OS_EXT 3 #else 4 #define OS_EXT extern 5 #endif
这段代码从C语言的规则上看就是,当文件中定义OS_GLOBALS时,我们将OS_EXT表示为空,那么编译器给这个文件夹中每个用OS_EXT修饰的全局变量分配内存空间。为了说明方便,我们用具体指变量OSIntQTaskTCB来说明,在OS.H中有:
OS_EXT OS_TCB OSIntQTaskTCB;
如果其他的文件夹没有定义OS_GLOBALS,就相当于在OSIntQTaskTCB前面加了extern,这样就只是声明了一个全局变量并没有分配空间。
如果仅在一个.C文件中定义OS_GLABALS,其他的C文件不定义OS_GLABALS,并且所有的.C文件都包含(不管是直接还是间接)文件OS.H,那么OSIntQTaskTCB实际上在包含OS_GLABALS的.C文件中就是变量的定义,而其他的C文件只是变量的声明,这样就定义了一个全局变量,而且不会产生重复定义的错误。
注:间接包含H文件,即A如果包含B,B包含C,那么A也同样包含C。
为了更好地理解,我们将以下语句中的OS_EXT删掉,编译会出现什么情况呢?
OS_EXT OS_TCB OSIntQTaskTCB;
没有去掉OS_EXT之前,μC/OS就将全局变量OSIntQTaskTCB的OS.H中定义,在包含OS.H的其他文件中声明。将OS_EXT去掉就变成在包含OS.H的其他文件中,所以会出现重复定义。由于多个C文件包含OS.H,所以重复定义的错误有多个。
如果把上面的语句改成下面的语句呢?
OS_EXT OS_TCB OSIntQTaskTCB=0;
同样地,会出现重复定义的情况,因为extern OS_TCB OSIntQTaskTCB=0;已经不只是声明那么简单,而是定义。
μC/OS主要是利用.h文件的包含和条件编译,让相同的语句以定义变量和声明变量两种不同的情况出现在不同的文件中,这样就顺利地完成全局变量的定义和声明。在平时的编程中,如果需要定义一些全局变量在多个C文件中,则可以采用这种条件编译和.h文件包含的做法。
3.8 总结
本章讲解了延时函数的使用,以及它们的原理。调用延时函数的本质就是,先将任务插入节拍列表,再进行任务切换让其他的任务运行;接着节拍列表就会在时钟节拍到来的时候管理这些任务,并快速处理到期的任务。处理的时候还要检查任务的状态是否跟延时有关,如果任务单纯只是延时状态,那么直接将任务脱离节拍列表后就可以就绪;如果任务不只是延时状态,还有挂起状态,脱离节拍列表后还需要处于挂起状态。任务脱离和插入节拍列表其实是一个非常简单的双向链表的脱离和插入过程,这个过程用到了哈希算法。除了自然到期之外,还可以调用函数OSTimeDlyResume解除任务的延时状态,相当于反悔——“算了,不要让任务XXX再继续延时了”,其中任务XXX是除调用函数OSTimeDlyResume之外的任务。