UCOS-II任务间通信(信号量、邮箱、消息队列)

时间:2021-04-04 20:08:05
保护任务之间的共享数据和提供任务之间的通讯方法:
利用宏OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()来关闭和打开中断,这可以用于多任务或者任务和ISR共享某些数据时可以采用这种方法。
利用OSSchedLock()和OSSchedUnlock()对uC/OS-II中的任务调度器上锁和开锁。
利用信号量、邮箱和消息队列。


  一个任务或者中断服务子程序可以通过事件控制块 ECB(Event Control Blocks)来向另外的任务发信号。一个任务还可以等待另一个任务或中断服务子程序给它发送信号。只有任务可以等待事件发生,中断服务程序是不能这样做的。多个任务也可以同时等待同一事件的发生。这种情况下,当事件发生后,所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态。


事件控制块ECB:
  µC/OS-II 通过 uCOS_II.H 中定义的 OS_EVENT 数据结构来维护一个事件控制块的所有信息。该结构中除了包含了事件本身的定义,如用于信号量的计数器,用于指向邮箱的指针,以及指向消息队列的指针数组等,还定义了等待该事件的所有任务的列表。
typedef struct 
{
  void *OSEventPtr; /* 指向消息或者消息队列的指针 */
  
  //类似OSRdyTbl[]和OSRdyGrp
  INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务列表 */
  INT8U OSEventGrp; /* 等待任务所在的组 */


  /* 事件类型可以是OS_EVENT_SEM(信号量)、OS_EVENT_TYPE_MBOX(邮箱)、OS_EVENT_TYPE_Q(消息队列)*/
  INT8U OSEventType; 
  
  NT16U OSEventCnt; /* 计数器(当事件是信号量时) */
} OS_EVENT;


  在µC/OS-II 中,事件控制块的总数由用户所需要的信号量、邮箱和消息队列的总数决定。该值由 OS_CFG.H 中的#define OS_MAX_EVENTS 定义。在调用 OSInit()时,所有事件控制块被链接成一个单向链表——空闲事件控制块链表。每当建立一个信号量、邮箱或者消息队列时,就从该链表中取出一个空闲事件控制块,并对它进行初始化。因为信号量、邮箱和消息队列一旦建立就不能删除,所以事件控制块也不能放回到空闲事件控制块链表中。


初始化一个事件控制块,OSEventWaitListInit( OS_EVENT *pevent)
  当建立一个信号量、邮箱或者消息队列时,相应的建立函数 OSSemInit(),OSMboxCreate(),或者 OSQCreate()通过调用OSEventWaitListInit()对事件控制块中的等待任务列表进行初始化。该函数初始化一个空的等待任务列表,其中没有任何任务。该函数的调用参数只有一个,就是指向需要初始化的事件控制块的指针 pevent。
void OSEventWaitListInit (OS_EVENT *pevent)
{
  INT8U i;
  pevent->OSEventGrp = 0x00;
  for (i = 0; i < OS_EVENT_TBL_SIZE; i++) 
  {
  pevent->OSEventTbl[i] = 0x00;
  }
}


使一个任务进入就绪态,OSEventTaskRdy( OS_EVENT *pevent, void *msg, INT8U msk)
  当发生了某个事件,该事件等待任务列表中的最高优先级任务(Highest Priority Task – HPT)要置于就绪态时,该事件对应的OSSemPost(),OSMboxPost(),OSQPost(),和 OSQPostFront()函数调用 OSEventTaskRdy()实现该操作。换句话说,该函数从等待任务队列中删除 HPT 任务(Highest Priority Task),并把该任务置于就绪态。 OSEventTaskRdy()函数要在中断禁止的情况下调用。
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];
  bity = OSMapTbl[y];
  x = OSUnMapTbl[pevent->OSEventTbl[y]];
  bitx = OSMapTbl[x];
  prio = (INT8U)((y << 3) + x);//找出事件等待列表中优先级最高的任务
  if ((pevent->OSEventTbl[y] &= ~bitx) == 0)//从任务等待列表中删除
  {
  pevent->OSEventGrp &= ~bity;
  }
  ptcb = OSTCBPrioTbl[prio];
  ptcb->OSTCBDly = 0;
  ptcb->OSTCBEventPtr = (OS_EVENT *)0;
  #if (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_MBOX_EN 
  //如果是消息队列或者邮箱则需要把消息传递给任务
  ptcb->OSTCBMsg = msg;
  #else
  msg = msg;
  #endif
  ptcb->OSTCBStat &= ~msk;//OSTCBStat任务状态字,为0表示任务进入就绪态。
  if (ptcb->OSTCBStat == OS_STAT_RDY) 
  {
  OSRdyGrp |= bity;
  OSRdyTbl[y] |= bitx;
  }
}


