Semaphores and mutexes: protecting shared resources
When two tasks touch the same UART or variable at once, the data gets mixed. Lock with a mutex, signal with a semaphore.
The problem
If task B cuts in and sends "SPD=100" while task A is sending "TEMP=25" over the UART, what goes out is "TEMSPD=100P=25". A shared resource must be used by one at a time.
Mutex: one key
xSemaphoreTake(uart_mutex, portMAX_DELAY);
uart_send(buf, len);
xSemaphoreGive(uart_mutex);
- Only the task that holds it can give it back.
- It includes priority inheritance, which reduces priority inversion.
- It is not used from interrupts.
Semaphore: a signal
- Binary semaphore: a signal that "something happened". The interrupt Gives, the task Takes and waits
- Counting semaphore: tracks how many are left when there are N resources (N buffer slots)
void USART1_IRQHandler(void) { // interrupt
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(rx_sem, &woken);
portYIELD_FROM_ISR(woken);
}
In an interrupt, always use the FromISR version of the function.
Deadlock
If A holds resource 1 and waits for resource 2, while B holds resource 2 and waits for resource 1, both stop forever. When taking several resources, always take them in the same order, and hold them briefly. A safer design is to let one task own the resource and have the other tasks send it requests through a queue.
What goes wrong
Two tasks printing strings on the same UART get their characters mixed. Code like this is more dangerous.
counter = counter + 1; // read → add → write, three steps
If the scheduler switches to task B right after task A reads the value, and B increments it, then A, when it comes back, adds 1 to the old value and writes it, and B's increment is lost. A section that only one task may be in at a time is called a critical section.
How the three differ
| Mutex | Binary semaphore | Counting semaphore | |
|---|---|---|---|
| Purpose | Protect a resource | Signal an event | Count things |
| Owner | Yes (only the task that locked it can unlock) | No | No |
| Priority inheritance | Yes | No | No |
| Use from an ISR | Not allowed | Give only | Give only |
Remember it as a mutex to guard a resource, a semaphore to say that something happened.
3D labRun what you have read so far→Guarding a resource with a mutex
if (xSemaphoreTake(uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
uart_send(msg); // keep it short
xSemaphoreGive(uart_mutex);
} else {
err_count++; // handle the timeout too
}
With a timeout instead of waiting forever (portMAX_DELAY), a lock that was never released somewhere shows up as an error instead of freezing the whole system.
Waking a task from an interrupt with a semaphore
void EXTI0_IRQHandler(void) {
BaseType_t woken = pdFALSE;
EXTI->PR = EXTI_PR_PR0;
xSemaphoreGiveFromISR(btn_sem, &woken);
portYIELD_FROM_ISR(woken); // switch at once if a more urgent task woke
}
void button_task(void *arg) {
for (;;) {
xSemaphoreTake(btn_sem, portMAX_DELAY); // wait without using the CPU until the event arrives
handle_button();
}
}
The interrupt handler only raises the flag, and the task does the real work. Inside an ISR, always use the functions that end in FromISR.
Deadlock
If task A holds mutex 1 and waits for 2, while task B holds 2 and waits for 1, both stop forever. Preventing it is simple.
- If several mutexes are needed, every task takes them in the same order.
- Do not create sections that wait for another lock while holding one.
- Do not call delay functions or wait on communication inside a locked section.
Problems that come up in practice
- Guarding a resource with a binary semaphore instead of a mutex, causing priority inversion
- Missing the
Giveon an error path, so nobody can get in afterwards - Trying to guard a resource shared between an interrupt and a task with a mutex (a mutex cannot be used in an ISR, so use a critical section or a queue)