第6章 任务之间的通讯与同步... 1< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
6.1 初始化一个ECB块,OSEventWaitListInit() 6
6.2 使一个任务进入就绪状态,OSEventTaskRdy() 7
6.3 使一个任务进入等待状态, OSEventTaskWait() 9
6.4 由于等待超时将一个任务置为就绪状态, OSEventTO() 9
6.5.1 建立一个信号量, OSSemCreate(). 11
6.5.2 等待一个信号量, OSSemPend(). 12
6.5.3 发送一个信号量, OSSemPost(). 14
6.5.4 无等待地请求一个信号量, OSSemAccept(). 16
6.5.5 查询一个信号量的当前状态, OSSemQuery(). 17
6.6.1 建立一个邮箱,OSMboxCreate(). 19
6.6.2 等待一个邮箱中的消息,OSMboxPend(). 20
6.6.3 发送一个消息到邮箱中,OSMboxPost(). 22
6.6.4 无等待地从邮箱中得到一个消息, OSMboxAccept(). 24
6.6.5 查询一个邮箱的状态, OSMboxQuery(). 25
6.6.7 使用邮箱实现延时,而不使用OSTimeDly(). 27
6.7.1 建立一个消息队列,OSQCreate(). 31
6.7.2 等待一个消息队列中的消息,OSQPend(). 33
6.7.3 向消息队列发送一个消息(FIFO),OSQPost(). 35
6.7.4 向消息队列发送一个消息(LIFO),OSQPostFront(). 37
6.7.5 无等待地从一个消息队列中取得消息, OSQAccept(). 39
6.7.6 清空一个消息队列, OSQFlush(). 40
第6章 任务之间的通讯与同步
在µ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(Event Control Blocks)来向另外的任务发信号[F6.1A(1)]。这里,所有的信号都被看成是事件(Event)。这也说明为什么上面把用于通讯的数据结构叫做事件控制块。一个任务还可以等待另一个任务或中断服务子程序给它发送信号[F6.1A(2)]。这里要注意的是,只有任务可以等待事件发生,中断服务子程序是不能这样做的。对于处于等待状态的任务,还可以给它指定一个最长等待时间,以此来防止因为等待的事件没有发生而无限期地等下去。
多个任务可以同时等待同一个事件的发生[F6.1B]。在这种情况下,当该事件发生后,所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态,准备执行。上面讲到的事件,可以是信号量、邮箱或者消息队列等。当事件控制块是一个信号量时,任务可以等待它,也可以给它发送消息。
< xmlnamespace prefix ="v" ns ="urn:schemas-microsoft-com:vml" />< xmlnamespace prefix ="w" ns ="urn:schemas-microsoft-com:office:word" />
图 6.1 事件控制块的使用
6.1 事件控制块ECB
µC/OS-II通过uCOS_II.H 中定义的OS_EVENT数据结构来维护一个事件控制块的所有信息[程序清单L6.1],也就是本章开篇讲到的事件控制块ECB。该结构中除了包含了事件本身的定义,如用于信号量的计数器,用于指向邮箱的指针,以及指向消息队列的指针数组等,还定义了等待该事件的所有任务的列表。程序清单 L6.1是该数据结构的定义。
程序清单 L6.1 ECB数据结构 |
typedef struct { |
void *OSEventPtr; /* 指向消息或者消息队列的指针 */ |
INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务列表 */ |
INT16U OSEventCnt; /* 计数器(当事件是信号量时) */ |
INT8U OSEventType; /* 时间类型 */ |
INT8U OSEventGrp; /* 等待任务所在的组 */ |
} 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.1 OSMapTbl[]
| |
Index | Bit Mask (Binary) |
0 | 00000001 |
1 | 00000010 |
2 | 00000100 |
3 | 00001000 |
4 | 00010000 |
5 | 00100000 |
6 | 01000000 |
7 | 10000000 |
从等待任务列表中删除一个任务的算法则正好相反,如程序清单 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 中的#define OS_MAX_EVENTS定义。在调用OSInit()时(见3.11节,µC/OS-II的初始化),所有事件控制块被链接成一个单向链表——空闲事件控制块链表(图 F6.3)。每当建立一个信号量、邮箱或者消息队列时,就从该链表中取出一个空闲事件控制块,并对它进行初始化。因为信号量、邮箱和消息队列一旦建立就不能删除,所以事件控制块也不能放回到空闲事件控制块链表中。
图 F6.3 空闲事件控制块链表——Figure 6.3
对于事件控制块进行的一些通用操作包括:
? 初始化一个事件控制块
? 使一个任务进入就绪态
? 使一个任务进入等待该事件的状态
? 因为等待超时而使一个任务进入就绪态
为了避免代码重复和减短程代码长度,µC/OS-II将上面的操作用4个系统函数实现,它们是:OSEventWaitListInit(),OSEventTaskRdy(),OSEventWait()和OSEventTO()。
6.2 初始化一个事件控制块,OSEventWaitListInit()
程序清单 L6.5是函数OSEventWaitListInit()的源代码。当建立一个信号量、邮箱或者消息队列时,相应的建立函数OSSemInit(),OSMboxCreate(),或者OSQCreate()通过调用OSEventWaitListInit()对事件控制块中的等待任务列表进行初始化。该函数初始化一个空的等待任务列表,其中没有任何任务。该函数的调用参数只有一个,就是指向需要初始化的事件控制块的指针pevent。
程序清单 L6.5 初始化ECB块的等待任务列表 |
void OSEventWaitListInit (OS_EVENT *pevent) |
{ |
INT8U i; |
|
|
pevent->OSEventGrp = 0x00; |
for (i = 0; i < OS_EVENT_TBL_SIZE; i++) { |
pevent->OSEventTbl[i] = 0x00; |
} |
} |
6.3 使一个任务进入就绪态,OSEventTaskRdy()
程序清单 L6.6是函数OSEventTaskRdy()的源代码。当发生了某个事件,该事件等待任务列表中的最高优先级任务(Highest Priority Task – HPT)要置于就绪态时,该事件对应的OSSemPost(),OSMboxPost(),OSQPost(),和OSQPostFront()函数调用OSEventTaskRdy()实现该操作。换句话说,该函数从等待任务队列中删除HPT任务(Highest Priority Task),并把该任务置于就绪态。图 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 使一个任务进入就绪状态 |
void OSEventTaskRdy (OS_EVENT *pevent, void *msg, INT8U msk) |
{ |
OS_TCB *ptcb; |
INT8U x; |
INT8U y; |
INT8U bitx; |
INT8U bity; |
INT8U prio; |
|
|
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 使一个任务进入就绪状态——Figure 6.4
6.4 使一个任务进入等待某事件发生状态, OSEventTaskWait()
程序清单 L6.7是OSEventTaskWait()函数的源代码。当某个任务要等待一个事件的发生时,相应事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用该函数将当前任务从就绪任务表中删除,并放到相应事件的事件控制块的等待任务表中。
程序清单 L6.7 使一个任务进入等待状态 |
void OSEventTaskWait (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 因为等待超时将任务置为就绪状态 |
void OSEventTO (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 任务、中断服务子程序和信号量之间的关系——Figure 6.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指针。
值得注意的是,在µC/OS-II中,信号量一旦建立就不能删除了,因此也就不可能将一个已分配的任务控制块再放回到空闲ECB链表中。如果有任务正在等待某个信号量,或者某任务的运行依赖于某信号量的出现时,删除该任务是很危险的。
程序清单 L6.9 建立一个信号量 |
OS_EVENT *OSSemCreate (INT16U cnt) |
{ |
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 等待一个信号量 |
void OSSemPend (OS_EVENT *pevent, INT16U timeout, 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; |
} else if (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 发出一个信号量 |
INT8U OSSemPost (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 无等待地请求一个信号量 |
INT16U OSSemAccept (OS_EVENT *pevent) |
{ |
INT16U cnt; |
|
|
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 查询一个信号量的状态 |
INT8U OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *pdata) |
{ |
INT8U i; |
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 < OS_EVENT_TBL_SIZE; 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, INT16U timeout, 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; |
} else if (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; |
} else if (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中的中断)。