使一个任务进入等待某事件发生状态, OSEventTaskWait( OS_EVENT *pevent)
  当某个任务要等待一个事件的发生时,相应事件的 OSSemPend(),OSMboxPend()或者 OSQPend()函数会调用该函数将当前任务从就绪任务表中删除,并放到相应事件的事件控制块的等待任务表中。
void OSEventTaskWait (OS_EVENT *pevent)
{
  OSTCBCur->OSTCBEventPtr = pevent;
  if ((OSRdyTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0) 
  {
  OSRdyGrp &= ~OSTCBCur->OSTCBBitY;
  }
  pevent->OSEventTbl[OSTCBCur->OSTCBY] |= OSTCBCur->OSTCBBitX;
  pevent->OSEventGrp |= OSTCBCur->OSTCBBitY;
}


由于等待超时而将任务置为就绪态, OSEventTO( OS_EVENT *pevent)
  当在预先指定的时间内任务等待的事件没有发生时,OSTimeTick()函数会因为等待超时而将任务的状态置为就绪。在这种情况下,事件的OSSemPend(),OSMboxPend()或者OSQPend()函数会调用OSEventTO()来完成这项工作。调用 OSEventTO()也应当先关中断。
void OSEventTO (OS_EVENT *pevent)
{
  if ((pevent->OSEventTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0)
  {
  pevent->OSEventGrp &= ~OSTCBCur->OSTCBBitY;
  }
  OSTCBCur->OSTCBStat = OS_STAT_RDY;
  OSTCBCur->OSTCBEventPtr = (OS_EVENT *)0;
}



信号量:
  µ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,并把该信号量作为一个可计数的信号量使用。


建立一个信号量, OSSemCreate()
  值得注意的是,在µC/OS-II 中,信号量一旦建立就不能删除了,因此也就不可能将一个已分配的任务控制块再放回到空闲 ECB 链表中。如果有任务正在等待某个信号量,或者某任务的运行依赖于某信号量的出现时,删除该任务是很危险的。
OS_EVENT *OSSemCreate (INT16U cnt)
{
  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_SEM;
  pevent->OSEventCnt = cnt;
  OSEventWaitListInit(pevent);
  }
  return (pevent);
}


等待一个信号量,OSSemPend(OS_EVENT *pevent,INT16U timeout,INT8U *err)
void OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)
{
  OS_ENTER_CRITICAL();
  if (pevent->OSEventType != OS_EVENT_TYPE_SEM) 
  {
  OS_EXIT_CRITICAL();
  *err = OS_ERR_EVENT_TYPE;
  }
  if (pevent->OSEventCnt > 0) 
  { 
  pevent->OSEventCnt--;
  OS_EXIT_CRITICAL();
  *err = OS_NO_ERR;
  } 
  else if (OSIntNesting > 0) 
  {
  OS_EXIT_CRITICAL();
  *err = OS_ERR_PEND_ISR;
  } 
  else 
  {
  OSTCBCur->OSTCBStat |= OS_STAT_SEM; 
  OSTCBCur->OSTCBDly = timeout;
  OSEventTaskWait(pevent);//让任务进入等待事件发生状态
  OS_EXIT_CRITICAL();
  OSSched();
  OS_ENTER_CRITICAL();
  
  //OSTCBStat 任务状态字,为0表示任务进入就绪态
  if (OSTCBCur->OSTCBStat & OS_STAT_SEM) 
  {
  OSEventTO(pevent);
  OS_EXIT_CRITICAL();
  *err = OS_TIMEOUT;
  }
  else 
  {
  OSTCBCur->OSTCBEventPtr = (OS_EVENT *)0;
  OS_EXIT_CRITICAL();
  *err = OS_NO_ERR;
  }
  }
}
发送一个信号量, OSSemPost()
INT8U OSSemPost (OS_EVENT *pevent)
{
  OS_ENTER_CRITICAL();
  if (pevent->OSEventType != OS_EVENT_TYPE_SEM) 
  {
  OS_EXIT_CRITICAL();
  return (OS_ERR_EVENT_TYPE);
  }
  if (pevent->OSEventGrp)//如果有任务正在等待该事件
  {
  OSEventTaskRdy(pevent, (void *)0, OS_STAT_SEM);//使一个任务进入就绪态
  OS_EXIT_CRITICAL();
  OSSched();
  return (OS_NO_ERR);
  } 
  else 
  {
  if (pevent->OSEventCnt < 65535) 
  {
  pevent->OSEventCnt++;
  OS_EXIT_CRITICAL();
  return (OS_NO_ERR);
  } 
  else 
  {
  OS_EXIT_CRITICAL();
  return (OS_SEM_OVF);
  }
  }
}
无等待地请求一个信号量, OSSemAccept()
  当一个任务请求一个信号量时,如果该信号量暂时无效,也可以让该任务简单地返回,而不是进入睡眠等待状态。这种情况下的操作是由 OSSemAccept()函数完成的。


