Hands-On FreeRTOS in STM32 01 — Producer-Consumer Problem

C / volatile / xSemaphoreCreateMutex() / xSemaphoreGive() / xSemaphoreTake() /xSemaphoreCreateCounting()

Yu-Cheng (Morton) Kuo
Nerd For Tech
16 min readJan 6, 2024

--

Complete code: https://github.com/yu-cheng-kuo-28/freertos-stm32-f303ze

Screen Recording Video 1: The “live expression” section of main_03.c in the CubeIDE:
https://drive.google.com/file/d/1TnwjLiVHxs6xph6oYhXh8zqYWq5tobym/view

Figures: Illustrations of FreeRTOS in STM32 [4][5]

I did the implementation on STM32 teaching board NUCLEO-F303ZE.

This article contains lots of detailed explanations, so it might be too lenghy to go over, but you will acquire a deep understanding and an intuitive grasp of FreeRTOS, I promise. Let’s be patient and delve into the world of FreeRTOS!

Outline

(0) Warm-Up
0–1 main_01.c: Multithreading with 2 LED light bulbs

(1) Recapturing STM32 HAL: Handle VS. Handler

(2) Producer-Consumer Problem: Use case 01
2–1 main_02.c: With a 1-byte buffer using 2 LED light bulbs
2–2 Detailed Explanation
2–3 Comparison of xSemaphoreCreateBinary() and xSemaphoreCreateMutex()
2–4 Necessity of xSemaphoreConsole
2–5 Why the functions “StartBlink01” & “StartBlink02” are not quite symmetric?

(3) Producer-Consumer Problem: Use case 02
3–1 main_03.c: With a 5-byte buffer using 2 LED light bulbs
3–2 Detailed Explanation

(4) Conclusion

(5) Referencess

(0) Warm-Up

0–1 main_01.c: Multithreading with 2 LED light bulbs

Frist of all, set up the STM32 board following the tutorial video from Digi-Key Electronics:

Digi-Key Electronics | Getting Started With STM32 and Nucleo Part 3: FreeRTOS — How To Run Multiple Threads w/ CMSIS-RTOS

Do what the video teaches, you will get something like the gif below:

Gif 1: main_01.c — Multithreading with 2 LED light bulbs (Red light on the left; green light on the right)
  • Red Light: Toggling every 0.5 second.
  • Green Light: Toggling every 0.6 second.
  • Key sections of the code are shown below. The rest of the code is automatically generated by CubeIDE after you follow the instructions from the video of Digi-Key Electronics.
/*
main_01.c: Multithreading with 2 LED light bulbs
*/

//...

/* USER CODE END Header_StartBlink01 */
void StartBlink01(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Green LED
osDelay(500);
}
/* USER CODE END 5 */
osThreadTerminate(NULL);
}

/* USER CODE BEGIN Header_StartBlink02 */
/**
* @brief Function implementing the blink02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlink02 */
void StartBlink02(void *argument)
{
/* USER CODE BEGIN StartBlink02 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14); // Red LED
osDelay(600);
}
/* USER CODE END StartBlink02 */
osThreadTerminate(NULL);
}

//...
  • The priorities of the two threads (tasks) are set as follows:
