值得注意的是,在μC/OS-II中,信号量一旦建立就不能删除了,因此也就不可能将一个已分配的任务控制块再放回到空闲ECB链表中。如果有任务正在等待某个信号量,或者某任务的运行依赖于某信号量的出现时,删除该任务是很危险的。
程序清单L6.9建立一个信号量
OS_EVENT*OSSemCreate(INT16Ucnt)
{
OS_EVENT*pevent;
OS_ENTER_CRITICAL();
pevent=OSEventFreeList;(1)
if(OSEventFreeList!=(OS_EVENT*)0){(2)
OSEventFreeList=(OS_EVENT*)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if(pevent!=(OS_EVENT*)0){(3)
pevent->OSEventType=OS_EVENT_TYPE_SEM;(4)
pevent->OSEventCnt=cnt;(5)
OSEventWaitListInit(pevent);(6)
}
return(pevent);(7)
}
6.6.2 等待一个信号量,OSSemPend()
程序清单L6.10是OSSemPend()函数的源代码。它首先检查指针pevent所指的任务控制块是否是由OSSemCreate()建立的[L6.10(1)]。如果信号量当前是可用的(信号量的计数值大于0)[L6.10(2)],将信号量的计数值减1[L6.10(3)],然后函数将“无错”错误代码返回给它的调用函数。显然,如果正在等待信号量,这时的输出正是我们所希望的,也是运行OSSemPend()函数最快的路径。
如果此时信号量无效(计数器的值是0),OSSemPend()函数要进一步检查它的调用函数是不是中断服务子程序[L6.10(4)]。在正常情况下,中断服务子程序是不会调用OSSemPend()函数的。这里加入这些代码,只是为了以防万一。当然,在信号量有效的情况下,即使是中断服务
子程序调用的OSSemPend(),函数也会成功返回,不会出任何错误。
如果信号量的计数值为0,而OSSemPend()函数又不是由中断服务子程序调用的,则调
用OSSemPend()函数的任务要进入睡眠状态,等待另一个任务(或者中断服务子程序)发出该信
号量(见下节)。OSSemPend()允许用户定义一个最长等待时间作为它的参数,这样可以避免该
任务无休止地等待下去。如果该参数值是一个大于0的值,那么该任务将一直等到信号有效或
者等待超时。如果该参数值为0,该任务将一直等待下去。OSSemPend()函数通过将任务控制块
中的状态标志.OSTCBStat置1,把任务置于睡眠状态[L6.10(5)],等待时间也同时置入任务控
制块中[L6.10(6)],该值在OSTimeTick()函数中被逐次递减。注意,OSTimeTick()函数对每个
任务的任务控制块的.OSTCBDly域做递减操作(只要该域不为0)[见3.10节,时钟节拍]。真
正将任务置入睡眠状态的操作在OSEventTaskWait()函数中执行[见6.03节,让一个任务等待
某个事件,OSEventTaskWait()][L6.10(7)]。
因为当前任务已经不是就绪态了,所以任务调度函数将下一个最高优先级的任务调入,准备运行[L6.10(8)]。当信号量有效或者等待时间到后,调用OSSemPend()函数的任务将再一次成为最高优先级任务。这时OSSched()函数返回。这之后,OSSemPend()要检查任务控制块中的状态标志,看该任务是否仍处于等待信号量的状态[L6.10(9)]。如果是,说明该任务还没有被OSSemPost()函数发出的信号量唤醒。事实上,该任务是因为等待超时而由TimeTick()函数把它置为就绪状态的。这种情况下,OSSemPend()函数调用 OSEventTO()函数将任务从等待任务列表中删除[L6.10(10)],并返回给它的调用任务一个“超时”的错误代码。如果任务的任务控制块中的OS_STAT_SEM标志位没有置位,就认为调用 OSSemPend()的任务已经得到了该信号量,将指向信号量ECB的指针从该任务的任务控制块中删除,并返回给调用函数一个“无错”的错误代码[L6.10(11)]。
程序清单L6.10等待一个信号量
voidOSSemPend(OS_EVENT*pevent,INT16Utimeout,INT8U*err)
{
OS_ENTER_CRITICAL();
if(pevent->OSEventType!=OS_EVENT_TYPE_SEM){(1)
OS_EXIT_CRITICAL();
*err=OS_ERR_EVENT_TYPE;
}
if(pevent->OSEventCnt>0){(2)
pevent->OSEventCnt--;(3)
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}elseif(OSIntNesting>0){(4)
OS_EXIT_CRITICAL();
*err=OS_ERR_PEND_ISR;
}else{
OSTCBCur->OSTCBStat|=OS_STAT_SEM;(5)
OSTCBCur->OSTCBDly=timeout;(6)
OSEventTaskWait(pevent);(7)
OS_EXIT_CRITICAL();
OSSched();(8)
OS_ENTER_CRITICAL();
if(OSTCBCur->OSTCBStat&OS_STAT_SEM){(9)
OSEventTO(pevent);(10)
OS_EXIT_CRITICAL();
*err=OS_TIMEOUT;
}else{
OSTCBCur->OSTCBEventPtr=(OS_EVENT*)0;(11)
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}
}
}
6.6.3 发送一个信号量,OSSemPost()
程序清单L6.11是OSSemPost()函数的源代码。它首先检查参数指针pevent指向的任务控制块是否是OSSemCreate()函数建立的[L6.11(1)],接着检查是否有任务在等待该信号量[L6.11(2)]。如果该任务控制块中的.OSEventGrp域不是0,说明有任务正在等待该信号量。这时,就要调用函数OSEventTaskRdy()[见6.02节,使一个任务进入就绪状态,OSEventTaskRdy()],把其中的最高优先级任务从等待任务列表中删除[L6.11(3)]并使它进入就绪状态。然后,调用OSSched()任务调度函数检查该任务是否是系统中的最高优先级的就绪任务[L6.11(4)]。如果是,这时就要进行任务切换[当OSSemPost()函数是在任务中调用的],准备执行该就绪任务。如果不是,OSSched()直接返回,调用 OSSemPost()的任务得以继续执行。如果这时没有任务在等待该信号量,该信号量的计数值就简单地加1[L6.11(5)]。
上面是由任务调用OSSemPost()时的情况。当中断服务子程序调用该函数时,不会发生上面的任务切换。如果需要,任务切换要等到中断嵌套的最外层中断服务子程序调用OSIntExit()函数后才能进行(见3.09节,μC/OS-II中的中断)。
程序清单L6.11发出一个信号量
INT8UOSSemPost(OS_EVENT*pevent)
{
OS_ENTER_CRITICAL();
if(pevent->OSEventType!=OS_EVENT_TYPE_SEM){(1)
OS_EXIT_CRITICAL();
return(OS_ERR_EVENT_TYPE);
}
if(pevent->OSEventGrp){(2)
OSEventTaskRdy(pevent,(void*)0,OS_STAT_SEM);(3)
OS_EXIT_CRITICAL();
OSSched();(4)
return(OS_NO_ERR);
}else{
if(pevent->OSEventCnt<65535){
pevent->OSEventCnt++;(5)
OS_EXIT_CRITICAL();
return(OS_NO_ERR);
}else{
OS_EXIT_CRITICAL();
return(OS_SEM_OVF);
}
}
}
6.6.4 无等待地请求一个信号量,OSSemAccept()
当一个任务请求一个信号量时,如果该信号量暂时无效,也可以让该任务简单地返回,而不是进入睡眠等待状态。这种情况下的操作是由OSSemAccept()函数完成的,其源代码见程序清单L6.12。该函数在最开始也是检查参数指针pevent指向的事件控制块是否是由OSSemCreate()函数建立的[L6.12(1)],接着从该信号量的事件控制块中取出当前计数值[L6.12(2)],并检查该信号量是否有效(计数值是否为非0值)[L6.12(3)]。如果有效,则将信号量的计数值减1[L6.12(4)],然后将信号量的原有计数值返回给调用函数[L6.12(5)]。调用函数需要对该返回值进行检查。如果该值是0,说明该信号量无效。如果该值大于0,说明该信号量有效,同时该值也暗示着该信号量当前可用的资源数。应该注意的是,这些可用资源中,已经被该调用函数自身占用了一个(该计数值已经被减1)。中断服务子程序要请求信号量时,只能用OSSemAccept()而不能用OSSemPend(),因为中断服务子程序是不允许等待的。
程序清单L6.12无等待地请求一个信号量
INT16UOSSemAccept(OS_EVENT*pevent)
{
INT16Ucnt;
OS_ENTER_CRITICAL();
if(pevent->OSEventType!=OS_EVENT_TYPE_SEM){(1)
OS_EXIT_CRITICAL();
return(0);
}
cnt=pevent->OSEventCnt;(2)
if(cnt>0){(3)
pevent->OSEventCnt--;(4)
}
OS_EXIT_CRITICAL();
return(cnt);(5)
}
6.6.5 查询一个信号量的当前状态,OSSemQuery()
在应用程序中,用户随时可以调用函数OSSemQuery()[程序清单L6.13]来查询一个信号量的当前状态。该函数有两个参数:一个是指向信号量对应事件控制块的指针pevent。该指针是在生产信号量时,由OSSemCreate()函数返回的;另一个是指向用于记录信号量信息的数据结构OS_SEM_DATA(见uCOS_II.H)的指针pdata。因此,调用该函数前,用户必须先定义该结构变量,用于存储信号量的有关信息。在这里,之所以使用一个新的数据结构的原因在于,调用函数应该只关心那些和特定信号量有关的信息, 而不是象OS_EVENT数据结构包含的很全面的信息。
该数据结构只包含信号量计数值.OSCnt和等待任务列表.OSEventTbl[]、.OSEventGrp,而OS_EVENT中还包含了另外的两个域.OSEventType和.OSEventPtr。
和其它与信号量有关的函数一样,OSSemQuery()也是先检查pevent指向的事件控制块是否是OSSemCreate()产生的[L6.13(1)],然后将等待任务列表[L6.13(2)]和计数值[L6.13(3)]
从OS_EVENT结构拷贝到OS_SEM_DATA结构变量中去。
程序清单L6.13查询一个信号量的状态
INT8UOSSemQuery(OS_EVENT*pevent,OS_SEM_DATA*pdata)
{
INT8Ui;
INT8U*psrc;
INT8U*pdest;
OS_ENTER_CRITICAL();
if(pevent->OSEventType!=OS_EVENT_TYPE_SEM){(1)
OS_EXIT_CRITICAL();
return(OS_ERR_EVENT_TYPE);
}
pdata->OSEventGrp=pevent->OSEventGrp;(2)
psrc=&pevent->OSEventTbl[0];
pdest=&pdata->OSEventTbl[0];
for(i=0;i
*pdest++=*psrc++;
}
pdata->OSCnt=pevent->OSEventCnt;(3)
OS_EXIT_CRITICAL();
return(OS_NO_ERR);
}
6.7 邮箱
邮箱是μC/OS-II中另一种通讯机制, 它可以使一个任务或者中断服务子程序向另一个任务发送一个指针型的变量。该指针指向一个包含了特定“消息”的数据结构。为了在μC/OS-II中使用邮箱,必须将OS_CFG.H中的OS_MBOX_EN常数置为1。
使用邮箱之前,必须先建立该邮箱。该操作可以通过调用OSMboxCreate()函数来完成(见下节),并且要指定指针的初始值。一般情况下,这个初始值是NULL,但也可以初始化一个邮
箱,使其在最开始就包含一条消息。如果使用邮箱的目的是用来通知一个事件的发生(发送一
条消息),那么就要初始化该邮箱为NULL,因为在开始时,事件还没有发生。如果用户用邮箱
来共享某些资源,那么就要初始化该邮箱为一个非NULL的指针。在这种情况下,邮箱被当成一
个二值信号量使用。
μC/OS-II提供了5种对邮箱的操作:OSMboxCreate(),OSMboxPend(),OSMboxPost(),
OSMboxAccept()和 OSMboxQuery()函数。图F6.6描述了任务、中断服务子程序和邮箱之间的关
系,这里用符号“I”表示邮箱。邮箱包含的内容是一个指向一条消息的指针。一个邮箱只能包
含一个这样的指针(邮箱为满时),或者一个指向NULL的指针(邮箱为空时)。从图F6.6可以看出,任务或者中断服务子程序可以调用函数OSMboxPost(),但是只有任务可以调用函数OSMboxPend()和OSMboxQuery()。
图F6.6任务、中断服务子程序和邮箱之间的关系
6.7.1 建立一个邮箱,OSMboxCreate()
程序清单L6.14是OSMboxCreate()函数的源代码,基本上和函数OSSemCreate()相似。不同之处在于事件控制块的类型被设置成OS_EVENT_TYPE_MBOX[L6.14(1)], 以及使用.OSEventPtr域来容纳消息指针,而不是使用.OSEventCnt域[L6.14(2)]。
OSMboxCreate()函数的返回值是一个指向事件控制块的指针[L6.14(3)]。这个指针在调用函数OSMboxPend(),OSMboxPost(),OSMboxAccept()和OSMboxQuery()时使用。因此,该指针可以看作是对应邮箱的句柄。值得注意的是,如果系统中已经没有事件控制块可用,函数OSMboxCreate()将返回一个NULL指针。
邮箱一旦建立,是不能被删除的。比如,如果有任务正在等待一个邮箱的信息,这时删除该邮箱,将有可能产生灾难性的后果。
程序清单L6.14建立一个邮箱
OS_EVENT*OSMboxCreate(void*msg)
{
OS_EVENT*pevent;
OS_ENTER_CRITICAL();
pevent=OSEventFreeList;
if(OSEventFreeList!=(OS_EVENT*)0){
OSEventFreeList=(OS_EVENT*)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if(pevent!=(OS_EVENT*)0){
pevent->OSEventType=OS_EVENT_TYPE_MBOX;(1)
pevent->OSEventPtr=msg;(2)
OSEventWaitListInit(pevent);
}
return(pevent);(3)
}
6.7.2 等待一个邮箱中的消息,OSMboxPend()
程序清单L6.15是OSMboxPend()函数的源代码。 同样, 它和OSSemPend()也很相似,因此,在这里只讲述其中的不同之处。OSMboxPend()首先检查该事件控制块是由 OSMboxCreate()函数建立的[L6.15(1)]。当.OSEventPtr域是一个非NULL的指针时,说明该邮箱中有可用的消息[L6.15(2)]。这种情况下,OSMboxPend()函数将该域的值复制到局部变量msg中,然后将.OSEventPtr置为NULL[L6.15(3)]。这正是我们所期望的,也是执行 OSMboxPend()函数最快的路径。
如果此时邮箱中没有消息是可用的(.OSEventPtr域是NULL指针),OSMboxPend()函数检查它的调用者是否是中断服务子程序[L6.15(4)]。象OSSemPend()函数一样,不能在中断服务子程序中调用OSMboxPend(),因为中断服务子程序是不能等待的。这里的代码同样是为了以防万一。但是,如果邮箱中有可用的消息,即使从中断服务子程序中调用OSMboxPend()函数,也一样是成功的。
如果邮箱中没有可用的消息,OSMboxPend()的调用任务就被挂起,直到邮箱中有了消息或者等待超时[L6.15(5)]。当有其它的任务向该邮箱发送了消息后(或者等待时间超时),这时,该任务再一次成为最高优先级任务,OSSched()返回。这时,OSMboxPend()函数要检查是否有消息被放到该任务的任务控制块中[L6.15(6)]。如果有,那么该次函数调用成功,对应的消息被返回到调用函数。
程序清单L6.15等待一个邮箱中的消息
void*OSMboxPend(OS_EVENT*pevent,INT16Utimeout,INT8U*err)
{
void*msg;
OS_ENTER_CRITICAL();
if(pevent->OSEventType!=OS_EVENT_TYPE_MBOX){(1)
OS_EXIT_CRITICAL();
*err=OS_ERR_EVENT_TYPE;
return((void*)0);
}
msg=pevent->OSEventPtr;
if(msg!=(void*)0){(2)
pevent->OSEventPtr=(void*)0;(3)
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}elseif(OSIntNesting>0){(4)
OS_EXIT_CRITICAL();
*err=OS_ERR_PEND_ISR;
}else{
OSTCBCur->OSTCBStat|=OS_STAT_MBOX;(5)
OSTCBCur->OSTCBDly=timeout;
OSEventTaskWait(pevent);
OS_EXIT_CRITICAL();
OSSched();
OS_ENTER_CRITICAL();
if((msg=OSTCBCur->OSTCBMsg)!=(void*)0){(6)
OSTCBCur->OSTCBMsg=(void*)0;
OSTCBCur->OSTCBStat=OS_STAT_RDY;
OSTCBCur->OSTCBEventPtr=(OS_EVENT*)0;
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}elseif(OSTCBCur->OSTCBStat&OS_STAT_MBOX){(7)
OSEventTO(pevent);(8)
OS_EXIT_CRITICAL();
msg=(void*)0;(9)
*err=OS_TIMEOUT;
}else{
msg=pevent->OSEventPtr;(10)
pevent->OSEventPtr=(void*)0;
(11)
OSTCBCur->OSTCBEventPtr=(OS_EVENT*)0;(12)
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}
}
return(msg);
}
在OSMboxPend()函数中,通过检查任务控制块中的.OSTCBStat域中的OS_STAT_MBOX位,可以知道是否等待超时。如果该域被置1,说明任务等待已经超时[L6.15(7)]。这时,通过调用函数OSEventTo()可以将任务从邮箱的等待列表中删除[L6.15(8)]。因为此时邮箱中没有消息,所以返回的指针是NULL[L6.15(9)]。如果OS_STAT_MBOX位没有被置1,说明所等待的消息已经被发出。OSMboxPend()的调用函数得到指向消息的指针[L6.15(10)]。此后,OSMboxPend()函数通过将邮箱事件控制块的.OSEventPtr域置为NULL清空该邮箱,并且要将任务任务控制块中指向邮箱事件控制块的指针删除[L6.15(12)]。
6.7.3 发送一个消息到邮箱中,OSMboxPost()
程序清单L6.16是OSMboxPost()函数的源代码。检查了事件控制块是否是一个邮箱后[L6.16(1)],OSMboxPost()函数还要检查是否有任务在等待该邮箱中的消息[L6.16(2)]。如果事件控制块中的OSEventGrp域包含非零值,就暗示着有任务在等待该消息。这时,调用OSEventTaskRdy()将其中的最高优先级任务从等待列表中删除[见6.02节,使一个任务进入就绪状态,OSEventTaskRdy()][L6.16(3)],加入系统的就绪任务列表中,准备运行。然后,调用OSSched()函数[L6.16(4)],检查该任务是否是系统中最高优先级的就绪任务。如果是,执行任
务切换[仅当OSMboxPost()函数是由任务调用时],该任务得以执行。如果该任务不是最高优先
级的任务,OSSched()返回,OSMboxPost()的调用函数继续执行。如果没有任何任务等待该消息,
指向消息的指针就被保存到邮箱中[L6.16(6)](假设此时邮箱中的指针不是非NULL的[L6.16(5)])。这样,下一个调用OSMboxPend()函数的任务就可以立刻得到该消息了。
注意,如果OSMboxPost()函数是从中断服务子程序中调用的,那么,这时并不发生上下文的切换。如果需要,中断服务子程序引起的上下文切换只发生在中断嵌套的最外层中断服务子程序对OSIntExit()函数的调用时(见3.09节,μC/OS-II中的中断)。