Semaphores and Mutexes
Overview
Section titled “Overview”FreeRTOS gives you several tools for coordination. They do not all solve the same problem.
At a high level:
- Binary semaphore: “an event happened”
- Counting semaphore: “this happened N times” or “N resources are available”
- Mutex: “only one task may use this shared resource at a time”
If students confuse these three, designs quickly become harder to debug than they need to be.
First Question: Event Or Protection?
Section titled “First Question: Event Or Protection?”Before picking an API, ask:
- Are you trying to wake a task because something happened?
- Or are you trying to prevent two tasks from touching the same resource at once?
If the answer is:
- event -> think semaphore
- resource protection -> think mutex
Quick Decision Table
Section titled “Quick Decision Table”| Need | Best tool | Why |
|---|---|---|
| ISR wakes a task | Binary semaphore | Simple one-bit event signal |
| A task counts multiple arrivals | Counting semaphore | Preserves event count |
| Protect LCD, UART, I2C, SPI | Mutex | Ownership and priority inheritance |
Binary Semaphore
Section titled “Binary Semaphore”A binary semaphore is best when the message is simply:
something happenedCommon uses:
- button interrupt wakes a task
- ADC or DMA completion wakes a task
- one task tells another task that a window is ready
ISR -> Task Pattern
Section titled “ISR -> Task Pattern”#include "FreeRTOS.h"#include "semphr.h"
static SemaphoreHandle_t xBtnSem;
void GPIO_InterruptHandler(void){ BaseType_t higher = pdFALSE;
// clear hardware interrupt flag here xSemaphoreGiveFromISR(xBtnSem, &higher); portYIELD_FROM_ISR(higher);}
static void ButtonTask(void *arg){ (void)arg;
for (;;) { if (xSemaphoreTake(xBtnSem, portMAX_DELAY) == pdTRUE) { // handle button event } }}
int main(void){ xBtnSem = xSemaphoreCreateBinary(); // create ButtonTask // enable GPIO interrupt after semaphore exists}What A Binary Semaphore Does Not Do
Section titled “What A Binary Semaphore Does Not Do”A binary semaphore does not remember an unlimited number of events. If the same event happens repeatedly before the task takes the semaphore again, those extra events can collapse into one pending signal.
That is fine when:
- you only care that the task wakes up
- multiple arrivals before wake-up are equivalent
That is not fine when:
- every event must be counted individually
Counting Semaphore
Section titled “Counting Semaphore”A counting semaphore is the right tool when the count matters.
Examples:
- there are 3 free buffers
- 5 packets arrived
- a producer can get ahead of a consumer and you must not lose that count
SemaphoreHandle_t xPool = NULL;
void InitPool(void){ xPool = xSemaphoreCreateCounting(3, 3);}
void UserTask(void *p){ for (;;) { if (xSemaphoreTake(xPool, pdMS_TO_TICKS(50)) == pdTRUE) { // got one token // use one resource xSemaphoreGive(xPool); } }}A mutex is for mutual exclusion, not event signaling.
Use it when two or more tasks share:
- LCD access
- UART output
- I2C bus
- SPI bus
- any non-reentrant driver or shared peripheral
SemaphoreHandle_t xUartMutex;
void UartWrite(const char *s){ xSemaphoreTake(xUartMutex, portMAX_DELAY); // send through UART xSemaphoreGive(xUartMutex);}Why a mutex instead of a binary semaphore?
- a mutex has ownership
- a mutex supports priority inheritance
- it is specifically designed for resource protection
Priority Inheritance
Section titled “Priority Inheritance”Suppose:
- a low-priority task holds the display lock
- a high-priority task wants the display
- a medium-priority task keeps preempting the low-priority one
Without protection against priority inversion, the high-priority task could wait far longer than expected. A mutex helps by temporarily boosting the priority of the low-priority owner so it can finish and release the resource.
Timeouts
Section titled “Timeouts”xSemaphoreTake(xSem, ticks) lets you control how long a task waits.
Common options:
0-> do not blockpdMS_TO_TICKS(20)-> wait up to 20 msportMAX_DELAY-> wait indefinitely
Use finite timeouts when:
- a missing event is possible
- you want to detect overload or bugs
Use portMAX_DELAY when:
- the task truly has nothing else to do until the event arrives
Lab 2 Interpretation
Section titled “Lab 2 Interpretation”For Lab 2, the intended reasoning is:
- Pushbuttons: binary semaphore, because the interrupt just needs to wake
ButtonTask - Microphone window ready: binary semaphore or queue, depending on whether you only need a wake-up or need to send extra data
- Display ownership: no mutex yet, because the lab deliberately keeps one drawing task as the sole LCD owner
Common Mistakes
Section titled “Common Mistakes”- Using a mutex from ISR context
- Using a binary semaphore when every event must be counted
- Enabling an interrupt before creating the semaphore it will signal
- Treating
volatileas if it replaced synchronization - Using a semaphore for resource protection when a mutex is the right tool
Summary
Section titled “Summary”Pick the tool based on the problem:
- event happened once -> binary semaphore
- event count matters -> counting semaphore
- shared resource must be locked -> mutex
That small decision often determines whether a FreeRTOS design feels simple or fragile.