查询一个信号量的当前状态, OSSemQuery( OS_EVENT *pevent, OS_SEM_DATA *pdata)
  该函数有两个参数:一个是指向信号量对应事件控制块的指针 pevent。该指针是在生产信号量时,由 OSSemCreate()函数返回的;另一个是指向用于记录信号量信息的数据结构OS_SEM_DATA的指针pdata。该函数的任务只是把 OS_EVENT 结构拷贝到 OS_SEM_DATA 结构变量中去。



邮箱:
  邮箱是µC/OS-II 中另一种通讯机制, 它可以使一个任务或者中断服务子程序向另一个任务发送一个指针型的变量。该指针指向一个包含了特定“消息”的数据结构。为了在µC/OS-II 中使用邮箱,必须将 OS_CFG.H 中的 OS_MBOX_EN 常数置为 1。


建立一个邮箱,OSMboxCreate( void *msg)
  邮箱一旦建立,是不能被删除的。比如,如果有任务正在等待一个邮箱的信息,这时删除该邮箱,将有可能产生灾难性的后果。
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;
  pevent->OSEventPtr = msg;
  OSEventWaitListInit(pevent);
  }
  return (pevent);
}


等待一个邮箱中的消息,void *OSMboxPend( OS_EVENT *pevent, INT16U timeout, INT8U *err)


发送一个消息到邮箱中 ,INT8U OSMboxPost( OS_EVENT *pevent, void *msg)


无等待地从邮箱中得到一个消息, void *OSMboxAccept( OS_EVENT *pevent)


查询一个邮箱的状态, INT8U OSMboxQuery( OS_EVENT *pevent, OS_MBOX_DATA *pdata)



消息队列:实际上就是一个循环数组(即队列)然后每个元素都可以存储响应的消息。
  消息队列是µC/OS-II 中另一种通讯机制, 它可以使一个任务或者中断服务子程序向另一个任务发送以指针方式定义的变量。因具体的应用有所不同,每个指针指向的数据结构变量也有所不同。为了使用µC/OS-II 的消息队列功能,需要在 OS_CFG.H 文件中,将 OS_Q_EN 常数设置为 1,并且通过常数 OS_MAX_QS 来决定µC/OS-II 支持的最多消息队列数。
  其中,消息队列的符号很像多个邮箱。实际上,我们可以将消息队列看作是多个邮箱组成的数组,只是它们共用一个等待任务列表。每个指针所指向的数据结构是由具体的应用程序决定的。


队列控制块:
  队列控制块是一个用于维护消息队列信息的数据结构,它包含了以下的一些域。
 .OSQPtr 在空闲队列控制块中链接所有的队列控制块。一旦建立了消息队列,该域就不再有用了。
