Mutexes can be one of the harder parts of FreeRTOS to learn. They are slower than semaphores, but don’t seem to do anything special. I am sure you have probably looked up what the difference is and heard about things like Priority inversion and Priority inheritance.
But it is always nice when working through computer theory, to have a simple example that really shows what you are trying to learn, so I have created the following code that shows the basics of how and why you use a mutex in FreeRTOS.
Here is the code
#define LED 16
SemaphoreHandle_t GuardSerial;
void low_priority(void* args) {
while (true) {
xSemaphoreTake(GuardSerial, portMAX_DELAY);
for (int i = 0; i < 200 ; i++) {
Serial.print("L-");
}
Serial.println();
xSemaphoreGive(GuardSerial);
delay(500);
}
}
void medium_priority(void* args) {
pinMode(LED, OUTPUT);
while (true) {
for (int i = 0; i < 10000 ; i++) {
int k = sin(random(0, 1));
k += random(0, 20);
k -= random(0, 10);
k -= cos(random(0, 1));
k *= tan(random(0, 1));
//just waist some time
}
digitalWrite(LED, !digitalRead(LED));
delay(100);
}
}
void high_priority(void* args) {
while (true) {
xSemaphoreTake(GuardSerial, portMAX_DELAY);
Serial.print("*******high_priority task testing mutexes*******\n");
xSemaphoreGive(GuardSerial);
delay(500);
}
}
void setup() {
Serial.begin(9600);
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);
xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);
xTaskCreate(high_priority, "HIGH", 2048, NULL, 3, NULL);
}
void loop() {
}
Understanding this Code
Now that you have seen this example, lets go through each section and see how it works.
The LED
Part of this example is going to use an LED, so to start, we need to define which pin it is connected to.
#define LED 16
Mutex vs Semaphore
A mutex is just a binary semaphore with some added safety measures. Because of this, they are slightly slower than semaphores, but much more reliable (more on this later).
To be able to access a mutex, we will need to create a handler for it, and because mutexes are related to semaphores, we will use the standard semaphore handler like this.
SemaphoreHandle_t GuardSerial;
The Low Priority Task
This example uses three tasks, so lets start working on the first task. We will begin it like most task functions with an infinite loop and a delay.
void low_priority(void* args) {
while (true) {
delay(500);
}
}
Then we will add in some code that prints “L-” 200 times to the Serial port.
void low_priority(void* args) {
while (true) {
for (int i = 0; i < 200 ; i++) {
Serial.print("L-");
}
Serial.println();
delay(500);
}
}
For this example, we are using mutexes to guard the Serial port, so before we use the Serial port to print “L-” 200 times, we need to get permission from the mutex with the xSemaphoreTake() function.
That function requires two arguments the first one is the semaphore or mutex we want permission from and the second one is the maximum time we should wait for permission before giving up. portMAX_DELAY = forever
void low_priority(void* args) {
while (true) {
xSemaphoreTake(GuardSerial, portMAX_DELAY);
for (int i = 0; i < 200 ; i++) {
Serial.print("L-");
}
Serial.println();
delay(500);
}
}
And when we are done with the Serial port we need to release it, so the other tasks can use it with the xSemaphoreGive() function.
void low_priority(void* args) {
while (true) {
xSemaphoreTake(GuardSerial, portMAX_DELAY);
for (int i = 0; i < 200 ; i++) {
Serial.print("L-");
}
Serial.println();
xSemaphoreGive(GuardSerial);
delay(500);
}
}
The Middle Task
This next task doesn’t actually use mutexes, instead its only job is to waist computer power and blink an LED. Later, we will use it to show why mutexes are better than binary semaphores.
void medium_priority(void* args) {
pinMode(LED, OUTPUT);
while (true) {
for (int i = 0; i < 10000 ; i++) {
int k = sin(random(0, 1));
k += random(0, 20);
k -= random(0, 10);
k -= cos(random(0, 1));
k *= tan(random(0, 1));
//just waist some time
}
digitalWrite(LED, !digitalRead(LED));
delay(100);
}
}
The High Priority Task
This last task is the simplest of the bunch. All it does is get permission from the mutex, send data through the Serial port, release the mutex, and finally wait a while before doing it all over again.
void high_priority(void* args) {
while (true) {
xSemaphoreTake(GuardSerial, portMAX_DELAY);
Serial.print("*******high_priority task testing mutexes*******\n");
xSemaphoreGive(GuardSerial);
delay(500);
}
}
Wrapping it all Together
Up to this point, we have only created some task functions, now we need to turn them into real tasks. We will do all of this and a few other things in the setup() function. Here is what it starts out like.
void setup() {
Serial.begin(9600);
}
The first thing we will do is create a new mutex and then update the semaphore handler we created earlier to hold it.
void setup() {
Serial.begin(9600);
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
}
Above you may have noticed that one line of code is commented out. If you uncommit it and commit out the line that comes after it, the program will use a binary semaphore instead of a mutex.
The next thing we will do is create the low priority task, and if you were wondering, we will create it with a priority of one.
void setup() {
Serial.begin(9600);
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);
}
After that, we will create four medium priority tasks. We need four of them to just make sure the computer is always running at least one on each core.
void setup() {
Serial.begin(9600);
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);
xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);
}
And lastly we will create the high priority task.
void setup() {
Serial.begin(9600);
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);
xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);
xTaskCreate(high_priority, "HIGH", 2048, NULL, 3, NULL);
}
Why Use Mutexes
When we started, I said I would show you how and why to use a mutex. We have seen the how, now lets look at the why.
In this example at line 48, there is some code that is commented out.
// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();
If you uncommit the first line and commit out the second line, the example will use a binary semaphore instead of a mutex.
Try running it, if you do you will see something a little strange happening, or more accurately you will see nothing happening in the Serial Monitor, but the led will keep blinking.
What this means is that the low and high priority tasks have been halted, and only the medium priority task is running which is not what we want. The high priority task should always be run first, and that is what mutexes are for, they guarantee that your high priority tasks get run as soon as possible.
Mutexes can do a lot but not every thing. There are still a few things that semaphores are better at, and if you want to learn a little more about them, you might like to see a semaphore example.