EmbeddedRelated.com

Message Queue

Category: Rtos | Also known as: message queues

A message queue is a synchronization and communication primitive that lets tasks, ISRs, and processes exchange discrete data items through a fixed-capacity, FIFO buffer, decoupling the producer and consumer in both time and execution context. It is common in RTOSes but also appears in general-purpose OS IPC and bare-metal firmware implementations. Items are typically copied into and out of the queue by value on most RTOS implementations, so neither side needs to share a pointer or coordinate memory ownership directly, though some systems queue pointers or use zero-copy patterns.

In practice

In bare-metal or RTOS-based firmware, a message queue is one of the primary ways to pass work from an ISR to a task without polling. The ISR writes a small struct or integer into the queue and returns immediately; the receiving task blocks on the queue until an item arrives, consuming CPU only when there is something to process. FreeRTOS exposes this pattern through `xQueueSend` / `xQueueSendFromISR` and `xQueueReceive`; Zephyr uses `k_msgq_put` / `k_msgq_get`; ThreadX uses `tx_queue_send` / `tx_queue_receive`. The API names differ, and the specific semantics around ISR use, blocking rules, and data copying vary across RTOSes, though the underlying producer-consumer pattern is broadly shared.

Queue depth and item size are chosen at creation time and are fixed for the lifetime of the queue on most RTOSes. A common sizing mistake is making the queue too shallow: under burst conditions the producer fills it and either blocks, drops items, or triggers an error, depending on the timeout passed. Profiling queue high-water marks during stress tests catches this early. Conversely, a very deep queue can hide a design problem where the consumer is simply too slow and latency is silently growing.

Message queues work well as the communication backbone for event-driven state machines, a pattern discussed in "Finite State Machines (FSM) in Embedded Systems (Part 4) - Let 'em talk". Each state machine task sits in a loop that blocks on its input queue; external events, timer expirations, and messages from other tasks all arrive as queue items, keeping the state machine decoupled from interrupt context. The blog post "You Don't Need an RTOS (Part 4)" also examines how queue-like structures can be built on bare-metal systems when a full RTOS is not warranted.

Because items are copied by value, the practical item size is usually kept small: a command enum plus a small payload struct, or a pointer to a separately allocated buffer. Passing large structs by value increases copy overhead and inflates the queue's RAM footprint. When payloads are variable in size or large, a common pattern is to pass a pointer to a memory-pool block through the queue and define clear ownership rules about which side frees the block.

Frequently asked

What happens when a task tries to send to a full queue?
Behavior depends on the timeout parameter supplied to the send call. With a zero timeout the call returns immediately with an error code. With a finite or infinite timeout the calling task blocks until space becomes available or the timeout expires. In an ISR context, most RTOSes only permit a zero timeout, so the ISR must handle the failure explicitly rather than block.
How is a message queue different from a mailbox?
The terms are used inconsistently across RTOSes. Some RTOSes define a mailbox as a single-slot storage primitive, effectively limiting it to holding one item at a time, while a message queue holds multiple items in FIFO order. The exact semantics of a mailbox, including whether it blocks, overwrites, or signals, vary significantly by RTOS. Always check the specific RTOS documentation because the naming and behavior are not standardized.
Is a message queue safe to use from both an ISR and a task simultaneously?
Yes, that is one of the primary design goals of a message queue. The RTOS internally protects the queue structure using a critical section or similar mechanism. However, many RTOSes provide separate ISR-safe API variants (e.g., FreeRTOS `xQueueSendFromISR`) that avoid operations that could block, since blocking inside an ISR is not permitted. The exact constraints and available variants differ by RTOS, so consulting the documentation for your specific kernel is important.
When should I use a message queue instead of a shared global variable protected by a mutex?
A message queue is preferable when you want to transfer ownership of data, buffer multiple events, or avoid having the consumer poll for changes. A mutex-protected global is simpler when the producer and consumer only need to share the latest value and the consumer actively reads it on demand. If an ISR is the producer, a queue is usually cleaner because an ISR cannot acquire a mutex on most RTOSes without risking priority inversion or blocking.
Can I use a message queue on a system without an RTOS?
Yes, with some manual work. A simple ring buffer with an atomic write pointer (updated only by the ISR) and a read pointer (updated only by the foreground loop) implements a single-producer, single-consumer queue without any RTOS. The 'You Don't Need an RTOS (Part 4)' blog post covers this approach. Multi-producer or multi-consumer cases require more care around concurrent access, typically handled with critical sections on small MCUs.

Differentiators vs similar concepts

Message queues are often compared to semaphores, mutexes, and mailboxes. A semaphore signals that an event occurred but carries no data payload; a message queue both signals and transfers data. A mutex is specifically for protecting shared resources through mutual exclusion and is not intended for data transfer. A mailbox (in RTOSes that distinguish it) typically holds only one item and may overwrite it rather than queuing multiple entries, making it suited for "latest value" sharing rather than ordered event delivery.