5.5.11 · D5Embedded Systems & Real-Time Software

Question bank — FreeRTOS IPC — queues, semaphores, mutexes, event groups

1,487 words7 min readBack to topic

True or false — justify

Every statement below is either subtly right or subtly wrong. Say why before you look.

A queue always transfers data faster than copying manually, because the kernel is optimized.
False — a queue copies the bytes just like you would, plus adds list bookkeeping and a critical section. Its value is safety and blocking, not raw speed.
A binary semaphore and a mutex are interchangeable because both are "count of 1".
False — they share the counter mechanism but a mutex has an owner and priority inheritance; a binary semaphore has neither, so only the mutex is safe as a lock.
A counting semaphore can be given more times than it was taken.
True — Give just increments the counter up to its max; you can pre-load it or let events accumulate. That "surplus count" is exactly how it remembers events that arrived while a task was busy.
xQueueSend on a full queue with xTicksToWait = 0 returns an error rather than blocking.
True — a zero timeout means "try, but never sleep." It returns pdFALSE (errQUEUE_FULL) immediately, which is the correct non-blocking pattern.
An event group can wake several tasks from a single bit-set operation.
True — because tasks wait on bits, setting one bit can satisfy the wait-mask of many blocked tasks at once, so it broadcasts. A semaphore's Give wakes only one taker.
Priority inheritance permanently raises the low-priority task's priority.
False — the boost is temporary; the moment the low task releases the mutex, the kernel restores its original priority.
A mutex can be recursively taken by the same task any number of times without deadlock (in FreeRTOS).
True only for a recursive mutex (xSemaphoreCreateRecursiveMutex). A plain mutex taken twice by its own owner deadlocks that task on itself.
Sending a pointer through a queue is always wrong.
False — it's a legitimate pattern for big payloads, provided you transfer ownership explicitly (sender stops touching it, receiver frees it). The trap is a dangling pointer if lifetimes aren't managed.

Spot the error

Each snippet or claim has one defect. Name it and give the fix.

xSemaphoreTake(mutex, portMAX_DELAY) called inside an ISR to lock a shared buffer.
Two errors: ISRs must not block (no portMAX_DELAY) and mutexes have no meaning in an ISR (no owner). Use a ...FromISR semaphore to signal, and do the locking in a task.
A task does xSemaphoreTake(m,…), and a different task later does xSemaphoreGive(m) on the same mutex.
A mutex may only be given back by the task that took it. This "give it from elsewhere" pattern violates ownership — use a binary semaphore if you truly want a hand-off between different tasks.
xQueueSend(q, v, …) where v is a uint32_t value (not &v).
Send takes a pointer to the item to copy, so you must pass &v. Passing the value reinterprets the number as an address and copies garbage.
An ISR calls xSemaphoreGiveFromISR(sem, &woke) but never calls portYIELD_FROM_ISR(woke).
The higher-priority woken task won't run until the next scheduler tick, adding latency. The woke flag must trigger an immediate context switch on ISR exit.
xEventGroupWaitBits(eg, MASK, pdFALSE, pdTRUE, …) used in a loop expecting fresh triggers each cycle.
pdFALSE for the clear-flag leaves the bits set, so the wait returns instantly on the stale bits every loop. Use pdTRUE to auto-clear on exit for edge-style events.
A "lock" implemented as: while(flag) {} flag = 1; …; flag = 0; in each task, no kernel object.
The check-and-set isn't atomic — a preemption between reading flag and setting it lets two tasks both enter. This is the race condition IPC objects exist to eliminate; use a mutex.

Why questions

Answer the "why", not just the "what".

Why does a queue copy by value instead of storing the sender's pointer by default?
Because the sender's local variable can go out of scope or be overwritten the instant it's preempted. Copying makes the message self-contained and lifetime-safe.
Why is a semaphore described as "a queue with itemSize = 0"?
FreeRTOS literally reuses queue code: Give ≡ Send with no data, Take ≡ Receive with no data, and the count is the number of zero-byte items present. One implementation, two faces.
Why must ISRs use the ...FromISR API variants?
The regular APIs may block, and an ISR has no task context to block/reschedule — blocking there would freeze the system. The FromISR variants never block and report a needed yield via the woke flag.
Why does a mutex prevent unbounded priority inversion but a binary semaphore doesn't?
Only the mutex records an owner, so the kernel knows whom to boost. It temporarily lifts the low-priority holder to the waiter's priority so a medium task can't indefinitely preempt it.
Why prefer "give a semaphore from the ISR, handle work in a task" over doing the work in the ISR?
ISRs must stay short to keep interrupt latency low; the semaphore defers heavy processing to a schedulable task (deferred interrupt processing).
Why can't several separate semaphores replace one event group for an AND-condition?
You'd have to take them one at a time, and a task can only block on one object at a time — so you can't cleanly wait for "A and B" as a single atomic condition. The event group waits on a combined mask.
Why is portMAX_DELAY on a consumer's xQueueReceive often good design, not lazy?
A blocked task is removed from the ready list, so the CPU spends zero cycles busy-waiting. The kernel wakes it precisely when data arrives — see Producer–Consumer Pattern.

Edge cases

The scenarios the naive mental model forgets.

What does xSemaphoreTake return when the timeout expires and the count was still 0?
It returns pdFALSE. The task then continues without holding the resource, so you must check the return value before assuming you got it.
A counting semaphore reaches its maximum count and another Give arrives — what happens?
The Give is rejected (returns pdFALSE) and the count stays at max; the extra event is lost. Size the max to the worst-case burst so you don't silently drop signals.
Two tasks of the same priority both block on the same empty queue; a Send arrives — who wakes?
The task that has been waiting longest is unblocked (FIFO among equal priorities). Priority is the primary key; wait order breaks ties.
A high-priority task holds a mutex and then blocks on another mutex held by a low task — does inheritance chain?
FreeRTOS supports basic inheritance but not a full chain across nested mutexes; deep nesting can still cause inversion. The safe design is to avoid holding two mutexes at once (or always lock in a fixed order to avoid deadlock).
An event group WaitBits with xWaitForAllBits = pdTRUE and xClearOnExit = pdTRUE times out — are the bits cleared?
No — clearing happens only when the wait is satisfied. On timeout the bits are left untouched, so a later cycle can still see them.
A queue is created with xQueueCreate(0, …) (length 0).
This is invalid — a queue must hold at least one item, and creation returns NULL. Always check the handle before using it (also relevant to heap allocation failures).
An ISR gives a binary semaphore that a task has already given (so it's full) — is that a problem?
A binary semaphore saturates at count 1, so the redundant give is simply dropped. You lose the "second event happened" information — switch to a counting semaphore if each event must be remembered.

Recall One-line self-test

Cover every answer above and re-derive the justification. If any answer collapsed to a bare "yes/no" in your head, that's the concept to revisit in the parent note.