/* Definitions for blink01 */
osThreadId_t blink01Handle;
const osThreadAttr_t blink01_attributes = {
.name = "blink01",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for blink02 */
osThreadId_t blink02Handle;
const osThreadAttr_t blink02_attributes = {
.name = "blink02",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
  • Remark: In RTOS, tasks are roughly equivalent to threads. [2][3]

(1) Recapturing STM32 HAL: Handle VS. Handler

Figures: Comparison table of handle VS. handler in STM32 HAL

You may refer to my previous article:

All right, enough prerequisites, let’s get going.

(2) Producer-Consumer Problem: Use case 01

2–1 main_02.c: With a 1-byte buffer using 2 LED light bulbs

Based on the code of the warm-up instance, we move forward.

Gif 2: main_02.c — With a 1-byte buffer using 2 LED light bulbs (Red light on the left; green light on the right)
  • Buffer: There’s only 1 buffer.
  • Red Light: The producer who produces 1 unit every 4 seconds.
  • Green Light: The consumer who consumes 1 unit every 1 second.
  • Key sections of the code are shown below. The rest of the code is automatically generated by CubeIDE after you follow the instructions from the video of Digi-Key Electronics.
/*
main_02.c: Emulate a simple producer-consumer case with 1 buffer using 2 LED light bulbs
*/

//...

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "semphr.h"
#include "stdio.h"
/* USER CODE END Includes */

//...

/* USER CODE BEGIN PV */
SemaphoreHandle_t xSemaphoreProduce;
SemaphoreHandle_t xSemaphoreConsole;
/* USER CODE END PV */

//...

/* USER CODE BEGIN 2 */
xSemaphoreProduce = xSemaphoreCreateBinary();
xSemaphoreConsole = xSemaphoreCreateMutex();
/* USER CODE END 2 */

//...

/* USER CODE END Header_StartBlink01 */
void StartBlink01(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Green LED
xSemaphoreGive(xSemaphoreProduce);
osDelay(4000); // Producing time
}
/* USER CODE END 5 */
osThreadTerminate(NULL);
}

/* USER CODE BEGIN Header_StartBlink02 */
/**
* @brief Function implementing the blink02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlink02 */
void StartBlink02(void *argument)
{
/* USER CODE BEGIN StartBlink02 */
/* Infinite loop */
for(;;) {
if(xSemaphoreTake(xSemaphoreProduce, portMAX_DELAY) == pdTRUE) {
// Consuming the blink
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14); // Red LED
// Protecting console output
if(xSemaphoreTake(xSemaphoreConsole, portMAX_DELAY) == pdTRUE) {
// printf("Blink Consumed\n");
xSemaphoreGive(xSemaphoreConsole);
}
osDelay(1000); // Consuming time
}
}
/* USER CODE END StartBlink02 */
osThreadTerminate(NULL);
}

//...

2–2 Detailed Explanation

(This part comes from ChatGPT-4. Since it did a great job explaining the code, I left this part to it.)

Let’s go through the specific segment of your code line by line, focusing particularly on the synchronization aspects as you’re new to FreeRTOS but have experience with C++ synchronization:

Semaphore and Mutex Initialization

SemaphoreHandle_t xSemaphoreProduce;
SemaphoreHandle_t xSemaphoreConsole;
  • SemaphoreHandle_t xSemaphoreProduce;: Declaration of a semaphore handle. In FreeRTOS, semaphores are used for synchronization between tasks. This specific semaphore (xSemaphoreProduce) will be used to synchronize the producer (StartBlink01) and the consumer (StartBlink02).
  • SemaphoreHandle_t xSemaphoreConsole;: Declaration of another semaphore handle, which will be used as a mutex (mutual exclusion semaphore) to control access to shared resources like the console output.
xSemaphoreProduce = xSemaphoreCreateBinary();
xSemaphoreConsole = xSemaphoreCreateMutex();
  • xSemaphoreCreateBinary(): This function creates a binary semaphore and assigns its handle to xSemaphoreProduce. A binary semaphore can be thought of as a simple signal flag: it's either taken or available.
  • xSemaphoreCreateMutex(): This creates a mutex semaphore and assigns its handle to xSemaphoreConsole. Mutexes are used to ensure that only one task accesses a shared resource at any given time.

Producer Task (StartBlink01)

