[Arduino FreeRTOS tutorial] How to use semaphore and mutex
In the previous article, we have introduced the use of FreeRTOS in the basics of Arduino and queue Queue kernel objects to use them. In this article, we will continue to learn more about FreeRTOS and its high-level APIs to help you understand the multitasking platform more deeply.
Semaphore ( Semaphore ) mutex ( the Mutex ) is used for synchronization, resource management and protection of resources from damage kernel object. In the first half of this article, we will understand the ideas behind semaphores and how to use them. In the second half, we will introduce how to use mutexes.
What is a semaphore?
In the previous article, we have discussed task priority, and we also know that high-priority tasks will preempt low-priority tasks, so when high-priority tasks are executed, low-priority tasks may experience data corruption Case. Because the task has not been executed, and data is constantly being transferred from the sensor to the task, this will cause data loss and failure of the entire application.
Therefore, resources need to be protected from data loss, and semaphore plays an important role here.
Semaphore is a signaling mechanism in which a task in a waiting state is signaled for execution by another task. In other words, when task1 completes its work, it will display a flag or add 1 to the flag, and then another task (task2) will receive the flag, indicating that it can now perform its work. When task2 finishes its work, the flag will decrease by 1.
Therefore, fundamentally speaking, it is a “give” and “receive” mechanism, and the semaphore is an integer variable used to synchronize access to resources.
Semaphore types in FreeRTOS
There are two types of semaphores.
1. Binary Semaphore
2. Counting Semaphore
1. Binary semaphore: It has two integer values 0 and 1. It is somewhat similar to a queue of length 1. For example, we have two tasks task1 and task2. Task1 sends data to task2, so task2 continues to check whether the queue item is 1, and then it can read the data, otherwise it must wait until it becomes 1. After getting the data, task2 decrements the queue and sets it to 0, which means task1 can send data to task2 again.
From the above example, it can be said that the binary semaphore is used for synchronization between tasks or between tasks and interrupts.
2. Counting semaphore: its value is greater than 0, and can be considered a queue with a length greater than 1. This semaphore is used to count events. In this use case, the event handler will “give” the semaphore (increase the semaphore count value) when the event occurs, and the handler task will “receive” the semaphore (decrease the semaphore count value) each time the event is processed ). .
Therefore, the count value is the difference between the number of events that have occurred and the number of events that have been processed.
Now, let’s see how to use semaphores in FreeRTOS code.
How to use semaphore in FreeRTOS?
FreeRTOS supports different APIs for creating semaphores, obtaining semaphores and providing semaphores.
Now, the same kernel object can have two types of APIs. If the semaphore must be provided from the ISR, the regular semaphore API cannot be used. You should use interrupt-protected APIs.
In this article, we will use binary semaphore because it is easy to understand and implement. Since the interrupt function is used here, you need to use the interrupt-protected API in the ISR function. When we talk about synchronizing tasks with interrupts, this means putting the task in the “running” state immediately after the ISR.
1. Create a semaphore:
To use any kernel object, we must first create it. To create a binary semaphore, use vSemaphoreCreateBinary().
This API takes no parameters and returns a variable of type SemaphoreHandle_t. Create a global variable name sema_v to store the semaphore.
2. Provide semaphore
In order to provide semaphores, there are two API functions-one for interrupts and the other for regular tasks.
1. xSemaphoreGive(): This API only accepts one parameter when creating a semaphore, that is, the variable name of the above semaphore, such as sema_v. It can be called from any regular task to be synchronized.
2. xSemaphoreGiveFromISR(): This function is an interrupt-protected API function of xSemaphoreGive(). When you need to synchronize ISR and normal tasks, you should use xSemaphoreGiveFromISR() from the ISR function.
3. Get the semaphore
To get the semaphore, use the API function xSemaphoreTake(). The API takes two parameters.
xSemaphore: The name of the semaphore, in this case sema_v.
xTicksToWait: This is the longest time the task waits for the semaphore to become available in the “blocking” state. In this article, we set xTicksToWait to portMAX_DELAY to make task_1 wait indefinitely in the Blocked state until sema_v is available.
Now, let’s use these APIs and write code to perform some tasks.
Here a button is connected to two LEDs. This button will act as an interrupt button, which is connected to pin 2 of the Arduino Uno. When this button is pressed, an interrupt will be generated, and the LED indicator connected to pin 8 will light up, and the indicator light will be off when pressed again.
Therefore, when the button is pressed, the ISR function will call xSemaphoreGiveFromISR(), and the TaskLED function will call the xSemaphoreTake() function.
In order to make the system appear to have multitasking capabilities, please connect other LEDs to pin 7. This pin will always be blinking.
Semaphore code description
Let’s open the Arduino IDE and start writing code
1. First, include the Arduino_FreeRTOS.h header file. Now, if you use any kernel objects (such as queue semaphores), you must also include a header file for it.
2. Declare a variable of type SemaphoreHandle_t to store the value of the semaphore.
3. In void setup(), use xTaskCreate to create two tasks (TaskLED and TaskBlink), and then use xSemaphoreCreateBinary() to create a semaphore. Create a task with the same priority and try to use this task later. In addition, configure pin 2 as an input, enable the internal pull-up resistor and connect the interrupt pin. Finally, start the scheduler as shown below.
4. Now, implement the ISR function. Create a function and name it the same name as the second parameter of the attachInterrupt() function. In order for the interrupt to work properly, you need to use the millisecond function and adjust the time to eliminate the jitter problem of the button. From this function, call the interruptHandler() function as shown below.
In the interruptHandler() function, call the xSemaphoreGiveFromISR() function.
This function will provide a semaphore to TaskLed to turn on the LED.
5. Create a TaskLed function, and in the while loop, call xSemaphoreTake() API and check whether the semaphore is successfully acquired. If it is equal to pdPASS (that is, 1), the LED is switched as shown in the figure below.
6. In addition, create a function to blink other LEDs connected to pin 7.
7. The void loop function remains empty. ,
The above is all the code. Now, upload this code and connect the LED and button with Arduino UNO according to the circuit diagram.
Circuit schematic
After uploading the code, you will see one LED flashing after 200 milliseconds, and when the button is pressed, the second LED will immediately light up.
In this way, the semaphore can be used in Arduino’s FreeRTOS, it needs to pass data from one task to another without causing any loss.
Now, let us see what a mutex is and how to use it in FreeRTOS.
What is a mutex?
As mentioned above, semaphore is a signal transmission mechanism, which is different from mutex. Mutex is a locking mechanism, which is different from semaphore with independent increment and decrement functions. In, the function itself has and is given. This is a technique to avoid damage to shared resources.
In order to protect shared resources, a token card (mutual exclusion) can be allocated to the resource. Anyone with this card can access other resources. Others should wait until the card is returned. In this way, only one resource can access the task, while other resources are waiting for opportunities.
Let us use an example to understand the mutex in FreeRTOS.
Here we have three tasks, one task is to print data on the LCD, the second task is to send LDR data to the LCD task, and the last task is to send temperature data on the LCD. Therefore, there are two tasks sharing the same resource, namely LCD. If the LDR task and the temperature task send data at the same time, one of the data may be damaged or lost.
Therefore, in order to protect data loss, we need to lock the LCD resource for task 1 until it completes the display task. Then the LCD task will be unlocked, and then task2 can perform its work.
You can observe the work of mutexes and semaphores in the figure below.
How to use mutex in FreeRTOS?
Mutexes can also be used in the same way as semaphores. First, create it, and then use the corresponding API to use it.
1. Create Mutex
To create a mutex, use xSemaphoreCreateMutex(). As the name implies, a mutex is a binary semaphore. They are used for different contexts and purposes. Binary semaphores are used for synchronization tasks, and mutexes are used to protect shared resources.
This API has no parameters and returns a variable of type SemaphoreHandle_t. If the mutex cannot be created, xSemaphoreCreateMutex() returns NULL.
2. Get Mutex
When a task wants to access a resource, it will obtain the mutex object by using xSemaphoreTake(). It is the same as the binary semaphore. It also requires two parameters.
xSemaphore: The name of the mutex that will be used, in this case Mutex_v.
xTicksToWait: This is the maximum time a task can wait for Mutex to be available in the “blocked” state. In this article, we set xTicksToWait to portMAX_DELAY to make task_1 wait indefinitely in the Blocked state until mutex_v is available.
3. Provide mutexes
After accessing the shared resource, the task should return the mutex so that other tasks can access it. xSemaphoreGive() is used to return the mutex.
The xSemaphoreGive() function takes only one parameter, that is, the given mutex, which is mutex_v in this example.
Using the above API, let us implement Mutex in FreeRTOS code using Arduino IDE.
Mutex code description
Here, the goal of this part is to use the serial monitor as a shared resource and perform two different tasks to access the serial monitor to print some messages.
1. The header file will remain the same as the semaphore.
2. Declare a variable of type SemaphoreHandle_t to store the value of the mutex.
3. In void setup(), initialize the serial monitor at 9600 baud rate, and use xTaskCreate() to create two tasks (Task1 and Task2). Then use xSemaphoreCreateMutex() to create a mutex. Create a task with the same priority and try to use the task later.
4. Now, create task functions for Task1 and Task2. In the while loop of the task function, before printing the message on the serial monitor, we must use xSemaphoreTake() to get the Mutex, then print the message, and then use xSemaphoreGive() to return the mutex.
Similarly, the Task2 function is implemented with a delay of 500ms.
5. The void loop() code remains empty.
Now, upload this code to Arduino UNO and open the serial monitor.
You will see that messages are being printed from task1 and task2.
To test the work of the mutex, just comment xSemaphoreGive(mutex_v); in any task. You can see that the program stops at the last printed message.
The above is the way to implement semaphore and mutex in FreeRTOS using Arduino. For more information about semaphores and mutexes, you can visit the official documentation of FreeRTOS .
Complete Code
The following is the complete code used in this article:
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
long debouncing_time = 150;
volatile unsigned long last_micros;SemaphoreHandle_t interruptSemaphore;
void setup() {
pinMode(2, INPUT_PULLUP);
xTaskCreate(TaskLed, “Led”, 128, NULL, 0, NULL );
xTaskCreate(TaskBlink, “LedBlink”, 128, NULL, 0, NULL );
interruptSemaphore = xSemaphoreCreateBinary();
if (interruptSemaphore != NULL) {
attachInterrupt(digitalPinToInterrupt(2), debounceInterrupt, LOW);
}
}void loop() {}
void interruptHandler() {
xSemaphoreGiveFromISR(interruptSemaphore, NULL);
}void TaskLed(void *pvParameters)
{
(void) pvParameters;
pinMode(8, OUTPUT);
for (;;) {
if (xSemaphoreTake(interruptSemaphore, portMAX_DELAY) == pdPASS) {
digitalWrite(8, !digitalRead(8));
}
}
}
void TaskBlink(void *pvParameters)
{
(void) pvParameters;
pinMode(7, OUTPUT);
for (;;) {
digitalWrite(7, HIGH);
vTaskDelay(200 / portTICK_PERIOD_MS);
digitalWrite(7, LOW);
vTaskDelay(200 / portTICK_PERIOD_MS);
}
}
void debounceInterrupt() {
if((long)(micros() – last_micros) >= debouncing_time * 1000) {
interruptHandler();
last_micros = micros();
}
}CODE for Mutex:
#include <Arduino_FreeRTOS.h>
#include <semphr.h>SemaphoreHandle_t mutex_v;
void setup() {
Serial.begin(9600);
mutex_v = xSemaphoreCreateMutex();
if (mutex_v == NULL) {
Serial.println(“Mutex can not be created”);
}
xTaskCreate(Task1, “Task1”, 128, NULL, 1, NULL);
xTaskCreate(Task2, “Task2”, 128, NULL, 1, NULL);
}void Task1(void *pvParameters) {
while(1) {
xSemaphoreTake(mutex_v, portMAX_DELAY);
Serial.println(“Hi from Task1”);
xSemaphoreGive(mutex_v);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}void Task2(void *pvParameters) {
while(1) {
xSemaphoreTake(mutex_v, portMAX_DELAY);
Serial.println(“Hi from Task2”);
xSemaphoreGive(mutex_v);
vTaskDelay(pdMS_TO_TICKS(500));
}
}void loop() {
}