UCOS你问我答系列之消息队列详解

消息队列是UCOS系统的一种通讯机制,它可以使任务向任务或者中断向任务发送一个指针变量.指针变量指向的数据结构由用户自定义,即我们常说的消息.首先来看一下UCOS消息队列结构体的设计.

本文使用的UCOS版本:V2.91.

消息队列结构体设计

消息队列结构体的本质设计是一个环形缓冲区.OSQStart,OSQEnd标识唤醒缓冲区的首尾边界. OSQIn,OSQOut在环形缓冲区内移动,标识当前队列内数据变化情况.

typedef struct os_q {                   /* QUEUE CONTROL BLOCK       */
    /* 在空闲队列链表连接所有的控制块.建立控制块之间的链式关系. */
    struct os_q   *OSQPtr;              /* Link to next queue control block in list of free blocks     */
    /* 指向消息队列的指针数组的起始地址的指针. 即用户创建消息队列是传入的指针数组 */
    void         **OSQStart;            /* Pointer to start of queue data                              */
    /* 指向消息队列的的指针数组最后一项的的下一个地址的指针.消息队列构成环形缓冲区. */
    void         **OSQEnd;              /* Pointer to end   of queue data                              */
    /* 指向消息队列插入下一条消息的位置的指针.  */
    void         **OSQIn;               /* Pointer to where next message will be inserted  in   the Q  */
    /* 指向消息队列取出下一条消息的位置的指针. */
    void         **OSQOut;              /* Pointer to where next message will be extracted from the Q  */
    /* 创建的消息队列的消息个数. */
    INT16U         OSQSize;             /* Size of queue (maximum number of entries)                   */
    /* 消息队列中当前已存储的消息的数量. 消息入队时加1 出队时减1. * /
    INT16U         OSQEntries;          /* Current number of entries in the queue                      */
} OS_Q;

消息队列创建

消息队列创建的传参需要一个指针数组来指向各个消息的指针.size表示创建消息的大小. OSQCreate函数的详解在代码内以注释的形式体现.

OS_EVENT  *OSQCreate (void    **start,
                      INT16U    size)
{
    OS_EVENT  *pevent;
    OS_Q      *pq;
#if OS_CRITICAL_METHOD == 3u                     /* Allocate storage for CPU status register           */
    OS_CPU_SR  cpu_sr = 0u;
#endif

    // 中断中不允许创建消息队列.
    if (OSIntNesting > 0u) {                     /* See if called from ISR ...                         */
        return ((OS_EVENT *)0);                  /* ... can't CREATE from an ISR                       */
    }
    OS_ENTER_CRITICAL();
    // 找到空闲可用的事件控制块.
    pevent = OSEventFreeList;                    /* Get next free event control block                  */
    if (OSEventFreeList != (OS_EVENT *)0) {      /* See if pool of free ECB pool was empty             */
        OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
    }
    OS_EXIT_CRITICAL();
    /*
        找到一个可用的消息控制块.(OSQTbl[]数组中的空闲可用项)设置消息控制块的相关属性.
        pevent->OSEventPtr     = pq; // 通过事件控制块的OSEventPtr指针即可获取到整个消息控制块的属性.
        OS_EventWaitListInit清除 OSEventGrp OSEventTbl值.
        函数创建完成后, 我们得到了一个OS_EVENT *类型的事件控制块指针,通过这个事件控制块指针的OSEventPtr
        指针变量可以访问到我们整个消息控制块.
    */
    if (pevent != (OS_EVENT *)0) {               /* See if we have an event control block              */
        OS_ENTER_CRITICAL();
        pq = OSQFreeList;                        /* Get a free queue control block                     */
        if (pq != (OS_Q *)0) {                   /* Were we able to get a queue control block ?        */
            OSQFreeList            = OSQFreeList->OSQPtr; /* Yes, Adjust free list pointer to next free*/
            OS_EXIT_CRITICAL();
            pq->OSQStart           = start;               /*      Initialize the queue                 */
            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;
#if OS_EVENT_NAME_EN > 0u
            pevent->OSEventName    = (INT8U *)(void *)"?";
#endif
            OS_EventWaitListInit(pevent);                 /*      Initalize the wait list              */
        } else {
            pevent->OSEventPtr = (void *)OSEventFreeList; /* No,  Return event control block on error  */
            OSEventFreeList    = pevent;
            OS_EXIT_CRITICAL();
            pevent = (OS_EVENT *)0;
        }
    }
    return (pevent);
}

