Skip to content

Semaphores and Mutexes

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.

Before picking an API, ask:

  1. Are you trying to wake a task because something happened?
  2. 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
NeedBest toolWhy
ISR wakes a taskBinary semaphoreSimple one-bit event signal
A task counts multiple arrivalsCounting semaphorePreserves event count
Protect LCD, UART, I2C, SPIMutexOwnership and priority inheritance

A binary semaphore is best when the message is simply:

something happened

Common uses:

  • button interrupt wakes a task
  • ADC or DMA completion wakes a task
  • one task tells another task that a window is ready
#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
}

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

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

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.

xSemaphoreTake(xSem, ticks) lets you control how long a task waits.

Common options:

  • 0 -> do not block
  • pdMS_TO_TICKS(20) -> wait up to 20 ms
  • portMAX_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

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
  • 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 volatile as if it replaced synchronization
  • Using a semaphore for resource protection when a mutex is the right tool

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.