void StartBlink01(void *argument) {
for(;;) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Green LED
xSemaphoreGive(xSemaphoreProduce);
osDelay(4000); // Producing time
}
osThreadTerminate(NULL);
}
  • HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0): This line toggles the state of a GPIO pin connected to a green LED. It’s a way of visually representing the production of an item.
  • xSemaphoreGive(xSemaphoreProduce): This releases the semaphore xSemaphoreProduce, signaling to the consumer task that a new 'item' (in this case, the action represented by the LED toggle) is ready.
  • osDelay(4000): This introduces a delay of 4000 milliseconds, simulating the time taken by the producer to ‘produce’ the next item.
  • osThreadTerminate(NULL): This function would terminate the thread if it were ever reached, which it won’t be in this case due to the infinite loop.

Consumer Task (StartBlink02)

void StartBlink02(void *argument) {
for(;;) {
if(xSemaphoreTake(xSemaphoreProduce, portMAX_DELAY) == pdTRUE) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14); // Red LED
if(xSemaphoreTake(xSemaphoreConsole, portMAX_DELAY) == pdTRUE) {
xSemaphoreGive(xSemaphoreConsole);
}
osDelay(1000); // Consuming time
}
}
osThreadTerminate(NULL);
}
  • xSemaphoreTake(xSemaphoreProduce, portMAX_DELAY): This attempts to take the semaphore xSemaphoreProduce. If the semaphore is not available (meaning the producer has not produced anything yet), this task will wait indefinitely (portMAX_DELAY) until the semaphore is given by the producer.
  • HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14): Toggles the state of another GPIO pin connected to a red LED, representing the consumption of the ‘item’.
  • xSemaphoreTake(xSemaphoreConsole, portMAX_DELAY): Attempts to take the xSemaphoreConsole mutex before accessing the shared resource (console). This is important for ensuring that only one task uses the console at a time.
  • xSemaphoreGive(xSemaphoreConsole): Releases the mutex semaphore xSemaphoreConsole after the critical section, allowing other tasks to use the console.
  • osDelay(1000): Adds a delay of 1000 milliseconds, simulating the time taken by the consumer to ‘consume’ the item.
  • osThreadTerminate(NULL): Like in the producer task, this line terminates the thread if reached, which it isn’t due to the infinite loop.

Overall Explanation

  • The producer task (StartBlink01) toggles a green LED to represent production and then signals the consumer task (StartBlink02) using a semaphore (xSemaphoreProduce).
  • The consumer task waits for this signal. Once received, it toggles a red LED to represent consumption.
  • A mutex (xSemaphoreConsole) is used to ensure safe access to shared resources like console output, though the actual console printing is commented out in your code.
  • This setup is a classic producer-consumer problem, a fundamental concept in concurrent programming, used to coordinate tasks with shared resources.

2–3 Comparison of xSemaphoreCreateBinary() and xSemaphoreCreateMutex()

(This part comes from ChatGPT-4. Since it did a great job explaining the code, I left this part to it.)

Sure, let’s delve into the differences between xSemaphoreCreateBinary() and xSemaphoreCreateMutex(), and explore the necessity of xSemaphoreConsole in your code:

1. Comparison of xSemaphoreCreateBinary() and xSemaphoreCreateMutex()

xSemaphoreCreateBinary()

  • Purpose: Creates a binary semaphore. Binary semaphores are primarily used for signaling between tasks or between tasks and an interrupt. They can be thought of as a simple flag that can be set or cleared.
  • Behavior: A binary semaphore has two states: taken and given. It’s similar to a binary flag. When a task takes a binary semaphore, it waits for the semaphore to be in the given state. Once the semaphore is taken, it moves to the taken state. It must be explicitly given back to move to the given state again.
  • Use-case: Suitable for synchronization between tasks or between an ISR (Interrupt Service Routine) and a task. For example, signaling a task from an ISR that an event (like a timer expiry or external input) has occurred.