请求消息队列

OSQPend函数的详解在代码内以注释的形式体现.

void  *OSQPend (OS_EVENT  *pevent,
                INT32U     timeout,
                INT8U     *perr)
{
    void      *pmsg;
    OS_Q      *pq;
#if OS_CRITICAL_METHOD == 3u                     /* Allocate storage for CPU status register           */
    OS_CPU_SR  cpu_sr = 0u;
#endif

#if OS_ARG_CHK_EN > 0u
    if (pevent == (OS_EVENT *)0) {               /* Validate 'pevent'                                  */
        *perr = OS_ERR_PEVENT_NULL;
        return ((void *)0);
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_Q) {/* Validate event block type                          */
        *perr = OS_ERR_EVENT_TYPE;
        return ((void *)0);
    }
    if (OSIntNesting > 0u) {                     /* See if called from ISR ...                         */
        *perr = OS_ERR_PEND_ISR;                 /* ... can't PEND from an ISR                         */
        return ((void *)0);
    }
    if (OSLockNesting > 0u) {                    /* See if called with scheduler locked ...            */
        *perr = OS_ERR_PEND_LOCKED;              /* ... can't PEND when locked                         */
        return ((void *)0);
    }
    OS_ENTER_CRITICAL();
    /*
        请求消息队列时, 可以通过pevent->OSEventPtr指针获取到消息队列指针.如果消息队列中存在消息.
        就取出消息(获取消息首地址).并向后移动OSQOut. if (pq->OSQOut == pq->OSQEnd)是对环形缓冲区进行边界控制.

    */
    pq = (OS_Q *)pevent->OSEventPtr;             /* Point at queue control block                       */
    if (pq->OSQEntries > 0u) {                   /* See if any messages in the queue                   */
        pmsg = *pq->OSQOut++;                    /* Yes, extract oldest message from the queue         */
        pq->OSQEntries--;                        /* Update the number of entries in the queue          */
        if (pq->OSQOut == pq->OSQEnd) {          /* Wrap OUT pointer if we are at the end of the queue */
            pq->OSQOut = pq->OSQStart;
        }
        OS_EXIT_CRITICAL();
        *perr = OS_ERR_NONE;
        return (pmsg);                           /* Return message received                            */
    }
    /*
        如果还没有消息,会运行到这里. 设置当前任务控制块的相关状态属性.
        OS_EventTaskWait函数将当前任务的优先级放入此消息队列的结构体成员变量,
        并将此任务的优先级从就绪表中移除.然后触发一次任务调度.CPU转到其他任务运行.
        此任务即处于了挂起等待消息的状态.
    */
    OSTCBCur->OSTCBStat     |= OS_STAT_Q;        /* Task will have to pend for a message to be posted  */
    OSTCBCur->OSTCBStatPend  = OS_STAT_PEND_OK;
    OSTCBCur->OSTCBDly       = timeout;          /* Load timeout into TCB                              */
    OS_EventTaskWait(pevent);                    /* Suspend task until event or timeout occurs         */
    OS_EXIT_CRITICAL();
    OS_Sched();                                  /* Find next highest priority task ready to run       */
    /*
        当收到消息或超时时间到导致任务被放入就绪表, 且被调度器调度时,会从这里开始接着运行.
        先检查pend到消息的原因, 如果是等待超时,就将此任务的优先级从信号量的等待事件中移除.
        如果是正常收到消息. 则从OSTCBCur->OSTCBMsg取出消息.
    */
    OS_ENTER_CRITICAL();
    switch (OSTCBCur->OSTCBStatPend) {                /* See if we timed-out or aborted                */
        case OS_STAT_PEND_OK:                         /* Extract message from TCB (Put there by QPost) */
             pmsg =  OSTCBCur->OSTCBMsg;
            *perr =  OS_ERR_NONE;
             break;

        case OS_STAT_PEND_ABORT:
             pmsg = (void *)0;
            *perr =  OS_ERR_PEND_ABORT;               /* Indicate that we aborted                      */
             break;

        case OS_STAT_PEND_TO:
        default:
             OS_EventTaskRemove(OSTCBCur, pevent);
             pmsg = (void *)0;
            *perr =  OS_ERR_TIMEOUT;                  /* Indicate that we didn't get event within TO   */
             break;
    }
    OSTCBCur->OSTCBStat          =  OS_STAT_RDY;      /* Set   task  status to ready                   */
    OSTCBCur->OSTCBStatPend      =  OS_STAT_PEND_OK;  /* Clear pend  status                            */
    OSTCBCur->OSTCBEventPtr      = (OS_EVENT  *)0;    /* Clear event pointers                          */
#if (OS_EVENT_MULTI_EN > 0u)
    OSTCBCur->OSTCBEventMultiPtr = (OS_EVENT **)0;
#endif
    OSTCBCur->OSTCBMsg           = (void      *)0;    /* Clear  received message                       */
    OS_EXIT_CRITICAL();
    return (pmsg);                                    /* Return received message                       */
}

