Examples are one of the best tools for learning a new library, and FreeRTOS is no exception, so to help you along in your journey of learning FreeRTOS and in particular FreeRTOS Semaphores here is a quick example.
It works by taking two lists of numbers and adding them together. It does this three times the first time with one task doing all the work, the second with two, and the third with five, and to keep all of those tasks in sync, we will use a counting semaphore.
Here is the code
int A[7] = {1, 2, 3, 4, 5, 6, 7};
int B[7] = {4, 7, 3, 8, 2, 2, 9};
SemaphoreHandle_t data_left;
void process(void* arg) {
while (xSemaphoreTake(data_left, portMAX_DELAY)) {
unsigned int count = uxSemaphoreGetCount(data_left);
Serial.printf("%i: %i + %i = %i \n", count, A[count], B[count], A[count] + B[count]);
delay(500);
}
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using One Thread");
xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
time = millis();
vSemaphoreDelete(data_left);
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using Two Threads");
xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
time = millis();
vSemaphoreDelete(data_left);
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using Five Threads");
xTaskCreate(process, "P-THREE", 2048, NULL, 1, NULL);
xTaskCreate(process, "P-FOUR", 2048, NULL, 1, NULL);
xTaskCreate(process, "P-FIVE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
}
void loop() {
}
How This Code Works
Now, that you have seen the example code, why don’t we look through the interesting sections of it and see what they do and how they work.
The Numbers
The first thing the program does is create two lists with 7 numbers in each. These will be the numbers we will add together later.
int A[7] = {1, 2, 3, 4, 5, 6, 7};
int B[7] = {4, 7, 3, 8, 2, 2, 9};
Creating The Semaphore Handler
You don’t actually access semaphores directly, instead you have to go through a semaphore handler, so the next thing, we will do, is create one.
SemaphoreHandle_t data_left;
The Task Function
We will start out with a basic task function that we will call process.
void process(void* arg) {
vTaskDelete(NULL);
}
Next, we are going to add an infinite loop that checks, if there is any data left to process.
void process(void* arg) {
while (xSemaphoreTake(data_left, portMAX_DELAY)) {
}
vTaskDelete(NULL);
}
If there is data to process, we will find the position of the data, process it(aka. add the two numbers together), print the result to Serial, and then delay for 500 milliseconds.
void process(void* arg) {
while (xSemaphoreTake(data_left, portMAX_DELAY)) {
unsigned int count = uxSemaphoreGetCount(data_left);
Serial.printf("%i: %i + %i = %i \n", count, A[count], B[count], A[count] + B[count]);
delay(500);
}
vTaskDelete(NULL);
}
Running The Task
This last bit of code is all run in side of the setup function.
void setup() {
Serial.begin(115200);
}
We are going to time how long it takes to process the data, so we will begin by recording the starting time in a variable.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
}
Then, we will create a new semaphore and put it in the semaphore handler we made earlier. This semaphore is a counting semaphores which means when we create it, we need to give it a max value and starting value.
Because we have 7 pairs of numbers to add, we will set the max value to 7 and the starting value to 7 as well.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
}
Then, we will setup our task function. At this point, everything is done for the first test.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using One Thread");
xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
}
Next, we will tell the setup function to wait until all the numbers have been processed, and then print how long it took.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using One Thread");
xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
}
The next test will use two tasks. We already have one task running in the background, so now all we need to do is create another one, reset the data, and of course we will need to time everything.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using One Thread");
xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
time = millis();
vSemaphoreDelete(data_left);
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using Two Threads");
xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
}
The third test is just like the last two, except now there are 5 tasks running.
void setup() {
Serial.begin(115200);
unsigned int time = millis();
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using One Thread");
xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
time = millis();
vSemaphoreDelete(data_left);
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using Two Threads");
xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
time = millis();
vSemaphoreDelete(data_left);
data_left = xSemaphoreCreateCounting(7, 7);
Serial.println("Using Five Threads");
xTaskCreate(process, "P-THREE", 2048, NULL, 1, NULL);
xTaskCreate(process, "P-FOUR", 2048, NULL, 1, NULL);
xTaskCreate(process, "P-FIVE", 2048, NULL, 1, NULL);
while (uxSemaphoreGetCount(data_left) != 0) delay(100);
Serial.printf("Time: %i\n", millis() - time);
}
This is what you should see in the Serial Monitor when you run the code.
A Little Misleading
If you look at the results from the test, you will see that it takes 3 seconds for the test with one task, 1.5 seconds for the test with two tasks, and 0.5 seconds for the test with five tasks.
Because of this you might be led to think, that the more tasks the better, but because we have the delay(500); in the process() function, the results you are seeing are greatly exaggerated.
In reality, because the ESP32 is a two core microcontroller, it should hit maximum performance while using two tasks.
I you are looking for some more cool examples about FreeRTOS, you might like to see how FreeRTOS Queues work.