xSemaphoreCreateMutex()

  • Purpose: Creates a mutex (mutual exclusion semaphore). Mutexes are used to protect shared resources. They ensure that only one task can access a resource at any given time.
  • Behavior: When a task takes a mutex to enter a critical section (i.e., accessing a shared resource), no other task can enter a critical section protected by the same mutex until the first task has given (released) the mutex.
  • Priority Inheritance: FreeRTOS mutexes support priority inheritance. If a high-priority task is waiting for a mutex held by a lower-priority task, the priority of the lower-priority task is temporarily raised to the priority of the higher-priority task. This helps in solving the priority inversion problem.
  • Use-case: Ideal for controlling access to shared resources like global variables, peripherals (like UART), or data structures.

2. Necessity of xSemaphoreConsole

The xSemaphoreConsole in your code is used as a mutex to protect the console output, which is a shared resource. This is necessary to prevent concurrency issues, like data corruption or unpredictable behavior, which can occur when multiple tasks try to access or modify the same resource simultaneously.

Why Protect the Console?

  • Simultaneous Access: If two tasks simultaneously try to print to the console, their outputs could interleave, leading to garbled, unreadable output.
  • Data Corruption: Concurrent access without synchronization can lead to data corruption, where the output from one task might overwrite or disrupt the output from another task.
  • Deterministic Behavior: Using a mutex ensures that only one task can access the console at a time, providing a deterministic and orderly output to the console.

In summary, xSemaphoreConsole as a mutex ensures that only one task at a time can perform operations on the console, thus preventing data corruption and ensuring coherent and orderly access to the shared console resource. This is a fundamental practice in concurrent programming to safeguard the integrity of shared resources.

2–5 Why the functions “StartBlink01” & “StartBlink02” are not quite symmetric?

(This part comes from ChatGPT-4. Since it did a great job explaining the code, I left this part to it.)

In the provided code, “StartBlink01” and “StartBlink02” are part of a producer-consumer scenario implemented using FreeRTOS. The asymmetry you’ve noticed between these functions, particularly in the use of synchronization mechanisms, can be explained by the nature of their operations and the design decisions made in this implementation.

[1] StartBlink01 — Producer Thread:

  • This function toggles a green LED (simulating the production of an item) and then signals the xSemaphoreProduce semaphore to indicate that an item has been produced.
  • The osDelay(4000) is used to simulate the time taken to produce an item.
  • Importantly, this function does not interact with any shared resources that might require mutual exclusion, hence a mutex is not used here. The semaphore xSemaphoreProduce is sufficient to synchronize the production of items with their consumption.

[2] StartBlink02 — Consumer Thread:

  • This function waits for the xSemaphoreProduce semaphore, indicating that an item is available for consumption.
  • Upon receiving the semaphore, it toggles a red LED (simulating the consumption of an item).
  • The xSemaphoreConsole mutex is used to protect console output. This is necessary if multiple threads might print to the console simultaneously, preventing garbled or overlapping output.
  • The osDelay(1000) simulates the time taken to consume an item.

In summary, “StartBlink01” does not require a mutex because it does not access any shared resources that would need mutual exclusion. The semaphore it uses is for synchronization with the consumer thread, signaling the availability of an item. On the other hand, “StartBlink02” uses a mutex in addition to the semaphore because it potentially accesses a shared resource (console output) that needs to be accessed in a mutually exclusive manner. The decision not to use a mutex in “StartBlink01” is likely based on the simplicity of the producer’s action (just toggling an LED) and the absence of any shared resource access in this particular function.

(3) Producer-Consumer Problem: Use case 02

3–1 main_03.c: With a 5-byte buffer using 2 LED light bulbs

In this more complex example, we not only display the LED lights toggling, but also showcase the “live expression” section in the CubeIDE to further observe the situations of the 5-bytes buffer along with the interaction between the producer and the consumer.

Gif 3: main_03.c — With a 5-byte buffer using 2 LED light bulbs (Red light on the left; green light on the right)

Screen Recording Video 1: The “live expression” section of main_03.c in the CubeIDE:
https://drive.google.com/file/d/1TnwjLiVHxs6xph6oYhXh8zqYWq5tobym/view

  • Key sections of the code are shown below. The rest of the code is automatically generated by CubeIDE after you follow the instructions from the video of Digi-Key Electronics.
