读者可以看出,任务优先级的低三位用于确定任务在总就绪表OSRdyTbl[]中的所在位。接下去的三位用于确定是在OSRdyTbl[]数组的第几个元素。OSMapTbl[]是在ROM中的(见文件OS_CORE.C)屏蔽字,用于限制OSRdyTbl[]数组的元素下标在0到7之间,见表3.1
图3.3μC/OS-Ⅱ就绪表
如果一个任务被删除了,则用程序清单3.6中的代码做求反处理。
程序清单L3.6从就绪表中删除一个任务
if((OSRdyTbl[prio>>3]&=~OSMapTbl[prio&0x07])==0)
OSRdyGrp&=~OSMapTbl[prio>>3];
以上代码将就绪任务表数组OSRdyTbl[]中相应元素的相应位清零,而对于OSRdyGrp,
只有当被删除任务所在任务组中全组任务一个都没有进入就绪态时,才将相应位清零。也
就是说OSRdyTbl[prio>>3]所有的位都是零时,OSRdyGrp的相应位才清零。为了找到那个
进入就绪态的优先级最高的任务,并不需要从OSRdyTbl[0]开始扫描整个就绪任务表,只
需要查另外一张表,即优先级判定表OSUnMapTbl([256])(见文件 OS_CORE.C)。OSRdyTbl[]
中每个字节的8位代表这一组的8个任务哪些进入就绪态了,低位的优先级高于高位。利用
这个字节为下标来查OSUnMapTbl这张表,返回的字节就是该组任务中就绪态任务中优先级
最高的那个任务所在的位置。这个返回值在0到7之间。确定进入就绪态的优先级最高的任
务是用以下代码完成的,如程序清单L3.7所示。
程序清单 L3.7 找出进入就绪态的优先级最高的任务
y=OSUnMapTbl[OSRdyGrp];
x=OSUnMapTbl[OSRdyTbl[y]];
prio=(y<<3)+x;
例如,如果OSRdyGrp的值为二进制01101000,查OSUnMapTbl[OSRdyGrp]得到的值是
3,它相应于OSRdyGrp中的第3位bit3,这里假设最右边的一位是第0位bit0。类似地,
如果OSRdyTbl[3]的值是二进制11100100,则OSUnMapTbl[OSRdyTbc[3]]的值是2,即第2
位。于是任务的优先级Prio就等于26(3*8+2)。利用这个优先级的值。查任务控制块优
先级表OSTCBPrioTbl[],得到指向相应任务的任务控制块OS_TCB的工作就完成了。
3.5 任务调度(TaskScheduling)
μC/OS-Ⅱ总是运行进入就绪态任务中优先级最高的那一个。确定哪个任务优先级最
高,下面该哪个任务运行了的工作是由调度器(Scheduler)完成的。任务级的调度是由函
数OSSched()完成的。中断级的调度是由另一个函数OSIntExt()完成的,这个函数将在以
后描述。OSSched()的代码如程序清单L3.8所示。
程序清单L3.8任务调度器(theTaskScheduler)
voidOSSched(void)
{
INT8Uy;
OS_ENTER_CRITICAL();
if((OSLockNesting|OSIntNesting)==0){(1)
y=OSUnMapTbl[OSRdyGrp];(2)
OSPrioHighRdy=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]]);(2)
if(OSPrioHighRdy!=OSPrioCur){(3)
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];(4)
OSCtxSwCtr++;(5)
OS_TASK_SW();(6)
}
}
OS_EXIT_CRITICAL();
}
μC/OS-Ⅱ任务调度所花的时间是常数,与应用程序中建立的任务数无关。如程序清单
中[L3.8(1)]条件语句的条件不满足,任务调度函数OSSched()将退出,不做任务调度。这
个条件是:如果在中断服务子程序中调用OSSched(),此时中断嵌套层数
OSIntNesting>0,或者由于用户至少调用了一次给任务调度上锁函数OSSchedLock(),使
OSLockNesting>0。如果不是在中断服务子程序调用OSSched(),并且任务调度是允许的,
即没有上锁,则任务调度函数将找出那个进入就绪态且优先级最高的任务[L3.8(2)],进入
就绪态的任务在就绪任务表中有相应的位置位。一旦找到那个优先级最高的任务,
OSSched()检验这个优先级最高的任务是不是当前正在运行的任务,以此来避免不必要的任
务调度[L3.8(3)]。注意,在μC/OS中曾经是先得到OSTCBHighRdy然后和OSTCBCur做比
较。因为这个比较是两个指针型变量的比较,在8位和一些16位微处理器中这种比较相对
较慢。而在μC/OS-Ⅱ中是两个整数的比较。并且,除非用户实际需要做任务切换,在查任
务控制块优先级表OSTCBPrioTbl[]时,不需要用指针变量来查OSTCBHighRdy。综合这两项
改进,即用整数比较代替指针的比较和当需要任务切换时再查表,使得μC/OS-Ⅱ比μC/OS
在8位和一些16位微处理器上要更快一些。
为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是
通过将以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来
实现的[L3.8(4)]。接着,统计计数器OSCtxSwCtr加1,以跟踪任务切换次数[L3.8(5)]。
最后宏调用OS_TASK_SW()来完成实际上的任务切换[L3.8(6)]。
任务切换很简单,由以下两步完成,将被挂起任务的微处理器寄存器推入堆栈,然后
将较高优先级的任务的寄存器值从栈中恢复到寄存器中。在μC/OS-Ⅱ中,就绪任务的栈结
构总是看起来跟刚刚发生过中断一样,所有微处理器的寄存器都保存在栈中。换句话说,
μC/OS-Ⅱ运行就绪态的任务所要做的一切,只是恢复所有的CPU寄存器并运行中断返回指
令。为了做任务切换,运行OS_TASK_SW(),人为模仿了一次中断。多数微处理器有软中断
指令或者陷阱指令TRAP来实现上述操作。中断服务子程序或陷阱处理(Traphardler),
也称作事故处理(exceptionhandler),必须提供中断向量给汇编语言函数OSCtxSw()。
OSCtxSw()除了需要OS_TCBHighRdy指向即将被挂起的任务,还需要让当前任务控制块
OSTCBCur指向即将被挂起的任务,参见第8章,移植μC/OS-Ⅱ,有关于OSCtxSw()的更详
尽的解释。
OSSched()的所有代码都属临界段代码。在寻找进入就绪态的优先级最高的任务过程
中,为防止中断服务子程序把一个或几个任务的就绪位置位,中断是被关掉的。为缩短切
换时间,OSSched()全部代码都可以用汇编语言写。为增加可读性,可移植性和将汇编语言
代码最少化,OSSched()是用C写的。
3.6 给调度器上锁和开锁(LockingandUnLockingtheScheduler)
给调度器上锁函数OSSchedlock()(程序清单L3.9)用于禁止任务调度,直到任务完
成后调用给调度器开锁函数OSSchedUnlock()为止,(程序清单L3.10)。调用
OSSchedlock()的任务保持对CPU的控制权,尽管有个优先级更高的任务进入了就绪态。然
而,此时中断是可以被识别的,中断服务也能得到(假设中断是开着的)。OSSchedlock()
和OSSchedUnlock()必须成对使用。变量OSLockNesting跟踪OSSchedLock()函数被调用的
次数,以允许嵌套的函数包含临界段代码,这段代码其它任务不得干预。μC/OS-Ⅱ允许嵌
套深度达255层。当OSLockNesting等于零时,调度重新得到允许。函数OSSchedLock()和
OSSchedUnlock()的使用要非常谨慎,因为它们影响μC/OS-Ⅱ对任务的正常管理。
当OSLockNesting减到零的时候,OSSchedUnlock()调用OSSched[L3.10(2)]。
OSSchedUnlock()是被某任务调用的,在调度器上锁的期间,可能有什么事件发生了并使一
个更高优先级的任务进入就绪态。
调用OSSchedLock()以后,用户的应用程序不得使用任何能将现行任务挂起的系统调用。也就是说,用户程序不得调用OSMboxPend()、OSQPend()、OSSemPend()、OSTaskSuspend
(OS_PR1O_SELF)、OSTimeDly()或OSTimeDlyHMSM(),直到OSLockNesting回零为止。因为调度器上了锁,用户就锁住了系统,任何其它任务都不能运行。
当低优先级的任务要发消息给多任务的邮箱、消息队列、信号量时(见第6章任务间通讯和同步),用户不希望高优先级的任务在邮箱、队列和信号量没有得到消息之前就取得了CPU的控制权,此时,用户可以使用禁止调度器函数。
程序清单 L3.9 给调度器上锁
voidOSSchedLock(void)
{
if(OSRunning==TRUE){
OS_ENTER_CRITICAL();
OSLockNesting++;
OS_EXIT_CRITICAL();
}
}
程序清单L3.10给调度器开锁.
voidOSSchedUnlock(void)
{
if(OSRunning==TRUE){
OS_ENTER_CRITICAL();
if(OSLockNesting>0){
OSLockNesting--;
if((OSLockNesting|OSIntNesting)==0){(1)
OS_EXIT_CRITICAL();
OSSched();(2)
}else{
OS_EXIT_CRITICAL();
}
}else{
OS_EXIT_CRITICAL();
}
}
}
3.7 空闲任务(IdleTask)
μC/OS-Ⅱ总是建立一个空闲任务,这个任务在没有其它任务进入就绪态时投入运行。这个空闲任务[OSTaskIdle()]永远设为最低优先级,即OS_LOWEST_PRI0。空闲任务OSTaskIdle()什么也不做,只是在不停地给一个32位的名叫OSIdleCtr的计数器加1,统计任务(见3.08节,统计任务)使用这个计数器以确定现行应用软件实际消耗的CPU时间。程序清单L3.11是空闲任务的代码。在计数器加1前后,中断是先关掉再开启的,因为8位以及大多数16位微处理器的32位加1需要多条指令,要防止高优先级的任务或中断服务子程序从中打入。空闲任务不可能被应用软件删除。
程序清单L3.11μC/OS-Ⅱ的空闲任务.
voidOSTaskIdle(void*pdata)
{
pdata=pdata;
for(;;){
OS_ENTER_CRITICAL();
OSIdleCtr++;
OS_EXIT_CRITICAL();
}
}
3.8 统计任务
μC/OS-Ⅱ有一个提供运行时间统计的任务。这个任务叫做OSTaskStat(),如果用户将系统定义常数OS_TASK_STAT_EN(见文件OS_CFG.H)设为1,这个任务就会建立。一旦得到了允许,OSTaskStat()每秒钟运行一次(见文件OS_CORE.C),计算当前的CPU利用率。换句话说,OSTaskStat()告诉用户应用程序使用了多少CPU时间,用百分比表示,这个值放在一个有符号8位整数OSCPUsage中,精读度是1个百分点。
如果用户应用程序打算使用统计任务,用户必须在初始化时建立一个唯一的任务,在这个任务中调用OSStatInit()(见文件OS_CORE.C)。换句话说,在调用系统启动函数OSStart()之前,用户初始代码必须先建立一个任务,在这个任务中调用系统统计初始化函数OSStatInit(),然后再建立应用程序中的其它任务。程序清单L3.12是统计任务的示意性代码。
程序清单L3.12初始化统计任务.
voidmain(void)
{
OSInit();/* 初始化uC/OS-II(1)*/
/* 安装uC/OS-II的任务切换向量 */
/* 创建用户起始任务(为了方便讨论,这里以TaskStart()作为起始任务)(2)*/
OSStart();/* 开始多任务调度 (3)*/
}
voidTaskStart(void*pdata)
{
/* 安装并启动uC/OS-II的时钟节拍 (4)*/
OSStatInit();/* 初始化统计任务 (5)*/
/* 创建用户应用程序任务 */
for(;;){
/* 这里是TaskStart()的代码!*/
}
}
因为用户的应用程序必须先建立一个起始任务[TaskStart()],当主程序main()调用系统启动函数OSStcnt()的时候,μC/OS-Ⅱ只有3个要管理的任务:TaskStart()、OSTaskIdle()和OSTaskStat()。请注意,任务TaskStart()的名称是无所谓的,叫什么名字都可以。因为μC/OS-Ⅱ已经将空闲任务的优先级设为最低,即OS_LOWEST_PR10,统计任务的优先级设为次低,OS_LOWEST_PR10-1。启动任务TaskStart()总是优先级最高的任务。
图F3.4解释初始化统计任务时的流程。用户必须首先调用的是μC/OS-Ⅱ中的系统初始化函数OSInit(),该函数初始化μC/OS-Ⅱ[图F3.4(2)]。有的处理器(例如Motorola的MC68HC11),不需要“设置”中断向量,中断向量已经在ROM中有了。用户必须调用OSTaskCreat()或者OSTaskCreatExt()以建立TaskStart()[图F3.4(3)]。进入多任务的条件准备好了以后,调用系统启动函数OSStart()。这个函数将使 TaskStart()开始执行,因为TaskStart()是优先级最高的任务[图F3.4(4)]]。
图F3.4统计任务的初始化
TaskStart()负责初始化和启动时钟节拍[图F3.4(5)]。在这里启动时钟节拍是必要的,因为用户不会希望在多任务还没有开始时就接收到时钟节拍中断。接下去TaskStart()调用统计初始化函数OSStatInit()[图F3.4(6)]。统计初始化函数OSStatInit()决定在没有其它应用任务运行时,空闲计数器(OSIdleCtr)的计数有多快。奔腾II微处理器以333MHz运行时,加1操作可以使该计数器的值达到每秒15,000,000次。OSIdleCtr的值离32位计数器的溢出极限值4,294,967,296还差得远。微处理器越来越快,用户要注意这里可能会是将来的一个潜在问题。
系统统计初始化任务函数OSStatInit()调用延迟函数OSTimeDly()将自身延时2个时钟节拍以停止自身的运行[图F3.4(7)]。这是为了使OSStatInit()与时钟节拍同步。μC/OS-Ⅱ然后选下一个优先级最高的进入就绪态的任务运行,这恰好是统计任务OSTaskStat()。
读者会在后面读到OSTaskStat()的代码,但粗看一下,OSTaskStat()所要做的第一件事就是查看统计任务就绪标志是否为“假”,如果是的话,也要延时两个时钟节拍[图F3.4(8)]。一定会是这样,因为标志OSStatRdy已被OSInit()函数初始化为“假”,所以
实际上DSTaskStat也将自己推入休眠态(Sleep)两个时钟节拍[图F3.4(9)]。于是任务切换到空闲任务,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)]。
OSStarInit()将统计任务就绪标志OSStatRdy设为“真”[图F3.4(16)],以此来允许两个时钟节拍以后OSTaskStat()开始计算CPU的利用率。
统计任务的初始化函数OSStatInit()的代码如程序清单L3.13所示。
程序清单L3.13统计任务的初始化.
voidOSStatInit(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]表达式 Needtotypesettheequation.
一旦上述计算完成,OSTaskStat()调用任务统计外界接入函数OSTaskStatHook()[L3.14(3)],这是一个用户可定义的函数,这个函数能使统计任务得到扩展。这样,用户可以计算并显示所有任务总的执行时间,每个任务执行时间的百分比以及其它信息(参见1.09节例3)。
程序清单L3.14统计任务
voidOSTaskStat(void*pdata)
{
INT32Urun;
INT8Susage;
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;
}elseif(usage<0){
OSCPUUsage=0;
}else{
OSCPUUsage=usage;
}
}else{
OSCPUUsage=0;
}
OSTaskStatHook();(3)
OSTimeDly(OS_TICKS_PER_SEC);
}
}
3.9 μC/OS中的中断处理
μC/OS中,中断服务子程序要用汇编语言来写。然而,如果用户使用的C语言编译器支持在线汇编语言的话,用户可以直接将中断服务子程序代码放在C语言的程序文件中。中断服务子程序的示意码如程序清单L3.15所示。
程序清单L3.15μC/OS-II中的中断服务子程序.
用户中断服务子程序:
保存全部CPU寄存器;(1)
调用OSIntEnter或OSIntNesting直接加1; (2)
执行用户代码做中断服务;(3)
调用OSIntExit(); (4)
执行中断返回指令; (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()之前要先清中断源,否则,中断将连续反复打入,用户应用程序就会崩溃!