.OSQStart 是指向消息队列的指针数组的起始地址的指针。用户应用程序在使用消息队列之前必须先定义该数组。
.OSQEnd 是指向消息队列结束单元的下一个地址的指针。该指针使得消息队列构成一个循环的缓冲区。
.OSQIn 是指向消息队列中插入下一条消息的位置的指针。当.OSQIn 和.OSQEnd 相等时,.OSQIn 被调整指向消息队列的起始单元。
.OSQOut 是指向消息队列中下一个取出消息的位置的指针。当.OSQOut 和.OSQEnd 相等时,.OSQOut 被调整指向消息队列的起始单元。
.OSQSize 是消息队列中总的单元数。该值是在建立消息队列时由用户应用程序决定的。在µC/OS-II 中,该值最大可以是 65,535。
.OSQEntries 是消息队列中当前的消息数量。当消息队列是空的时,该值为 0。当消息队列满了以后,该值和.OSQSize 值一样。 在消息队列刚刚建立时,该值为 0。


建立一个消息队列,OSQCreate()
OS_EVENT *OSQCreate (void **start, INT16U size)
{
  OS_EVENT *pevent;
  OS_Q *pq;
  OS_ENTER_CRITICAL();
  pevent = OSEventFreeList;
  if (OSEventFreeList != (OS_EVENT *)0) 
  {
  OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
  }
  OS_EXIT_CRITICAL();
  if (pevent != (OS_EVENT *)0) 
  {
  OS_ENTER_CRITICAL();
  pq = OSQFreeList;// 从空闲队列控制块列表中取出一个队列控制块
  if (OSQFreeList != (OS_Q *)0)
  {
  OSQFreeList = OSQFreeList->OSQPtr;
  }
  OS_EXIT_CRITICAL();
  if (pq != (OS_Q *)0) 
  {
  pq->OSQStart = start;
  pq->OSQEnd = &start[size];
  pq->OSQIn = start;
  pq->OSQOut = start;
  pq->OSQSize = size;
  pq->OSQEntries = 0;
  pevent->OSEventType = OS_EVENT_TYPE_Q;
  pevent->OSEventPtr = pq;
  OSEventWaitListInit(pevent);
  }
  else 
  {
  OS_ENTER_CRITICAL();
  pevent->OSEventPtr = (void *)OSEventFreeList;
  OSEventFreeList = pevent;
  OS_EXIT_CRITICAL();
  pevent = (OS_EVENT *)0;
  }
  }
  return (pevent);
}
等待一个消息队列中的消息,void *OSQPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)


向消息队列发送一个消息(FIFO),INT8U OSQPost (OS_EVENT *pevent, void *msg)
如果 OSQPost()函数是由中断服务子程序调用的,那么即使产生了更高优先级的任务,也不会在调用 OSSched()函数时发生任务切换。这个动作一直要等到中断嵌套的最外层中断服务子程序调用 OSIntExit()函数时才能进行。如果此时消息队列已满,那么该消息将由于不能插入到消息队列中而丢失。


向消息队列发送一个消息(后进先出 LIFO),INT8U OSQPostFront (OS_EVENT *pevent, void *msg)


无等待地从消息队列中取一条消息,void *OSQAccept (OS_EVENT *pevent)


清空消息队列,INT8U OSQFlush (OS_EVENT *pevent)


查询一个消息队列的状态,INT8U OSQQuery (OS_EVENT *pevent, OS_Q_DATA *pdata)
  OS_Q_DATA(见 uCOS_II.H)数据结构的指针pdata。该结构包含了有关消息队列的信息。在调用 OSQQuery()函数之前,必须先定义该数据结构变量。OS_Q_DATA 结构包含下面的几个域。
  .OSMsg 如果消息队列中有消息,它包含指针
  .OSQOut 所指向的队列单元中的内容。如果队列是空的,.OSMsg 包含一个 NULL 指针。
  .OSNMsgs 是消息队列中的消息数(.OSQEntries 的拷贝)。
  .OSQSize 是消息队列的总的容量
  .OSEventTbl[]和.OSEventGrp 是消息队列的等待任务列表。通过它们,OSQQuery()的调用函数可以得到等待该消息队列中的消息的任务总数