/*
main_03.c: Emulate a producer-consumer case with 5 buffer using 2 LED light bulbs
*/

//...

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "semphr.h"
#include "stdio.h"
/* USER CODE END Includes */

//...

/* USER CODE BEGIN PV */
SemaphoreHandle_t xSemaphoreProduce;
SemaphoreHandle_t xSemaphoreConsole;

#define BUFFER_SIZE 5
//uint8_t buffer[BUFFER_SIZE];
//int bufferIndexProducer = 0;
//int bufferIndexConsumer = 0;
volatile uint8_t buffer[BUFFER_SIZE];
volatile int bufferIndexProducer;
volatile int bufferIndexConsumer;

SemaphoreHandle_t xSemaphoreEmptySlots;
SemaphoreHandle_t xSemaphoreFilledSlots;
SemaphoreHandle_t xSemaphoreConsole;
/* USER CODE END PV */

//...

/* USER CODE BEGIN 2 */
xSemaphoreProduce = xSemaphoreCreateBinary();
xSemaphoreConsole = xSemaphoreCreateMutex();

xSemaphoreEmptySlots = xSemaphoreCreateCounting(BUFFER_SIZE, BUFFER_SIZE);
xSemaphoreFilledSlots = xSemaphoreCreateCounting(BUFFER_SIZE, 0);
xSemaphoreConsole = xSemaphoreCreateMutex();
/* USER CODE END 2 */

//...

/* USER CODE END Header_StartBlink01 */
void StartBlink01(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(int8_t i = 0; i < 1000; ++i) {
if(xSemaphoreTake(xSemaphoreEmptySlots, portMAX_DELAY) == pdTRUE) {
// Produce an item
buffer[bufferIndexProducer] = 100; // Represents a produced item
bufferIndexProducer = (bufferIndexProducer + 1) % BUFFER_SIZE;

// Signal that a new item has been produced
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Green LED
xSemaphoreGive(xSemaphoreFilledSlots);
}
osDelay(1000); // Producing time
}
/* USER CODE END 5 */
osThreadTerminate(NULL);
}

/* USER CODE BEGIN Header_StartBlink02 */
/**
* @brief Function implementing the blink02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlink02 */
void StartBlink02(void *argument)
{
/* USER CODE BEGIN StartBlink02 */
/* Infinite loop */
for(int8_t i = 0; i < 1000; ++i) {
if (i == 0) {
osDelay(8000);
}

if(xSemaphoreTake(xSemaphoreFilledSlots, portMAX_DELAY) == pdTRUE) {
// Consume the item
// uint8_t item = buffer[bufferIndexConsumer];
buffer[bufferIndexConsumer] = 5;
bufferIndexConsumer = (bufferIndexConsumer + 1) % BUFFER_SIZE;

// Signal that an item has been consumed
xSemaphoreGive(xSemaphoreEmptySlots);

// Use the consumed item (e.g., toggle an LED)
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14); // Red LED

// Protecting console output
if(xSemaphoreTake(xSemaphoreConsole, portMAX_DELAY) == pdTRUE) {
// printf("Blink Consumed\n");
xSemaphoreGive(xSemaphoreConsole);
}
}
osDelay(800); // Consuming time
}
/* USER CODE END StartBlink02 */
osThreadTerminate(NULL);
}

//...

You may think about why the code above resulted in the output in Gif 3 & Video 1 before moving on to the next paragraph. I designed the synchronization pattern for the purpose of showcasing the mechanism of the producer-consumer problem.

3–2 Detailed Explanation

(This part comes from ChatGPT-4. Since it did a great job explaining the code, I left this part to it.)

The code snippet demonstrates a producer-consumer problem using semaphores for synchronization. Let’s break down the key components:

The code snippet you’ve provided is from an STM32 microcontroller application, using the FreeRTOS real-time operating system. It demonstrates a producer-consumer problem using semaphores for synchronization. Let’s break down the key components:

