Ground Embedded KO Subscribe

Notes / RTOS

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.

GROUND EMBEDDED · NOTERTOS 04Semaphores and mutexesBUFFERhead →tail →write · read

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

A uses UARTB waitsB uses UART
Mutex. Task B waits until A gives the key back and then uses the UART. The two strings do not mix.
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

Task Aholds resource 1Waits for resource 2Task Bholds resource 2Waits for resource 1
Deadlock. Each waits for what the other holds. It cannot happen if locks are always taken in the same order.

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 Give on 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)
3D LABRTOS Scheduler SimulationRun what this note explains and watch it respond.Open the lab →FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →