Message Queue Management
A message queue (or simply a queue) is a µC/OS-II object that allows a task or an ISR to send pointer-sized variables to another task. Each pointer typically is initialized to point to some application-specific data structure containing a message. µC/OS-II provides nine services to access message queues: OSQCreate(), OSQDel(), OSQPend(), OSQPost(), OSQPostFront(), OSQPostOpt(), OSQAccept(), OSQFlush(), and OSQQuery().
- 1 Message Queue Configuration
- 2 Creating a message queue, OSQCreate()
- 3 Deleting a message queue, OSQDel()
- 4 Waiting for a message at a queue (blocking), OSQPend()
- 5 Sending a message to a queue (FIFO), OSQPost()
- 6 Sending a message to a queue (LIFO), OSQPostFront()
- 7 Sending a message to a queue (FIFO or LIFO), OSQPostOpt()
- 8 Getting a Message without Waiting, OSQAccept()
- 9 Flushing a Queue, OSQFlush()
- 10 Obtaining the Status of a Queue, OSQQuery()
- 11 Using a Message Queue When Reading Analog Inputs
- 12 Using a Queue as a Counting Semaphore
Message Queue Configuration
To enable µC/OS-II message queue services, you must set configuration constants in OS_CFG.H. Specifically, table 11.1 shows which services are compiled based on the value of configuration constants found in OS_CFG.H. You should note that NONE of the mailbox services are enabled when OS_Q_EN is set to 0 or OS_MAX_QS is set to 0. To enable a specific feature (i.e. service), simply set the corresponding configuration constant to 1. You will notice that OSQCreate() and OSQPend() cannot be individually disabled like the other services. That’s because they are always needed when you enable µC/OS-II message mailbox management. You must enable at least one of the post services: OSQPost(), OSQPostFront() and OSQPostOpt().
µC/OS-II Queue Service | Enabled when set to 1 in |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Figure 11.1 shows a flow diagram to illustrate the relationship between tasks, ISRs, and a message queue. Note that the symbology used to represent a queue looks like a mailbox with multiple entries. In fact, you can think of a queue as an array of mailboxes, except that there is only one wait list associated with the queue. The hourglass represents a timeout that can be specified with the OSQPend() call. Again, what the pointers point to is application specific. N represents the number of entries the queue holds. The queue is full when your application calls OSQPost() [or OSQPostFront() or OSQPostOpt() ] N times before your application has called OSQPend() or OSQAccept() .
As you can see from Figure 11.1, a task or an ISR can call OSQPost(), OSQPostFront(), OSQPostOpt(), OSQFlush(), or OSQAccept(). However, only tasks are allowed to call OSQDel(), OSQPend() and OSQQuery().
Figure 11.2 shows the different data structures needed to implement a message queue.
(1) An ECB is required because you need a wait list, and using an ECB allows queue services to use some of the same code used by semaphores, mutexes and mailboxes.
(2) When a message queue is created, a queue control block (i.e., an OS_Q, see OS_Q.C) is allocated and linked to the ECB using the .OSEventPtr field in OS_EVENT.
(3) Before you create a queue, however, you need to allocate an array of pointers that contains the desired number of queue entries. In other words, the number of elements in the array corresponds to the number of entries in the queue. The starting address of the array is passed to OSQCreate() as an argument as well as the size (in number of elements) of the array. In fact, you don’t actually need to use an array as long as the memory occupies contiguous locations.
The configuration constant OS_MAX_QS in OS_CFG.H specifies how many queues you are allowed to have in your application and must be greater than 0. When µC/OS-II is initialized, a list of free queue control blocks is created as shown in Figure11.3.
A queue control block is a data structure used to maintain information about the queue. It contains the fields described below. Note that the fields are preceded with a dot to show that they are members of a structure as opposed to simple variables.
.OSQPtr links queue control blocks in the list of free queue control blocks. Once the queue is created, this field is not used.
.OSQStart contains a pointer to the start of the message queue storage area. Your application must declare this storage area before creating the queue.
.OSQEnd is a pointer to one location past the end of the queue. This pointer is used to make the queue a circular buffer.
.OSQIn is a pointer to the location in the queue where the next message will be inserted. .OSQIn is adjusted back to the beginning of the message storage area when .OSQIn equals .OSQEnd.
.OSQOut is a pointer to the next message to be extracted from the queue. .OSQOut is adjusted back to the beginning of the message storage area when .OSQOut equals .OSQEnd. .OSQOut is also used to insert a message [see OSQPostFront() and OSQPostOpt()].
.OSQSize contains the size of the message storage area. The size of the queue is determined by your application when the queue is created. Note that µC/OS-II allows the queue to contain up to 65,535 entries.
.OSQEntries contains the current number of entries in the message queue. The queue is empty when .OSQEntries is 0 and full when it equals .OSQSize. The message queue is empty when the queue is created.
A message queue is basically a circular buffer as shown in Figure 11.4.
(1)
(3) Each entry contains a pointer. The pointer to the next message is deposited at the entry pointed to by .OSQIn unless the queue is full (i.e., .OSQEntries == .OSQSize) . Depositing the pointer at .OSQIn implements a FIFO (First-In-First-Out) queue. This is what OSQPost() does.
(2) µC/OS-II implements a LIFO (Last-In-First-Out) queue by pointing to the entry preceeding .OSQOut and depositing the pointer at that location (see OSQPostFront() and OSQPostOpt()).
(4) The pointer is also considered full when .OSQEntries == .OSQSize. Message pointers are always extracted from the entry pointed to by .OSQOut.
(5) The pointers .OSQStart and .OSQEnd are simply markers used to establish the beginning and end of the array so that .OSQIn and .OSQOut can wrap around to implement this circular motion.
Creating a message queue, OSQCreate()
A message queue (or simply a queue) needs to be created before it can be used. Creating a queue is accomplished by calling OSQCreate() and passing it two arguments: a pointer to an array that will hold the messages and the size of this array. The array must be declared as an array of pointers to void as follows:
void *MyArrayOfMsg[SIZE];
You would pass the address of MyArrayOfMsg[] to OSQCreate() as well as the size of this array. The message queue is assumed to be initially empty – it doesn’t contain any messages.
The code to create a mailbox is shown in Listing 11.1.
Listing - Listing 11.1 Creating a message queue
OS_EVENT *OSQCreate (void **start, INT16U size)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr; (1)
#endif
OS_EVENT *pevent;
OS_Q *pq;
if (OSIntNesting > 0) { (2)
return ((OS_EVENT *)0);
}
OS_ENTER_CRITICAL();
pevent = OSEventFreeList; (3)
if (OSEventFreeList != (OS_EVENT *)0) {
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if (pevent != (OS_EVENT *)0) { (4)
OS_ENTER_CRITICAL();
pq = OSQFreeList;
if (pq != (OS_Q *)0) {
OSQFreeList = OSQFreeList->OSQPtr;
OS_EXIT_CRITICAL();
pq->OSQStart = start; (5)
pq->OSQEnd = &start[size];
pq->OSQIn = start;
pq->OSQOut = start;
pq->OSQSize = size;
pq->OSQEntries = 0;
pevent->OSEventType = OS_EVENT_TYPE_Q; (6)
pevent->OSEventCnt = 0;
pevent->OSEventPtr = pq;
OS_EventWaitListInit(pevent); (7)
} else {
pevent->OSEventPtr = (void *)OSEventFreeList; (8)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
pevent = (OS_EVENT *)0;
}
}
return (pevent); (9)
}(1) A local variable called cpu_sr to support OS_CRITICAL_METHOD #3 is allocated.
(2) OSQCreate() starts by making sure you are not calling this function from an ISR because this is not allowed. All kernel objects need to be created from task level code or before multitasking starts.
(3) OSQCreate() then attempts to obtain an ECB from the free list of ECBs (see Figure 6.5) and adjusts the linked list accordingly.
(4) If there is an ECB available, OSQCreate() attempts to allocate a queue control block (OS_Q) from the free list of queue control blocks (see Figure 11.3) and adjusts the linked list accordingly.
(5)
(6) If a queue control block was available from the free list, the fields of the queue control block are initialized followed by the ones of the ECB. You should note that the .OSEventType field is set to OS_EVENT_TYPE_Q so that subsequent message queue services can check the validity of the ECB.
(7) The wait list is cleared indicating that no task is currently waiting on the message queue.
(8) If an ECB was available but a queue control block was not then, the ECB is returned to the free list since we cannot satisfy the request to create a queue unless we also have a queue control block.
(9) OSQCreate() returns either a pointer to the ECB upon successfully creating a message queue or, a NULL pointer if not. This pointer must be used (if not NULL) in subsequent calls that operate on message queues. The pointer is basically used as the queue’s handle.
Deleting a message queue, OSQDel()
The code to delete a message queue is shown in listing 11.2 and this code will only be generated by the compiler if OS_Q_DEL_EN is set to 1 in OS_CFG.H. This is a function that you must use with caution because multiple tasks could attempt to access a deleted message queue. You should always use this function with great care. Generally speaking, before you would delete a message queue, you would first delete all the tasks that access the message queue.
Listing - Listing 11.2 Deleting a Message Queue
OS_EVENT *OSQDel (OS_EVENT *pevent, INT8U opt, INT8U *err)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
BOOLEAN tasks_waiting;
OS_Q *pq;
if (OSIntNesting > 0) { (1)
*err = OS_ERR_DEL_ISR;
return ((OS_EVENT *)0);
}
#if OS_ARG_CHK_EN > 0
if (pevent == (OS_EVENT *)0) { (2)
*err = OS_ERR_PEVENT_NULL;
return (pevent);
}
if (pevent->OSEventType != OS_EVENT_TYPE_Q) { (3)
*err = OS_ERR_EVENT_TYPE;
return (pevent);
}
#endif
OS_ENTER_CRITICAL();
if (pevent->OSEventGrp != 0x00) { (4)
tasks_waiting = TRUE;
} else {
tasks_waiting = FALSE;
}
switch (opt) {
case OS_DEL_NO_PEND:
if (tasks_waiting == FALSE) {
pq = pevent->OSEventPtr; (5)
pq->OSQPtr = OSQFreeList;
OSQFreeList = pq;
pevent->OSEventType = OS_EVENT_TYPE_UNUSED; (6)
pevent->OSEventPtr = OSEventFreeList; (7)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;
return ((OS_EVENT *)0); (8)
} else {
OS_EXIT_CRITICAL();
*err = OS_ERR_TASK_WAITING;
return (pevent);
}
case OS_DEL_ALWAYS:
while (pevent->OSEventGrp != 0x00) { (9)
OS_EventTaskRdy(pevent, (void *)0, OS_STAT_Q); (10)
}
pq = pevent->OSEventPtr; (11)
pq->OSQPtr = OSQFreeList;
OSQFreeList = pq;
pevent->OSEventType = OS_EVENT_TYPE_UNUSED; (12)
pevent->OSEventPtr = OSEventFreeList; (13)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
if (tasks_waiting == TRUE) {
OS_Sched(); (14)
}
*err = OS_NO_ERR;
return ((OS_EVENT *)0); (15)
default:
OS_EXIT_CRITICAL();
*err = OS_ERR_INVALID_OPT;
return (pevent);
}
}(1) OSQDel() starts by making sure that this function is not called from an ISR because that’s not allowed.
(2)
(3) If OS_ARG_CHK_EN (see OS_CFG.H) is set to 1, OSQDel() validates pevent to ensure that it’s not a NULL pointer and that it points to an ECB that was created as a queue.
(4) OSQDel() then determines whether there are any tasks waiting on the queue. The flag tasks_waiting is set accordingly.
Based on the option (i.e. opt) specified in the call, OSQDel() will either delete the queue only if no tasks are pending on the queue (opt == OS_DEL_NO_PEND) or, delete the queue even if tasks are waiting (opt == OS_DEL_ALWAYS).
(5) When opt is set to OS_DEL_NO_PEND and there is no task waiting on the queue, OSQDel() starts by returning the queue control block to the free list.
(6)
(7) OSQDel() then marks the ECB as unused and the ECB is returned to the free list of ECBs. This will allow another message queue (or any other ECB based object) to be created.
(8) You will note that OSQDel() returns a NULL pointer since, at this point, the queue should no longer be accessed through the original pointer. Because of this, you should call OSQDel() as follows:
QPtr = OSQDel(QPtr, opt, &err);
OSQDel() returns an error code if there were task waiting on the queue (i.e., OS_ERR_TASK_WAITING) because by specifying OS_DEL_NO_PEND you indicated that you didn’t want to delete the queue if there are tasks waiting on the queue.
(9)
(10) When opt is set to OS_DEL_ALWAYS then all tasks waiting on the queue will be readied. Each task will think it received a message when in fact no message has been sent. The task should examine the pointer returned to it to make sure it’s non-NULL. Also, you should note that interrupts are disabled while each task is being readied. This, of course, increases interrupt latency of your system.
(11) OSQDel() then returns the queue control block to the free list.
(12)
(13) Once all pending tasks are readied, OSQDel() marks the ECB as unused and the ECB is returned to the free list of ECBs.
(14) The scheduler is called only if there were tasks waiting on the queue.
(15) Again, you will note that OSQDel() returns a NULL pointer since, at this point, the queue should no longer be accessed through the original pointer.