Includes and Variable Definitions:

#include "semphr.h"  // Includes FreeRTOS's semaphore API
#include "stdio.h" // Standard input-output library for console operations
SemaphoreHandle_t xSemaphoreProduce;
SemaphoreHandle_t xSemaphoreConsole;
volatile uint8_t buffer[BUFFER_SIZE]; // Shared buffer between producer and consumer
volatile int bufferIndexProducer; // Index where producer will put the item
volatile int bufferIndexConsumer; // Index where consumer will take the item
SemaphoreHandle_t xSemaphoreEmptySlots; // Counts empty slots in the buffer
SemaphoreHandle_t xSemaphoreFilledSlots;// Counts filled slots in the buffer
SemaphoreHandle_t xSemaphoreConsole; // Mutex for console access
  • Semaphores xSemaphoreProduce, xSemaphoreEmptySlots, xSemaphoreFilledSlots, and xSemaphoreConsole are declared.
  • volatile keyword is used for buffer, bufferIndexProducer, and bufferIndexConsumer to prevent compiler optimizations, ensuring the most recent values are always read from memory, which is crucial in a concurrent environment.

Initialization in USER CODE BEGIN 2:

xSemaphoreProduce = xSemaphoreCreateBinary();
xSemaphoreConsole = xSemaphoreCreateMutex();
xSemaphoreEmptySlots = xSemaphoreCreateCounting(BUFFER_SIZE, BUFFER_SIZE);
xSemaphoreFilledSlots = xSemaphoreCreateCounting(BUFFER_SIZE, 0);
xSemaphoreConsole = xSemaphoreCreateMutex();
  • xSemaphoreProduce: A binary semaphore, initialized to '0'. Used by the producer to signal the consumer.
  • xSemaphoreEmptySlots: A counting semaphore, showing the number of empty slots in the buffer. Initialized to BUFFER_SIZE.
  • xSemaphoreFilledSlots: A counting semaphore, indicating the number of filled slots. Initialized to '0'.
  • xSemaphoreConsole: A mutex semaphore for protecting console access.

StartBlink01 (Producer Thread):

void StartBlink01(void *argument) {
for(int8_t i = 0; i < 1000; ++i) {
if(xSemaphoreTake(xSemaphoreEmptySlots, portMAX_DELAY) == pdTRUE) {
buffer[bufferIndexProducer] = 100;
bufferIndexProducer = (bufferIndexProducer + 1) % BUFFER_SIZE;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
xSemaphoreGive(xSemaphoreFilledSlots);
}
osDelay(1000);
}
osThreadTerminate(NULL);
}
  • Waits for an empty slot in the buffer (xSemaphoreEmptySlots).
  • Produces an item (here, represented as writing ‘100’ to buffer) and toggles a green LED.
  • Signals that a new item has been produced by giving xSemaphoreFilledSlots.
  • Waits for 1 second before producing the next item.

StartBlink02 (Consumer Thread):

void StartBlink02(void *argument) {
for(int8_t i = 0; i < 1000; ++i) {
if (i == 0) { osDelay(8000); }
if(xSemaphoreTake(xSemaphoreFilledSlots, portMAX_DELAY) == pdTRUE) {
buffer[bufferIndexConsumer] = 5;
bufferIndexConsumer = (bufferIndexConsumer + 1) % BUFFER_SIZE;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14);
xSemaphoreGive(xSemaphoreEmptySlots);
if(xSemaphoreTake(xSemaphoreConsole, portMAX_DELAY) == pdTRUE) {
// printf("Blink Consumed\\\\n");
xSemaphoreGive(xSemaphoreConsole);
}
}
osDelay(800);
}
osThreadTerminate(NULL);
}
  • Initially waits 8 seconds before starting to consume.
  • Consumes an item (here, represented by changing the value in buffer to '5') and toggles a red LED.
  • Signals an empty slot is now available in the buffer (xSemaphoreEmptySlots).
  • Protects console output using xSemaphoreConsole.
  • Waits for 0.8 seconds before trying to consume the next item.

