Inter-Task Communication
Queues, Semaphores, Mutexes.
In any real-time application, multiple tasks must exchange data and coordinate actions. If tasks communicate without proper synchronization, shared data can become corrupted, leading to unpredictable system behavior. The RTOS provides three fundamental inter-task communication primitives: queues, semaphores, and mutexes. Each serves a distinct purpose and understanding when to use each is critical for both embedded development and GATE preparation.
Core Concept Explanation
A queue is the primary data passing mechanism between tasks. It is a fixed-size FIFO buffer that stores complete copies of data items. A producer task writes items into the queue and a consumer task reads them. If the queue is full, the producer blocks (waits) until space is available. If the queue is empty, the consumer blocks until data arrives. This blocking behavior automatically synchronizes producer and consumer without any additional code.
A semaphore is a signaling primitive with an internal counter. A binary semaphore has a counter that can only be 0 or 1, and is used to signal events between tasks or from an ISR to a task. A counting semaphore has a counter that can range from 0 to a maximum value, and is used to track the availability of multiple identical resources (like a pool of buffers). The give() operation increments the count; the take() operation decrements it. If count is 0 when take() is called, the calling task blocks.
A mutex (mutual exclusion lock) is similar to a binary semaphore but with two critical differences. First, it has ownership: only the task that locked the mutex can unlock it. Second, it supports priority inheritance, which prevents priority inversion (discussed in the next article). Mutexes are used exclusively to protect shared resources such as global variables, peripheral registers, or memory buffers. Unlike semaphores, mutexes cannot be given from an ISR.
Mathematical Expression
The queue throughput is limited by the smaller of the producer and consumer rates. If the producer sends at rate Rp items per second and the consumer processes at rate Rc items per second, the queue will fill up if Rp exceeds Rc. For a queue of depth D items, the maximum burst the queue can absorb before blocking is D items. The time until the queue fills up from a burst of items at rate Rp with consumer at rate Rc is: T_fill = D / (Rp - Rc) seconds, valid when Rp is greater than Rc.
For semaphore-based synchronization, the wait latency is bounded by the priority of the task calling take() and the time the semaphore is unavailable. In a preemptive RTOS with proper priority assignment, a high-priority task will be unblocked within one context switch time after the semaphore is given.
Practical Understanding
In FreeRTOS, queues are created with xQueueCreate(length, item_size). Items are sent with xQueueSend() and received with xQueueReceive(). Both functions accept a timeout parameter: if the operation cannot complete within the timeout, the function returns errQUEUE_FULL or errQUEUE_EMPTY instead of blocking indefinitely. This prevents deadlocks in production systems.
A common pattern in embedded systems is ISR-to-task communication using binary semaphores or queues. When a hardware interrupt fires (for example, UART data received), the ISR should not process data directly because ISR execution time must be minimal. Instead, the ISR gives a semaphore or writes to a queue, which unblocks a dedicated handler task to process the data. This keeps interrupt latency minimal while allowing complex processing in task context.
Given:
Producer task: sends sensor data at 500 items/second
Consumer task: processes data at 400 items/second
Queue depth: 20 items
Item size: 4 bytes each
Why this formula applies:
Queue fills up because producer is faster than consumer.
T_fill = D / (Rp - Rc)
Formula:
T_fill = Queue_depth / (Producer_rate - Consumer_rate)
Queue RAM = depth x item_size
Substitution:
T_fill = 20 / (500 - 400) = 20 / 100
Queue RAM = 20 x 4 = 80 bytes
Calculation:
T_fill = 0.2 seconds
Queue RAM = 80 bytes
Final Answer:
Queue fills up in 0.2 seconds. Producer will block after 0.2s.
Queue needs only 80 bytes of RAM.Exam Tip: A key GATE trap - Semaphores have NO ownership. Any task can give a semaphore even if it did not take it. Mutexes have ownership: only the task that took the mutex can give it back. This is why mutexes support priority inheritance but semaphores do not.
Mechanism - How Each Primitive Works
- Queue send: kernel copies item into queue buffer, increments write pointer. If a task was blocked waiting for data, kernel unblocks it and may trigger preemption.
- Queue receive: kernel copies item out of queue buffer, decrements item count. If a task was blocked waiting for space, kernel unblocks it.
- Semaphore give: kernel increments counter. If a task was blocked on take(), kernel moves it to Ready state.
- Semaphore take: if counter is greater than 0, decrement and proceed. If counter is 0, block the calling task until give() is called.
- Mutex take: if mutex is free, lock it and record owning task. If already locked, the calling task blocks. On mutex give(), ownership is released and waiting task unblocked.
Quick Revision
- Queue: FIFO data buffer between tasks. Copies data. Blocks on full (producer) or empty (consumer). Suitable for structured data passing.
- Binary semaphore: value 0 or 1. Used for event signaling. Can be given from ISR. No ownership.
- Counting semaphore: value 0 to N. Used for resource pool management (N identical resources).
- Mutex: binary, has ownership. Only locker can unlock. Supports priority inheritance. Cannot be given from ISR.
- T_fill = Queue_depth / (Rp - Rc). Queue fills in this time when producer is faster than consumer.
- Exam trap: Never use mutex from an ISR. Use binary semaphore for ISR-to-task signaling. This distinction appears frequently in GATE questions.
Inter-Task Communication Quiz
Test your understanding of queues, semaphores, and mutexes in RTOS inter-task communication.