快速回顾
本文进一步解析事件模型中,消息队列类型的函数源码。
先回顾一下之前的通用事件控制块类型OS_EVENT:
cpp
//ucos_ii.h
typedef struct os_event {
INT8U OSEventType; /* 事件类型,有六种(其中一种是UNUSED) */
void *OSEventPtr; /* OSEventPtr是一个多用途的指针,当作为链表时,可以指向下一个控制块;当作为具体的事件控制块时,指向具体的事件结构,如OS_Q。信号量不使用该指针 */
INT16U OSEventCnt; /* 信号量计数器,其它事件类型不使用该成员 */
OS_PRIO OSEventGrp; /* 等待信号的任务优先级组,和OSEventTbl共同组成"事件等待表" */
OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待信号的组内优先级 */
#if OS_EVENT_NAME_EN > 0u
INT8U *OSEventName; //事件名称
#endif
} OS_EVENT;
消息队列有自己的专属结构,该结构通过OSEventPtr指针挂载在OS_EVENT中。
cpp
//ucos_ii.h
typedef struct os_q {
struct os_q *OSQPtr; /* 链接下一个消息队列控制块,一旦脱离链表,该指针就没用了 */
void **OSQStart; /* 消息队列首条消息的存放地址 */
void **OSQEnd; /* 消息队列最后一条消息的存放地址 */
void **OSQIn; /* 写指针,即下一条消息的存放地址 */
void **OSQOut; /* 读指针,即下一条消息的读取地址 */
INT16U OSQSize; /* 消息队列的长度(能存放几条消息) */
INT16U OSQEntries; /* 当前存储的消息数量 */
} OS_Q;
消息队列的创建
创建消息队列的函数为OSQCreate(void**start****,INT16Usize****),其中start表示传入的消息队列首地址,size表示队列的大小(可以存几条消息)。
cpp
//os_q.c
OS_EVENT *OSQCreate (void **start,
INT16U size)
{
OS_EVENT *pevent;
OS_Q *pq;
#if OS_CRITICAL_METHOD == 3u /* 初始化临界区变量 */
OS_CPU_SR cpu_sr = 0u;
#endif
#ifdef OS_SAFETY_CRITICAL_IEC61508
if (OSSafetyCriticalStartFlag == OS_TRUE) {
OS_SAFETY_CRITICAL_EXCEPTION();
}
#endif
if (OSIntNesting > 0u) { /* 不能在中断中调用 */
return ((OS_EVENT *)0);
}
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 (pq != (OS_Q *)0) { /* 如果获取的消息队列控制块非空,则 */
OSQFreeList = OSQFreeList->OSQPtr; /* 将链表头指向下一个,完成头一个的脱离 */
OS_EXIT_CRITICAL();
pq->OSQStart = start; /* 开始初始化消息队列 */
pq->OSQEnd = &start[size];
pq->OSQIn = start;
pq->OSQOut = start;
pq->OSQSize = size;
pq->OSQEntries = 0u;
pevent->OSEventType = OS_EVENT_TYPE_Q; //事件控制块类型为消息队列
pevent->OSEventCnt = 0u; //消息队列不需要该成员
pevent->OSEventPtr = pq; //具体的事件结构指针指向QS_Q类型
#if OS_EVENT_NAME_EN > 0u
pevent->OSEventName = (INT8U *)(void *)"?";
#endif
OS_EventWaitListInit(pevent); /* 将事件等待表清空 */
} else { /* 如果获得的消息队列控制块为空,说明系统设置的余量不足,则将一切复原回创建之前的状态 */
pevent->OSEventPtr = (void *)OSEventFreeList;
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
pevent = (OS_EVENT *)0;
}
}
return (pevent); //返回创建好的事件控制块
}
消息队列的操作
OSQPend(OS_EVENT *pevent, INT32U timeout, INT8U *perr)
功能描述:等待消息队列中的消息,如果消息非空,则立即执行,否则阻塞等待新的消息到来。该函数还支持等待超时,当timeout大于0时,等待timeout事件后系统会自动将该任务就绪,如传入timeout为0,则会一直等待直到有消息到来。
cpp
//os_q.c
void *OSQPend (OS_EVENT *pevent,
INT32U timeout,
INT8U *perr)
{
void *pmsg;
OS_Q *pq;
#if OS_CRITICAL_METHOD == 3u /* 初始化临界区变量 */
OS_CPU_SR cpu_sr = 0u;
#endif
#ifdef OS_SAFETY_CRITICAL
if (perr == (INT8U *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
}
#endif
#if OS_ARG_CHK_EN > 0u
if (pevent == (OS_EVENT *)0) { /* 事件不能为空 */
*perr = OS_ERR_PEVENT_NULL;
return ((void *)0);
}
#endif
if (pevent->OSEventType != OS_EVENT_TYPE_Q) { /* 事件必须是消息队列类型 */
*perr = OS_ERR_EVENT_TYPE;
return ((void *)0);
}
if (OSIntNesting > 0u) { /* 不能在中断中调用 */
*perr = OS_ERR_PEND_ISR;
return ((void *)0);
}
if (OSLockNesting > 0u) { /* 当调度器被上锁是不能调用 */
*perr = OS_ERR_PEND_LOCKED;
return ((void *)0);
}
OS_ENTER_CRITICAL();
pq = (OS_Q *)pevent->OSEventPtr; /* 获取消息队列控制块指针 */
if (pq->OSQEntries > 0u) { /* 查看是否有消息存储 */
pmsg = *pq->OSQOut++; /* 如果有,弹出最旧的消息 */
pq->OSQEntries--; /* 将消息存储数量减1 */
if (pq->OSQOut == pq->OSQEnd) { /* 如果读指针已经在最后一条,则将读指针指向首条 */
pq->OSQOut = pq->OSQStart;
}
OS_EXIT_CRITICAL();
*perr = OS_ERR_NONE;
return (pmsg); /* 返回读取的消息 */
}
OSTCBCur->OSTCBStat |= OS_STAT_Q; /* 任务状态变成"等待消息队列" */
OSTCBCur->OSTCBStatPend = OS_STAT_PEND_OK;
OSTCBCur->OSTCBDly = timeout; /* 填上传入的超时时长 */
OS_EventTaskWait(pevent); /* 将任务和事件双向链接,详见信号量章节 */
OS_EXIT_CRITICAL();
OS_Sched(); /* 进行上下文切换,下面的代码只有在任务重新就绪后才有机会运行 */
OS_ENTER_CRITICAL(); //再次回到这里执行,说明任务已经就绪,只有以下三种可能原因
switch (OSTCBCur->OSTCBStatPend) { /* 通过pend状态查看任务重新执行的原因 */
case OS_STAT_PEND_OK: /* 有新消息入队导致任务就绪 */
pmsg = OSTCBCur->OSTCBMsg; //在等待期间,有信息传入时会传入一个临时空间,从该空间取出最新消息
*perr = OS_ERR_NONE;
break;
case OS_STAT_PEND_ABORT: /* 别的任务执行了OSQPendAbort导致pend结束 */
pmsg = (void *)0; //Abort导致的就绪,消息只能填空
*perr = OS_ERR_PEND_ABORT;
break;
case OS_STAT_PEND_TO: //等待超时导致的就绪
default:
OS_EventTaskRemove(OSTCBCur, pevent); //取消事件对任务的绑定
pmsg = (void *)0; //超时导致的就绪,消息只能填空
*perr = OS_ERR_TIMEOUT;
break;
}
OSTCBCur->OSTCBStat = OS_STAT_RDY; /* 设置任务状态为就绪 */
OSTCBCur->OSTCBStatPend = OS_STAT_PEND_OK; /* 设置pend状态为OK */
OSTCBCur->OSTCBEventPtr = (OS_EVENT *)0; /* 清除任务对事件的绑定 */
#if (OS_EVENT_MULTI_EN > 0u)
OSTCBCur->OSTCBEventMultiPtr = (OS_EVENT **)0;
#endif
OSTCBCur->OSTCBMsg = (void *)0;
OS_EXIT_CRITICAL();
return (pmsg); /* 返回收到的消息 */
}
OSQPost(OS_EVENT *pevent, void *pmsg)
功能描述:传入一条新消息给对应的消息队列,当没有任务等待该消息队列时,直接将新消息存储到队列中。如果有任务正在等待,则直接传给最高优先级的等待任务,并设置该任务就绪,此时消息不会填装到队列里。
cpp
//os_q.c
INT8U OSQPost (OS_EVENT *pevent,
void *pmsg)
{
OS_Q *pq;
#if OS_CRITICAL_METHOD == 3u /* 初始化临界区变量 */
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_ARG_CHK_EN > 0u
if (pevent == (OS_EVENT *)0) { /* 事件不能为空 */
return (OS_ERR_PEVENT_NULL);
}
#endif
if (pevent->OSEventType != OS_EVENT_TYPE_Q) { /* 传入事件类型必须是消息队列 */
return (OS_ERR_EVENT_TYPE);
}
OS_ENTER_CRITICAL();
if (pevent->OSEventGrp != 0u) { /* 如果有任务在等待消息 */
/* 使等待消息的优先级最级的任务进入就绪状态,并将该消息传入,详见信号量章节 */
(void)OS_EventTaskRdy(pevent, pmsg, OS_STAT_Q, OS_STAT_PEND_OK);
OS_EXIT_CRITICAL();
OS_Sched(); /* 尝试上下文切换 */
return (OS_ERR_NONE);
}
pq = (OS_Q *)pevent->OSEventPtr; /* 暂时没有任务等待消息,则存储该消息到队列中 */
if (pq->OSQEntries >= pq->OSQSize) { /* 消息量不能超过队列长度 */
OS_EXIT_CRITICAL();
return (OS_ERR_Q_FULL);
}
*pq->OSQIn++ = pmsg; /* 将消息填装进队列中 */
pq->OSQEntries++; /* 消息队列的消息数加1 */
if (pq->OSQIn == pq->OSQEnd) { /* 如果写指针超出队列末尾,则调整写指针指向队首 */
pq->OSQIn = pq->OSQStart;
}
OS_EXIT_CRITICAL();
return (OS_ERR_NONE);
}
OSQPostFront(OS_EVENT *pevent, void *pmsg)
功能描述:基本功能和OSQPost() 相同,但OSQPostFront() 总是传入消息到下一个读出的位置,具体是通过操作读指针OSQOut来达到这一目的的(前者是通过OSQIn来填装消息的)。
OSQPendAbort(OS_EVENT *pevent, INT8U opt, INT8U *perr)
功能描述:将正在pend等待的任务设置为就绪状态,详见信号量章节的**OSSemPendAbort()**函数,功能基本一样。
OSQAccept(OS_EVENT *pevent, INT8U *perr)
功能描述:与OSQPend() 类似,从消息队列中取一条消息,区别在于,OSQAccept()无论能否取到消息,都不会阻塞等待,而是立即返回执行(没有消息时返回0)。这也是一个比较常用的API,当一个任务要做的事情很多时,使用它防止被某一部分阻塞(多数情况下最好拆分称两个任务分开执行)。
OSQFlush(OS_EVENT *pevent)
功能描述:清空消息队列。
消息队列的删除
对应函数为OSQDel(),具体实现可以参考信号量章节的OSSemDel()。