Synchronization and Semaphores:

  • Producer-Consumer Synchronization: The producer and consumer are synchronized using the xSemaphoreEmptySlots and xSemaphoreFilledSlots semaphores. The producer waits for an empty slot to produce an item, and the consumer waits for a filled slot to consume an item.
  • Binary Semaphore (xSemaphoreProduce): Typically used for signaling, not used in the provided snippet.
  • Counting Semaphores (xSemaphoreEmptySlots and `xSemaphoreFilledSlots`): Keep track of resource counts (empty and filled slots in the buffer).
  • Mutex Semaphore (xSemaphoreConsole): Ensures that only one thread at a time can access a shared resource, here intended for console output.

This implementation avoids race conditions and ensures data integrity between the producer and consumer by appropriately using semaphores for synchronization.

(4) Conclusion

By dissecting the code line by line, you now have gained more insights into how all sorts of components of FreeRTOS behave and work to resolve the producer-consumer problem.

You may compare this solution to the producer-consumer problem in STM32 FreeRTOS with the one in C++ from my previous article as follows:

// 17_producer_consumer_02.cpp

#include <iostream> // Includes the standard I/O library
#include <queue> // Includes the queue library for queue operations
#include <thread> // Includes the thread library for threading operations
#include <mutex> // Includes the mutex library for synchronization
#include <condition_variable> // Includes the condition_variable library for thread communication

std::mutex mtx; // Declares a mutex for critical section management
std::condition_variable cond_var; // Declares a condition variable for blocking and waking threads
std::queue<int> buffer; // Declares a queue to act as the buffer
const unsigned int MAX_BUFFER_SIZE = 10; // Sets the maximum size of the buffer
void producer(int value) { // Defines the producer function
std::unique_lock<std::mutex> lock(mtx); // Locks the mutex before accessing the buffer
cond_var.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; }); // Waits if the buffer is full
std::cout << "Producing " << value << std::endl; // Prints the produced value
buffer.push(value); // Pushes the value into the buffer
std::cout << "Buffer size after producing: " << buffer.size() << std::endl << std::endl; // Prints the buffer size after producing
lock.unlock(); // Unlocks the mutex
cond_var.notify_one(); // Notifies one waiting thread
}
void consumer() { // Defines the consumer function
std::unique_lock<std::mutex> lock(mtx); // Locks the mutex before accessing the buffer
cond_var.wait(lock, [] { return buffer.size() > 0; }); // Waits if the buffer is empty
int value = buffer.front(); // Gets the value from the front of the buffer
buffer.pop(); // Removes the value from the buffer
std::cout << "Consuming " << value << std::endl; // Prints the consumed value
std::cout << "Buffer size after consuming: " << buffer.size() << std::endl << std::endl; // Prints the buffer size after consuming
lock.unlock(); // Unlocks the mutex
cond_var.notify_one(); // Notifies one waiting thread
}
int main() { // The main function
std::thread producerThread([] { // Creates a producer thread
for (int i = 1; i <= 20; ++i) {
producer(i); // Produces 20 items
}
});
// Delay before starting consumer thread
std::this_thread::sleep_for(std::chrono::seconds(3));
std::thread consumerThread([] { // Creates a consumer thread
for (int i = 1; i <= 20; ++i) {
consumer(); // Consumes 20 items
}
});
producerThread.join(); // Waits for producer thread to finish
consumerThread.join(); // Waits for consumer thread to finish
return 0; // Ends the program
}

Or, you can check this concise implementation of FreeRTOS in STM32.

--

--

Yu-Cheng (Morton) Kuo
Nerd For Tech

CS/DS blog with C/C++/Embedded Systems/Python. Embedded Software Engineer. Email: yc.kuo.28@gmail.com