Electronics - FreeRTOS - Semaphore and mutex on STM32F3Discovery with I2C and UART

Tasks are great, queues as well.

But as soon as several tasks want to use the same resource at the same time, things get messy fast.

This tutorial introduces 2 synchronisation mechanisms that keep everything in order: The mutex and the binary semaphore.

Let's get started.

 

Shopping list

  • Logic Analyzer for I2C and UART bus communication
     

The problem with sharing

Two tasks write to the UART.

Neither knows what the other is doing.

Without coordination, one task can start sending a message, get preempted halfway through, and the other task jumps in and sends its own message on top.

The result on the terminal is garbled output that belongs to nobody.

This is a race condition.

You can reproduce it in seconds by removing the mutex and pressing the button repeatedly while the sensor data keeps scrolling on the screen.

 

The mutex

A mutex is a lock.

One task takes it, does its work, and gives it back.

Any other task that tries to take it (the mutex, not the task) while it is held simply waits (even with higher priority), without burning CPU cycles.

The key property of a mutex is ownership: Only the task that took it can give it back.

This ownership model is what enables priority inheritance, which prevents a subtle scheduling problem called priority inversion.

In this project, the UART is the shared resource.

Both display_task and button_task need to write to it.

The mutex ensures they never do so simultaneously.

Each message arrives complete and intact, no matter how aggressively the button is pressed.

 

Priority inversion and priority inheritance

Imagine three tasks: LOW, MEDIUM, and HIGH.

LOW takes the mutex.

HIGH wakes up and wants it, so HIGH blocks and waits for LOW to finish.

So far so good.

Then MEDIUM wakes up.

MEDIUM does not need the mutex at all, but it has a higher priority than LOW.

The scheduler preempts LOW and gives the CPU to MEDIUM.

LOW is now suspended, still holding the mutex.

HIGH is still waiting.

MEDIUM runs freely, indefinitely.

HIGH, the most important task in the system, is effectively blocked by MEDIUM, a task it should be able to preempt.

This is priority inversion.

FreeRTOS fixes this automatically with priority inheritance.

The moment HIGH blocks on the mutex held by LOW, FreeRTOS temporarily promotes LOW to the same priority as HIGH.

MEDIUM can no longer preempt LOW.

LOW finishes, releases the mutex, returns to its original priority.

HIGH takes the mutex and runs.

No code required.

It is built into xSemaphoreCreateMutex().

 

The binary semaphore

A binary semaphore is not a lock: It is a signal.

It has 2 states: available or not available.

Anyone can give it.

Anyone can take it.

There is no ownership, no priority inheritance.

The classic use case is ISR-to-task communication.

The USER button on PA0 triggers an EXTI0 interrupt.

The ISR cannot block, cannot delay, cannot do anything slow.

All it does is give the semaphore and return.

button_task is blocked waiting on that semaphore.

The moment the ISR gives it, FreeRTOS wakes up the task.

No polling, no global flags, no wasted cycles.

The task sleeps until the exact moment it is needed.

One detail worth noting: the ISR uses xSemaphoreGiveFromISR() instead of the regular xSemaphoreGive().

The FromISR variants are the only FreeRTOS API calls safe to use from interrupt context.

They never block, and they signal the scheduler to perform a context switch immediately after the ISR returns if a higher-priority task was woken.

 

Mutex vs binary semaphore

A mutex protects a resource, has ownership, includes priority inheritance, and cannot be given from an ISR.

A binary semaphore signals an event, has no ownership, no priority inheritance, and can be given from an ISR via xSemaphoreGiveFromISR().

 

The button behaviour

The interrupt fires on the rising edge of PA0, the moment the button goes from LOW to HIGH.

Holding the button down does not generate multiple interrupts.

Each press and release cycle produces exactly one Button pressed! message.

 

Reading signals

Connect the following pins to your logic analyzer:

  • PB6 : I2C SCL
  • PB7 : I2C SDA
  • PA9 : UART TX

 

 

Build and run

$ cd firmware/stm32f3discovery/

$ chmod +x run.sh

$ ./run.sh all

Press the USER button while the accelerometer data keeps scrolling on the screen.

Each press inserts a clean Button pressed! message without disturbing the sensor output. 

To see what happens without the mutex, remove the semaphore calls from the UART print function and press the button repeatedly.

The corruption appears almost immediately.

 

Conclusion

Shared resources and event signalling are 2 of the most common problems in concurrent firmware.

FreeRTOS has clean and minimal solutions for both.

The mutex protects, the semaphore signals, and the scheduler enforces the rules automatically.

Good job, you did it angel

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.