闲任务,OSTaskIdle()开始运行,这是唯一一个就绪态任务了。CPU处在空闲任务OSTaskIdle中,直到TaskStart()的延迟两个时钟节拍完成[图3.4(10)]。两个时钟节拍之后,TaskStart()恢复运行[图F3.4(11)]。 在执行OSStartInit()时,空闲计数器OSIdleCtr被清零[图F3.4(12)]。然后,OSStatInit()将自身延时整整一秒[图F3.4(13)]。因为没有其它进入就绪态的任务,OSTaskIdle()又获得了CPU的控制权[图F3.4(14)]。一秒钟以后,TaskStart()继续运行,还是在OSStatInit()中,空闲计数器将1秒钟内计数的值存入空闲计数器最大值OSIdleCtrMax中[图F3.4(15)]。< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
OSStarInit()将统计任务就绪标志OSStatRdy设为“真”[图F3.4(16)],以此来允许两个时钟节拍以后OSTaskStat()开始计算CPU的利用率。
统计任务的初始化函数OSStatInit()的代码如程序清单 L3.13所示。
程序清单 L3.13 统计任务的初始化. |
void OSStatInit (void) |
{ |
OSTimeDly(2); |
OS_ENTER_CRITICAL(); |
OSIdleCtr = 0L; |
OS_EXIT_CRITICAL(); |
OSTimeDly(OS_TICKS_PER_SEC); |
OS_ENTER_CRITICAL(); |
OSIdleCtrMax = OSIdleCtr; |
OSStatRdy = TRUE; |
OS_EXIT_CRITICAL(); |
} |
统计任务OSStat()的代码程序清单L3.14所示。在前面一段中,已经讨论了为什么要等待统计任务就绪标志OSStatRdy[L3.14(1)]。这个任务每秒执行一次,以确定所有应用程序中的任务消耗了多少CPU时间。当用户的应用程序代码加入以后,运行空闲任务的CPU时间就少了,OSIdleCtr就不会像原来什么任务都不运行时有那么多计数。要知道,OSIdleCtr的最大计数值是OSStatInit()在初始化时保存在计数器最大值OSIdleCtrMax中的。CPU利用率(表达式[3.1])是保存在变量OSCPUsage[L3.14(2)]中的:
[3.1]表达式 Need to typeset the equation.
一旦上述计算完成,OSTaskStat()调用任务统计外界接入函数OSTaskStatHook() [L3.14(3)],这是一个用户可定义的函数,这个函数能使统计任务得到扩展。这样,用户可以计算并显示所有任务总的执行时间,每个任务执行时间的百分比以及其它信息(参见1.09节例3)。
程序清单 L3.14 统计任务 |
void OSTaskStat (void *pdata) |
{ |
INT32U run; |
INT8S usage; |
|
|
pdata = pdata; |
while (OSStatRdy == FALSE) { (1) |
OSTimeDly(2 * OS_TICKS_PER_SEC); |
} |
for (;;) { |
OS_ENTER_CRITICAL(); |
OSIdleCtrRun = OSIdleCtr; |
run = OSIdleCtr; |
OSIdleCtr = 0L; |
OS_EXIT_CRITICAL(); |
if (OSIdleCtrMax > 0L) { |
usage = (INT8S)(100L - 100L * run / OSIdleCtrMax); (2) |
if (usage > 100) { |
OSCPUUsage = 100; |
} else if (usage < 0) { |
OSCPUUsage = 0; |
} else { |
OSCPUUsage = usage; |
} |
} else { |
OSCPUUsage = 0; |
} |
OSTaskStatHook(); (3) |
OSTimeDly(OS_TICKS_PER_SEC); |
} |
} |
3.0 μC/OS中的中断处理
μC/OS中,中断服务子程序要用汇编语言来写。然而,如果用户使用的C语言编译器支持在线汇编语言的话,用户可以直接将中断服务子程序代码放在C语言的程序文件中。中断服务子程序的示意码如程序清单L3.15所示。
程序清单 L3.15 μC/OS-II中的中断服务子程序. |
用户中断服务子程序: |
保存全部CPU寄存器; (1) |
调用OSIntEnter或OSIntNesting直接加1; (2) |
执行用户代码做中断服务; (3) |
调用OSIntExit(); (4) |
恢复所有CPU寄存器; (5) |
执行中断返回指令; (6) |
用户代码应该将全部CPU寄存器推入当前任务栈[L3.15(1)]。注意,有些微处理器,例如Motorola68020(及68020以上的微处理器),做中断服务时使用另外的堆栈。
μC/OS-Ⅱ可以用在这类微处理器中,当任务切换时,寄存器是保存在被中断了的那个任务的栈中的。
μC/OS-Ⅱ需要知道用户在做中断服务,故用户应该调用OSIntEnter(),或者将全程变量OSIntNesting[L3.15(2)]直接加1,如果用户使用的微处理器有存储器直接加1的单条指令的话。如果用户使用的微处理器没有这样的指令,必须先将OSIntNesting读入寄存器,再将寄存器加1,然后再写回到变量OSIatNesting中去,就不如调用OSIatEnter()。OSIntNesting是共享资源。OSIntEnter()把上述三条指令用开中断、关中断保护起来,以保证处理OSIntNesting时的排它性。直接给OSIntNesting加1比调用OSIntEnter()快得多,可能时,直接加1更好。要当心的是,在有些情况下,从OSIntEnter()返回时,会把中断开了。遇到这种情况,在调用OSIntEnter()之前要先清中断源,否则,中断将连续反复打入,用户应用程序就会崩溃!
上述两步完成以后,用户可以开始服务于叫中断的设备了[L3.15(3)]。这一段完全取决于应用。μC/OS-Ⅱ允许中断嵌套,因为μC/OS-Ⅱ跟踪嵌套层数OSIntNesting。然而,为允许中断嵌套,在多数情况下,用户应在开中断之前先清中断源。
调用脱离中断函数OSIntExit()[L3.15(4)]标志着中断服务子程序的终结,OSIntExit()将中断嵌套层数计数器减1。当嵌套计数器减到零时,所有中断,包括嵌套的中断就都完成了,此时μC/OS-Ⅱ要判定有没有优先级较高的任务被中断服务子程序(或任一嵌套的中断)唤醒了。如果有优先级高的任务进入了就绪态,μC/OS-Ⅱ就返回到那个高优先级的任务,OSIntExit()返回到调用点[L3.15(5)]。保存的寄存器的值是在这时恢复的,然后是执行中断返回指令[L3.16(6)]。注意,如果调度被禁止了(OSIntNesting>0),μC/OS-Ⅱ将被返回到被中断了的任务。
以上描述的详细解释如图F3.5所示。中断来到了[F3.5(1)]但还不能被被CPU识别,也许是因为中断被μC/OS-Ⅱ或用户应用程序关了,或者是因为CPU还没执行完当前指令。一旦CPU响应了这个中断[F3.5(2)],CPU的中断向量(至少大多数微处理器是如此)跳转到中断服务子程序[F3.5(3)]。如上所述,中断服务子程序保存CPU寄存器(也叫做 CPU context)[F3.5(4)],一旦做完,用户中断服务子程序通知μC/OS-Ⅱ进入中断服务子程序了,办法是调用OSIntEnter()或者给OSIntNesting直接加1[F3.5(5)]。然后用户中断服务代码开始执行[F3.5(6)]。用户中断服务中做的事要尽可能地少,要把大部分工作留给任务去做。中断服务子程序通知某任务去做事的手段是调用以下函数之一:OSMboxPost(),OSQPost(),OSQPostFront(),OSSemPost()。中断发生并由上述函数发出消息时,接收消息的任务可能是,也可能不是挂起在邮箱、队列或信号量上的任务。用户中断服务完成以后,要调用OSIntExit()[F3.5(7)]。从时序图上可以看出,对被中断了的任务说来,如果没有高优先级的任务被中断服务子程序激活而进入就绪态,OSIntExit()只占用很短的运行时间。进而,在这种情况下,CPU寄存器只是简单地恢复[F3.5(8)]并执行中断返回指令[F3.5(9)]。如果中断服务子程序使一个高优先级的任务进入了就绪态,则OSIntExit()将占用较长的运行时间,因为这时要做任务切换[F3.5(10)]。新任务的寄存器内容要恢复并执行中断返回指令[F3.5(12)]。
< xmlnamespace prefix ="v" ns ="urn:schemas-microsoft-com:vml" />
图3.5 中断服务
进入中断函数OSIntEnter()的代码如程序清单L3.16所示,从中断服务中退出函数OSIntExit()的代码如程序清单L3.17所示。如前所述,OSIntEnter()所做的事是非常少的。
程序清单 L3.16 通知μC/OS-Ⅱ,中断服务子程序开始了. |
void OSIntEnter (void) |
{ |
OS_ENTER_CRITICAL(); |
OSIntNesting++; |
OS_EXIT_CRITICAL(); |
} |
程序清单 L3.17 通知μC/OS-Ⅱ,脱离了中断服务 |
void OSIntExit (void) |
{ |
OS_ENTER_CRITICAL(); (1) |
if ((--OSIntNesting | OSLockNesting) == 0) { (2) |
OSIntExitY = OSUnMapTbl[OSRdyGrp]; (3) |
OSPrioHighRdy = (INT8U)((OSIntExitY << 3) + |
OSUnMapTbl[OSRdyTbl[OSIntExitY]]); |
if (OSPrioHighRdy != OSPrioCur) { |
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; |
OSCtxSwCtr++; |
OSIntCtxSw(); (4) |
} |
} |
OS_EXIT_CRITICAL(); |
} |
OSIntExit()看起来非常像OSSched()。但有三点不同。第一点,OSIntExit()使中断嵌套层数减1[L3.17(2)]而调度函数OSSched()的调度条件是:中断嵌套层数计数器和锁定嵌套计数器(OSLockNesting)二者都必须是零。第二个不同点是,OSRdyTbl[]所需的检索值Y是保存在全程变量OSIntExitY中的[L3.17(3)]。这是为了避免在任务栈中安排局部变量。这个变量在哪儿和中断任务切换函数OSIntCtxSw()有关,(见9.04.03节,中断任务切换函数)。最后一点,如果需要做任务切换,OSIntExit()将调用OSIntCtxSw()[L3.17(4)]而不是调用OS_TASK_SW(),正像在OSSched()函数中那样。
调用中断切换函数OSIntCtxSw()而不调用任务切换函数OS_TASK_SW(),有两个原因,首先是,如程序清单中L3.5(1)和图F3.6(1)所示,一半的工作,即CPU寄存器入栈的工作已经做完了。第二个原因是,在中断服务子程序中调用OSIntExit()时,将返回地址推入了堆栈[L3.15(4)和F3.6(2)]。OSIntExit()中的进入临界段函数OS_ENTER_CRITICAL()或许将CPU的状态字也推入了堆栈L3.7(1)和F3.6(3)。这取决于中断是怎么被关掉的(见第8章移植μC/OS-Ⅱ)。最后,调用OSIntCtxSw()时的返回地址又被推入了堆栈[L3.17(4)和F3.1(4)],除了栈中不相关的部分,当任务挂起时,栈结构应该与μC/OS-Ⅱ所规定的完全一致。OSIntCtxSw()只需要对栈指针做简单的调整,如图F3.6(5)所示。换句话说,调整栈结构要保证所有挂起任务的栈结构看起来是一样的。
图3.6中断中的任务切换函数OSIntCtxSw()调整栈结构
有的微处理器,像Motorola 68HC11中断发生时CPU寄存器是自动入栈的,且要想允许中断嵌套的话,在中断服务子程序中要重新开中断,这可以视作一个优点。确实,如果用户中断服务子程序执行得非常快,用户不需要通知任务自身进入了中断服务,只要不在中断服务期间开中断,也不需要调用OSIntEnter()或OSIntNesting加1。程序清单L3。18中的示意代码表示这种情况。一个任务和这个中断服务子程序通讯的唯一方法是通过全程变量。
程序清单 L3.18 Motorola 68HC11中的中断服务子程序 |
M68HC11_ISR: /* 快中断服务程序,必须禁止中断*/ |
所有寄存器被CPU自动保存; |
执行用户代码以响应中断; |
执行中断返回指令; |
3.1 时钟节拍
μC/OS需要用户提供周期性信号源,用于实现时间延时和确认超时。节拍率应在每秒10次到100次之间,或者说10到100Hz。时钟节拍率越高,系统的额外负荷就越重。时钟节拍的实际频率取决于用户应用程序的精度。时钟节拍源可以是专门的硬件定时器,也可以是来自50/60Hz交流电源的信号。
用户必须在多任务系统启动以后再开启时钟节拍器,也就是在调用OSStart()之后。换句话说,在调用OSStart()之后做的第一件事是初始化定时器中断。通常,容易犯的错误是将允许时钟节拍器中断放在系统初始化函数OSInit()之后,在调启动多任务系统启动函数OSStart()之前,如程序清单L3.19所示。
程序清单 L3.19 启动时钟就节拍器的不正确做法. |
void main(void) |
{ |
. |
. |
OSInit(); /* 初始化uC/OS-II */ |
. |
. |
/* 应用程序初始化代码 ... */ |
/* ... 通过调用OSTaskCreate()创建至少一个任务 */ |
. |
. |
允许时钟节拍(TICKER)中断; /* 千万不要在这里允许时钟节拍中断!!! */ |
. |
. |
OSStart(); /* 开始多任务调度 */ |
} |
这里潜在地危险是,时钟节拍中断有可能在μC/OS-Ⅱ启动第一个任务之前发生,此时μC/OS-Ⅱ是处在一种不确定的状态之中,用户应用程序有可能会崩溃。
μC/OS-Ⅱ中的时钟节拍服务是通过在中断服务子程序中调用OSTimeTick()实现的。时钟节拍中断服从所有前面章节中描述的规则。时钟节拍中断服务子程序的示意代码如程序清单L3.20所示。这段代码必须用汇编语言编写,因为在C语言里不能直接处理CPU的寄存器。
程序清单 L3.20 时钟节拍中断服务子程序的示意代码 |
void OSTickISR(void) |
{ |
保存处理器寄存器的值; |
调用OSIntEnter()或是将OSIntNesting加1; |
调用OSTimeTick(); |
|
调用OSIntExit(); |
恢复处理器寄存器的值; |
执行中断返回指令; |
} |
时钟节拍函数OSTimeTick()的代码如程序清单3.21所示。OSTimtick()以调用可由用户定义的时钟节拍外连函数OSTimTickHook()开始,这个外连函数可以将时钟节拍函数OSTimtick()予以扩展[L3.2(1)]。笔者决定首先调用OSTimTickHook()是打算在时钟节拍中断服务一开始就给用户一个可以做点儿什么的机会,因为用户可能会有一些时间要求苛刻的工作要做。OSTimtick()中量大的工作是给每个用户任务控制块OS_TCB中的时间延时项OSTCBDly减1(如果该项不为零的话)。OSTimTick()从OSTCBList开始,沿着OS_TCB链表做,一直做到空闲任务[L3.21(3)]。当某任务的任务控制块中的时间延时项OSTCBDly减到了零,这个任务就进入了就绪态[L3.21(5)]。而确切被任务挂起的函数OSTaskSuspend()挂起的任务则不会进入就绪态[L3.21(4)]。OSTimTick()的执行时间直接与应用程序中建立了多少个任务成正比。
程序清单 L3.21 时钟节拍函数 OSTimtick() 的一个节拍服务 |
void OSTimeTick (void) |
{ |
OS_TCB *ptcb; |
|
OSTimeTickHook(); (1) |
ptcb = OSTCBList; (2) |
while (ptcb->OSTCBPrio != OS_IDLE_PRIO) { (3) |
OS_ENTER_CRITICAL(); |
if (ptcb->OSTCBDly != 0) { |
if (--ptcb->OSTCBDly == 0) { |
if (!(ptcb->OSTCBStat & OS_STAT_SUSPEND)) { (4) |
OSRdyGrp |= ptcb->OSTCBBitY; (5) |
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; |
} else { |
ptcb->OSTCBDly = 1; |
} |
} |
} |
ptcb = ptcb->OSTCBNext; |
OS_EXIT_CRITICAL(); |
} |
OS_ENTER_CRITICAL(); (6) |
OSTime++; (7) |
OS_EXIT_CRITICAL(); |
} |
OSTimeTick()还通过调用OSTime()[L3.21(7)]累加从开机以来的时间,用的是一个无符号32位变量。注意,在给OSTime加1之前使用了关中断,因为多数微处理器给32位数加1的操作都得使用多条指令。
中断服务子程序似乎就得写这么长,如果用户不喜欢将中断服务程序写这么长,可以从任务级调用OSTimeTick(),如程序清单L3.22所示。要想这么做,得建立一个高于应用程序中所有其它任务优先级的任务。时钟节拍中断服务子程序利用信号量或邮箱发信号给这个高优先级的任务。
程序清单 L3.22 时钟节拍任务 TickTask() 作时钟节拍服务. |
void TickTask (void *pdata) |
{ |
pdata = pdata; |
for (;;) { |
OSMboxPend(...); /* 等待从时钟节拍中断服务程序发来的信号 */ |
OSTimeTick(); |
} |
} |
用户当然需要先建立一个邮箱(初始化成NULL)用于发信号给上述任何告知时钟节拍中断已经发生了(程序清单L3.23)。
程序清单L3.23时钟节拍中断服务函数OSTickISR()做节拍服务。 |
void OSTickISR(void) |
{ |
保存处理器寄存器的值; |
调用OSIntEnter()或是将OSIntNesting加1; |
|
发送一个‘空’消息(例如, (void *)1)到时钟节拍的邮箱; |
|
调用OSIntExit(); |
恢复处理器寄存器的值; |
执行中断返回指令; |
} |
3.2 μC/OS-Ⅱ初始化
在调用μC/OS-Ⅱ的任何其它服务之前,μC/OS-Ⅱ要求用户首先调用系统初始化函数OSIint()。OSIint()初始化μC/OS-Ⅱ所有的变量和数据结构(见OS_CORE.C)。
OSInit()建立空闲任务idle task,这个任务总是处于就绪态的。空闲任务OSTaskIdle()的优先级总是设成最低,即OS_LOWEST_PRIO。如果统计任务允许OS_TASK_STAT_EN和任务建立扩展允许都设为1,则OSInit()还得建立统计任务OSTaskStat()并且让其进入就绪态。OSTaskStat的优先级总是设为OS_LOWEST_PRIO-1。
图F3.7表示调用OSInit()之后,一些μC/OS-Ⅱ变量和数据结构之间的关系。其解释是基于以下假设的:
l 在文件OS_CFG.H中,OS_TASK_STAT_EN是设为1的。
l 在文件OS_CFG.H中,OS_LOWEST_PRIO是设为63的。
l 在文件OS_CFG.H中, 最多任务数OS_MAX_TASKS是设成大于2的。
以上两个任务的任务控制块(OS_TCBs)是用双向链表链接在一起的。OSTCBList指向这个链表的起始处。当建立一个任务时,这个任务总是被放在这个链表的起始处。换句话说,OSTCBList总是指向最后建立的那个任务。链的终点指向空字符NULL(也就是零)。
因为这两个任务都处在就绪态,在就绪任务表OSRdyTbl[]中的相应位是设为1的。还有,因为这两个任务的相应位是在OSRdyTbl[]的同一行上,即属同一组,故OSRdyGrp中只有1位是设为1的。
μC/OS-Ⅱ还初始化了4个空数据结构缓冲区,如图F3.8所示。每个缓冲区都是单向链表,允许μC/OS-Ⅱ从缓冲区中迅速得到或释放一个缓冲区中的元素。注意,空任务控制块在空缓冲区中的数目取决于最多任务数OS_MAX_TASKS,这个最多任务数是在OS_CFG.H文件中定义的。μC/OS-Ⅱ自动安排总的系统任务数OS_N_SYS_TASKS(见文件μC/OS-Ⅱ.H)。控制块OS_TCB的数目也就自动确定了。当然,包括足够的任务控制块分配给统计任务和空闲任务。指向空事件表OSEventFreeList和空队列表OSFreeList的指针将在第6章,任务间通讯与同步中讨论。指向空存储区的指针表OSMemFreeList将在第7章存储管理中讨论。
3.3 μC/OS-Ⅱ的启动
多任务的启动是用户通过调用OSStart()实现的。然而,启动μC/OS-Ⅱ之前,用户至少要建立一个应用任务,如程序清单L3.24所示。
程序清单 L3.24 初始化和启动μC/OS-Ⅱ |
void main (void) |
{ |
OSInit(); /* 初始化uC/OS-II */ |
. |
. |
通过调用OSTaskCreate()或OSTaskCreateExt()创建至少一个任务; |
. |
. |
OSStart(); /* 开始多任务调度!OSStart()永远不会返回 */ |
} |
图3.7 调用OSInit()之后的数据结构
图3.8 空缓冲区
OSStart()的代码如程序清单L3.25所示。当调用OSStart()时,OSStart()从任务就绪表中找出那个用户建立的优先级最高任务的任务控制块[L3.25(1)]。然后,OSStart()调用高优先级就绪任务启动函数OSStartHighRdy()[L3,25(2)],(见汇编语言文件OS_CPU_A.ASM),这个文件与选择的微处理器有关。实质上,函数OSStartHighRdy()是将任务栈中保存的值弹回到CPU寄存器中,然后执行一条中断返回指令,中断返回指令强制执行该任务代码。见9.04.01节,高优先级就绪任务启动函数OSStartHighRdy()。那一节详细介绍对于80x86微处理器是怎么做的。注意,OSStartHighRdy()将永远不返回到OSStart()。
程序清单 L3.25 启动多任务. |
void OSStart (void) |
{ |
INT8U y; |
INT8U x; |
|
if (OSRunning == FALSE) { |
y = OSUnMapTbl[OSRdyGrp]; |
x = OSUnMapTbl[OSRdyTbl[y]]; |
OSPrioHighRdy = (INT8U)((y << 3) + x); |
OSPrioCur = OSPrioHighRdy; |
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; (1) |
OSTCBCur = OSTCBHighRdy; |
OSStartHighRdy(); (2) |
} |
} |
多任务启动以后变量与数据结构中的内容如图F3.9所示。这里笔者假设用户建立的任务优先级为6,注意,OSTaskCtr指出已经建立了3个任务。OSRunning已设为“真”,指出多任务已经开始,OSPrioCur和OSPrioHighRdy存放的是用户应用任务的优先级,OSTCBCur和OSTCBHighRdy二者都指向用户任务的任务控制块。
3.4 获取当前μC/OS-Ⅱ的版本号
应用程序调用OSVersion()[程序清单L3.26]可以得到当前μC/OS-Ⅱ的版本号。OSVersion()函数返回版本号值乘以100。换言之,200表示版本号2.00。
程序清单 L3.26 得到μC/OS-Ⅱ当前版本号 |
INT16U OSVersion (void) |
{ |
return (OS_VERSION); |
} |
为找到μC/OS-Ⅱ的最新版本以及如何做版本升级,用户可以与出版商联系,或者查看μC/OS-Ⅱ得正式网站WWW. uCOS-II.COM
图3.9调用OSStart()以后的变量与数据结构
3.5 OSEvent???()函数
读者或许注意到有4个OS_CORE.C中的函数没有在本章中提到。这4个函数是OSEventWaitListInit(),OSEventTaskRdy(),OSEventTaskWait(),OSEventTO()。这几个函数是放在文件OS_CORE.C中的,而对如何使用这个函数的解释见第6章,任务间的通讯与同步。