消息队列发送

OSQPost函数用于一个任务或者中断服务子程序向另一个任务发送消息.OSQPost函数的详解在代码内以注释的形式体现.

INT8U  OSQPost (OS_EVENT  *pevent,
                void      *pmsg)
{
    OS_Q      *pq;
#if OS_CRITICAL_METHOD == 3u                           /* Allocate storage for CPU status register     */
    OS_CPU_SR  cpu_sr = 0u;
#endif

#if OS_ARG_CHK_EN > 0u
    if (pevent == (OS_EVENT *)0) {                     /* Validate 'pevent'                            */
        return (OS_ERR_PEVENT_NULL);
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_Q) {      /* Validate event block type                    */
        return (OS_ERR_EVENT_TYPE);
    }
    OS_ENTER_CRITICAL();
    /*
        pevent->OSEventGrp非0表示有任务在等待接收消息, 此时调用OS_EventTaskRdy函数,找到等待此消息的那个任务的
        优先级, 将此任务放入就绪表并从等待事件表中移除.然后触发一次任务调度.(如果等待消息的那个任务就绪表中
        优先级最高的任务,就会立即运行.)
    */
    if (pevent->OSEventGrp != 0u) {                    /* See if any task pending on queue             */
                                                       /* Ready highest priority task waiting on event */
        (void)OS_EventTaskRdy(pevent, pmsg, OS_STAT_Q, OS_STAT_PEND_OK);
        OS_EXIT_CRITICAL();
        OS_Sched();                                    /* Find highest priority task ready to run      */
        return (OS_ERR_NONE);
    }
    /*
        如果在发送消息的时候.没有检测到有任务在等待消息. 就会运行到这里, 获取到消息指针pq,
        首先进行缓冲区判满操作,若消息缓冲区不满, 就将消息指针放入OSQIn指向的位置并移动OSQIn的
        位置,并累加消息计数值OSQEntries.if (pq->OSQIn == pq->OSQEnd)是对消息缓冲区进行边界控制.
    */
    pq = (OS_Q *)pevent->OSEventPtr;                   /* Point to queue control block                 */
    if (pq->OSQEntries >= pq->OSQSize) {               /* Make sure queue is not full                  */
        OS_EXIT_CRITICAL();
        return (OS_ERR_Q_FULL);
    }
    *pq->OSQIn++ = pmsg;                               /* Insert message into queue                    */
    pq->OSQEntries++;                                  /* Update the nbr of entries in the queue       */
    if (pq->OSQIn == pq->OSQEnd) {                     /* Wrap IN ptr if we are at end of queue        */
        pq->OSQIn = pq->OSQStart;
    }
    OS_EXIT_CRITICAL();
    return (OS_ERR_NONE);
}

问答环节

1.消息队列是怎样的传递消息的. 
UCOS消息队列传递的是指向待传送数据的首地址的指针.UCOS的消息队列的消息传送并不产生内存拷贝.消息发送方组包好消息后,将发送消息的首地址放入消息队列.消息接收方从消息队列取出消息指针,按照约定的消息格式解析消息.
2.消息队列使用时有哪些注意事项.
因为UCOS消息队列是指针传递, 对于要传递的消息,可以是一块动态分配的内存.(注意这种情况需要处理好内存的分配释放操作,避免产生内存泄漏).也可以是全局变量/数据/自定义数据结构.或者是函数指针. 因为是指针形式传递,所以千万不能传递局部变量.如果消息内存是使用动态内存分配的话,需要做好内存管理.发送失败时在发送方释放内存, 发送成功时在接收方接收成功并处理消息后释放内存.避免产生内存泄漏.


版权声明:本文为w___dream原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。