在μC/OS-II中,有多种方法可以保护任务之间的共享数据和提供任务之间的通讯。在前面的章节中,已经讲到了其中的两种:
一是利用宏OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()来关闭中断和打开中断。当两个任务或者一个任务和一个中断服务子程序共享某些数据时,可以采用这种方法,详见3.00节临界段、8.03.02节 OS_ENTER_CRITICAL() 和OS_EXIT_CRITICAL()及9.03.02节临界段,OS_CPU.H;
二是利用函数OSSchedLock()和OSSchekUnlock()对μC/OS-II中的任务调度函数上锁和开锁。用这种方法也可以实现数据的共享,详见3.06节 给调度器上锁和开锁。
本章将介绍另外三种用于数据共享和任务通讯的方法:信号量、邮箱和消息队列。
图F6.1介绍了任务和中断服务子程序之间是如何进行通讯的。
一个任务或者中断服务子程序可以通过事件控制块ECB(EventCONtrolBlocks)来向另外的任务发信号[F6.1A(1)]。这里,所有的信号都被看成是事件(Event)。这也说明为什么上面把用于通讯的数据结构叫做事件控制块。一个任务还可以等待另一个任务或中断服务子程序给它发送信号[F6.1A(2)]。这里要注意的是,只有任务可以等待事件发生,中断服务子程序是不能这样做的。对于处于等待状态的任务,还可以给它指定一个最长等待时间,以此来防止因为等待的事件没有发生而无限期地等下去。
多个任务可以同时等待同一个事件的发生[F6.1B]。在这种情况下,当该事件发生后,所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态,准备执行。上面讲到的事件,可以是信号量、邮箱或者消息队列等。当事件控制块是一个信号量时,任务可以等待它,也可以给它发送消息。
图6.1事件控制块的使用
6.1 事件控制块ECB
μC/OS-II通过uCOS_II.H中定义的OS_EVENT数据结构来维护一个事件控制块的所有信息[程序清单L6.1],也就是本章开篇讲到的事件控制块ECB。该结构中除了包含了事件本身的定义,如用于信号量的计数器,用于指向邮箱的指针,以及指向消息队列的指针数组等,还定义了等待该事件的所有任务的列表。程序清单L6.1是该数据结构的定义。
程序清单L6.1ECB数据结构
typedefSTruct{
void*OSEventPtr;/*指向消息或者消息队列的指针*/
INT8UOSEventTbl[OS_EVENT_TBL_SIZE];/*等待任务列表*/
INT16UOSEventCnt;/*计数器(当事件是信号量时)*/
INT8UOSEventType;/*时间类型*/
INT8UOSEventGrp;/*等待任务所在的组*/
}OS_EVENT;
.OSEventPtr指针,只有在所定义的事件是邮箱或者消息队列时才使用。 当所定义的事件是邮箱时,它指向一个消息,而当所定义的事件是消息队列时,它指向一个数据结构,详见6.06节消息邮箱和6.07节消息队列。
.OSEventTbl[] 和 .OSEventGrp 很像前面讲到的OSRdyTbl[]和OSRdyGrp,只不过前两者包含的是等待某事件的任务,而后两者包含的是系统中处于就绪状态的任务。(见3.04节就绪表)
.OSEventCnt 当事件是一个信号量时,.OSEventCnt是用于信号量的计数器,(见6.05节信号量)。
.OSEventType 定义了事件的具体类型。它可以是信号量(OS_EVENT_SEM)、邮箱
(OS_EVENT_TYPE_MBOX)或消息队列(OS_EVENT_TYPE_Q)中的一种。用户要根据该域的具体值
来调用相应的系统函数,以保证对其进行的操作的正确性。
每个等待事件发生的任务都被加入到该事件事件控制块中的等待任务列表中,该列表包括.OSEventGrp和.OSEventTbl[]两个域。变量前面的[.]说明该变量是数据结构的一个域。在这
里,所有的任务的优先级被分成8组(每组8个优先级),分别对应.OSEventGrp中的8位。当
某组中有任务处于等待该事件的状态时,.OSEventGrp中对应的位就被置位。相应地,该任务
在.OSEventTbl[]中的对应位也被置位。.OSEventTbl[]数组的大小由系统中任务的最低优先级
决定,这个值由uCOS_II.H中的OS_LOWEST_PRIO常数定义。这样,在任务优先级比较少的情况
下,减少μC/OS-II对系统RAM的占用量。
当一个事件发生后,该事件的等待事件列表中优先级最高的任务,也即在.OSEventTbl[]中,所有被置1的位中,优先级代码最小的任务得到该事件。图F6.2给出
了.OSEventGrp和.OSEventTbl[]之间的对应关系。该关系可以描述为:
当.OSEventTbl[0]中的任何一位为1时,.OSEventGrp中的第0位为1。
当.OSEventTbl[1]中的任何一位为1时,.OSEventGrp中的第1位为1。
当.OSEventTbl[2]中的任何一位为1时,.OSEventGrp中的第2位为1。
当.OSEventTbl[3]中的任何一位为1时,.OSEventGrp中的第3位为1。
当.OSEventTbl[4]中的任何一位为1时,.OSEventGrp中的第4位为1。
当.OSEventTbl[5]中的任何一位为1时,.OSEventGrp中的第5位为1。
当.OSEventTbl[6]中的任何一位为1时,.OSEventGrp中的第6位为1。
当.OSEventTbl[7]中的任何一位为1时,.OSEventGrp中的第7位为1。
图F6.2事件的等待任务列表
下面的代码将一个任务放到事件的等待任务列表中。
程序清单L6.2——将一个任务插入到事件的等待任务列表中
pevent->OSEventGrp|=OSMapTbl[prio>>3];
pevent->OSEventTbl[prio>>3]|=OSMapTbl[prio&0x07];
其中,prio是任务的优先级,pevent是指向事件控制块的指针。
从程序清单L6.2可以看出,插入一个任务到等待任务列表中所花的时间是相同的,和表中现有多少个任务无关。从图F6.2中可以看出该算法的原理:任务优先级的最低3位决定了该任务在相应的.OSEventTbl[]中的位置,紧接着的3位则决定了该任务优先级在.OSEventTbl[]中的字节索引。该算法中用到的查找表OSMapTbl[](定义在OS_CORE.C中)一般在ROM中实现。
表T6.1OSMapTbl[]
从等待任务列表中删除一个任务的算法则正好相反,如程序清单L6.3所示。
程序清单L6.3从等待任务列表中删除一个任务
if((pevent->OSEventTbl[prio>>3]&=~OSMapTbl[prio&0x07])==0){
pevent->OSEventGrp&=~OSMapTbl[prio>>3];
}
该代码清除了任务在.OSEventTbl[]中的相应位,并且,如果其所在的组中不再有处于等待该事件的任务时(即.OSEventTbl[prio>>3]为0),将.OSEventGrp中的相应位也清除了。和上面的由任务优先级确定该任务在等待表中的位置的算法类似,从等待任务列表中查找处于等待状态的最高优先级任务的算法,也不是从.OSEventTbl[0]开始逐个查询,而是采用了查找另一个表OSUnMapTbl[256](见文件OS_CORE.C)。这里,用于索引的8位分别代表对应的8组中有任务处于等待状态,其中的最低位具有最高的优先级。用这个值索引,首先得到最高优先级任务所在的组的位置(0~7之间的一个数)。然后利用.OSEventTbl[]中对应字节再在OSUnMapTbl[]中查找,就可以得到最高优先级任务在组中的位置(也是0~7之间的一个数)。
这样,最终就可以得到处于等待该事件状态的最高优先级任务了。程序清单L6.4是该算法的具体实现代码。
程序清单L6.4在等待任务列表中查找最高优先级的任务
y=OSUnMapTbl[pevent->OSEventGrp];
x=OSUnMapTbl[pevent->OSEventTbl[y]];
prio=(y<<3)+x;
举例来说,如果.OSEventGrp的值是01101000(二进制),而对应的OSUnMapTbl[.OSEventGrp]值为3,说明最高优先级任务所在的组是3。类似地,如果.OSEventTbl[3]的值是11100100(二进制),OSUnMapTbl[.OSEventTbl[3]]的值为2,则处于等待状态的任务的最高优先级是3×8+2=26。
在μC/OS-II中,事件控制块的总数由用户所需要的信号量、邮箱和消息队列的总数决定。
该值由OS_CFG.H中的#defineOS_MAX_EVENTS定义。 在调用OSInit()时 (见3.11节, μC/OS-II
的初始化),所有事件控制块被链接成一个单向链表——空闲事件控制块链表(图F6.3)。每
当建立一个信号量、邮箱或者消息队列时,就从该链表中取出一个空闲事件控制块,并对它进
行初始化。因为信号量、邮箱和消息队列一旦建立就不能删除,所以事件控制块也不能放回到
空闲事件控制块链表中。
图F6.3空闲事件控制块链表——Figure6.3
对于事件控制块进行的一些通用操作包括:
y 初始化一个事件控制块
y 使一个任务进入就绪态
y 使一个任务进入等待该事件的状态
y 因为等待超时而使一个任务进入就绪态
为了避免代码重复和减短程代码长度,μC/OS-II将上面的操作用4个系统函数实现,它们是:OSEventWaitListInit(),OSEventTaskRdy(),OSEventWait()和OSEventTO()。
6.2 初始化一个事件控制块,OSEventWaitListInit()
程序清单L6.5是函数OSEventWaitListInit()的源代码。当建立一个信号量、邮箱或者消息队列时,相应的建立函数OSSemInit(),OSMboxCreate(),或者OSQCreate()通过调用
OSEventWaitListInit()对事件控制块中的等待任务列表进行初始化。该函数初始化一个空的等
待任务列表,其中没有任何任务。该函数的调用参数只有一个,就是指向需要初始化的事件控
制块的指针pevent。
程序清单L6.5初始化ECB块的等待任务列表
voidOSEventWaitListInit(OS_EVENT*pevent)
{
INT8Ui;
pevent->OSEventGrp=0x00;
for(i=0;i
pevent->OSEventTbl[i]=0x00;
}
}
6.3 使一个任务进入就绪态,OSEventTaskRdy()
程序清单L6.6是函数OSEventTaskRdy()的源代码。当发生了某个事件,该事件等待任务列表中的最高优先级任务(HighestPriorityTask–HPT)要置于就绪态时,该事件对应的OSSemPost(),OSMboxPost(),OSQPost(),和OSQPostFront()函数调用OSEventTaskRdy()实现该操作。换句话说,该函数从等待任务队列中删除HPT任务(HighestPriorityTask),并把该任务置于就绪态。图F6.4给出了OSEventTaskRdy()函数最开始的4个动作。
该函数首先计算HPT任务在.OSEventTbl[]中的字节索引[L6.6/F6.4(1)],其结果是一个从0到OS_LOWEST_PRIO/8+1之间的数,并利用该索引得到该优先级任务在.OSEventGrp中的位屏蔽码[L6.6/F6.4(2)](从表T6.1可以得到该值)。然后,OSEventTaskRdy()函数判断HPT任务在.OSEventTbl[]中相应位的位置[L6.6/F6.4(3)], 其结果是一个从0到OS_LOWEST_PRIO/8+1
之间的数,以及相应的位屏蔽码[L6.6/F6.4(4)]。根据以上结果,OSEventTaskRdy()函数计算
出HPT任务的优先级[L6.6(5)],然后就可以从等待任务列表中删除该任务了[L6.6(6)]。
任务的任务控制块中包含有需要改变的信息。知道了HPT任务的优先级,就可以得到指向该任务的任务控制块的指针[L6.6(7)]。因为最高优先级任务运行条件已经得到满足,必须停止OSTimeTick()函数对.OSTCBDly域的递减操作,所以OSEventTaskRdy()直接将该域清澈0[L6.6(8)]。因为该任务不再等待该事件的发生,所以OSEventTaskRdy()函数将其任务控制块中指向事件控制块的指针指向NULL[L6.6(9)]。如果OSEventTaskRdy()是由OSMboxPost()或者OSQPost()调用的,该函数还要将相应的消息传递给HPT,放在它的任务控制块中[L6.6(10)]。
另外,当OSEventTaskRdy()被调用时,位屏蔽码msk作为参数传递给它。该参数是用于对任务控制块中的位清零的位屏蔽码,和所发生事件的类型相对应[L6.6(11)]。最后,根据.OSTCBStat判断该任务是否已处于就绪状态[L6.6(12)]。如果是,则将HPT插入到μC/OS-II的就绪任务列表中[L6.6(13)]。注意,HPT任务得到该事件后不一定进入就绪状态,也许该任务已经由于其它
原因挂起了。[见4.07节,挂起一个任务,OSTaskSuspend(),和4.08节,恢复一个任务,
OSTaskResume()]。
另外,.OSEventTaskRdy()函数要在中断禁止的情况下调用。
程序清单L6.6使一个任务进入就绪状态
voidOSEventTaskRdy(OS_EVENT*pevent,void*msg,INT8Umsk)
{
OS_TCB*ptcb;
INT8Ux;
INT8Uy;
INT8Ubitx;
INT8Ubity;
INT8Uprio;
y=OSUnMapTbl[pevent->OSEventGrp];(1)
bity=OSMapTbl[y];(2)
x=OSUnMapTbl[pevent->OSEventTbl[y]];(3)
bitx=OSMapTbl[x];(4)
prio=(INT8U)((y<<3)+x);(5)
if((pevent->OSEventTbl[y]&=~bitx)==0){(6)
pevent->OSEventGrp&=~bity;
}
ptcb=OSTCBPrioTbl[prio];(7)
ptcb->OSTCBDly=0;(8)
ptcb->OSTCBEventPtr=(OS_EVENT*)0;(9)
#if(OS_Q_EN&&(OS_MAX_QS>=2))||OS_MBOX_EN
ptcb->OSTCBMsg=msg;(10)
#else
msg=msg;
#endif
ptcb->OSTCBStat&=~msk;(11)
if(ptcb->OSTCBStat==OS_STAT_RDY){(12)
OSRdyGrp|=bity;(13)
OSRdyTbl[y]|=bitx;
}
}
图F6.4使一个任务进入就绪状态——Figure6.4
6.4 使一个任务进入等待某事件发生状态,OSEventTaskWait()
程序清单L6.7是OSEventTaskWait()函数的源代码。当某个任务要等待一个事件的发生时,相应事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用该函数将当前任务从就绪任务表中删除,并放到相应事件的事件控制块的等待任务表中。
程序清单L6.7使一个任务进入等待状态
voidOSEventTaskWait(OS_EVENT*pevent)
{
OSTCBCur->OSTCBEventPtr=pevent;(1)
if((OSRdyTbl[OSTCBCur->OSTCBY]&=~OSTCBCur->OSTCBBitX)==0){(2)
OSRdyGrp&=~OSTCBCur->OSTCBBitY;
}
pevent->OSEventTbl[OSTCBCur->OSTCBY]|=OSTCBCur->OSTCBBitX;(3)
pevent->OSEventGrp|=OSTCBCur->OSTCBBitY;
}
在该函数中,首先将指向事件控制块的指针放到任务的任务控制块中[L6.7(1)],接着将任务从就绪任务表中删除[L6.7(2)],并把该任务放到事件控制块的等待任务表中[L6.7(3)]。
6.5 由于等待超时而将任务置为就绪态,OSEventTO()
程序清单L6.8是OSEventTO()函数的源代码。当在预先指定的时间内任务等待的事件没有发生时,OSTimeTick()函数会因为等待超时而将任务的状态置为就绪。在这种情况下,事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用OSEventTO()来完成这项工作。该函数负责从事件控制块中的等待任务列表里将任务删除[L6.8(1)],并把它置成就绪状态[L6.8(2)]。最后,从任务控制块中将指向事件控制块的指针删除[L6.8(3)]。用户应当注意,调用OSEventTO()也应当先关中断。
程序清单L6.8因为等待超时将任务置为就绪状态
voidOSEventTO(OS_EVENT*pevent)
{
if((pevent->OSEventTbl[OSTCBCur->OSTCBY]&=~OSTCBCur->OSTCBBitX) ==0)
{ (1)
pevent->OSEventGrp&=~OSTCBCur->OSTCBBitY;
}
OSTCBCur->OSTCBStat=OS_STAT_RDY;(2)
OSTCBCur->OSTCBEventPtr=(OS_EVENT*)0;(3)
}
6.6 信号量
μC/OS-II中的信号量由两部分组成:一个是信号量的计数值,它是一个16位的无符号整数 (0到65,535之间) ; 另一个是由等待该信号量的任务组成的等待任务表。 用户要在OS_CFG.H中将OS_SEM_EN开关量常数置成1,这样μC/OS-II才能支持信号量。
在使用一个信号量之前, 首先要建立该信号量, 也即调用OSSemCreate()函数(见下一节) ,
对信号量的初始计数值赋值。该初始值为0到65,535之间的一个数。如果信号量是用来表示一个或者多个事件的发生, 那么该信号量的初始值应设为0。 如果信号量是用于对共享资源的访问,那么该信号量的初始值应设为1(例如,把它当作二值信号量使用)。最后,如果该信号量是用来表示允许任务访问n个相同的资源,那么该初始值显然应该是n,并把该信号量作为一个可计数的信号量使用。
μC/OS-II提供了5个对信号量进行操作的函数。它们是:OSSemCreate(),OSSemPend(),
OSSemPost(),OSSemAccept()和OSSemQuery()函数。图F6.5说明了任务、中断服务子程序和
信号量之间的关系。图中用钥匙或者旗帜的符号来表示信号量:如果信号量用于对共享资源的
访问,那么信号量就用钥匙符号。符号旁边的数字N代表可用资源数。对于二值信号量,该值
就是1;如果信号量用于表示某事件的发生,那么就用旗帜符号。这时的数字N代表事件已经发
生的次数。从图F6.5中可以看出OSSemPost()函数可以由任务或者中断服务子程序调用,而
OSSemPend()和OSSemQuery()函数只能有任务程序调用。
图F6.5任务、中断服务子程序和信号量之间的关系——Figure6.5
6.6.1 建立一个信号量,OSSemCreate()
程序清单L6.9是OSSemCreate()函数的源代码。首先,它从空闲任务控制块链表中得到一个事件控制块[L6.9(1)],并对空闲事件控制链表的指针进行适当的调整,使它指向下一个空闲的事件控制块[L6.9(2)]。如果这时有任务控制块可用[L6.9(3)],就将该任务控制块的事件类型设置成信号量OS_EVENT_TYPE_SEM[L6.9(4)]。其它的信号量操作函数OSSem???()通过检查该域来保证所操作的任务控制块类型的正确。例如,这可以防止调用OSSemPost()函数对一个用作邮箱的任务控制块进行操作[6.06节,邮箱]。接着,用信号量的初始值对任务控制块进行初始化[L6.9(5)],并调用 OSEventWaitListInit()函数对事件控制任务控制块的等待任务列表进行初始化[见6.01节,初始化一个任务控制块,OSEventWaitListInit()][L6.9(6)]。因为信号量正在被初始化,所以这时没有任何任务等待该信号量。最后,OSSemCreate()返回给调用函数一个指向任务控制块的指针。以后对信号量的所有操作,如OSSemPend(),OSSemPost(),OSSemAccept()和OSSemQuery()都是通过该指针完成的。因此,这个指针实际上就是该信号量的句柄。如果系统中没有可用的任务控制块,OSSemCreate()将返回一个NULL指针。