Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Developing with FreeRTOS on ESP32: Multitasking and Synchronization

June 12, 2024

Developing with FreeRTOS on ESP32: Multitasking and Synchronization

Welcome back to our ongoing series on developing with FreeRTOS on the ESP32 microcontroller. In the previous posts, we have looked at the basics of FreeRTOS as well as task management. Today, we will dive deeper into multitasking and synchronization techniques with FreeRTOS on ESP32.

Multitasking with FreeRTOS

Microcontrollers often require handling multiple tasks simultaneously. FreeRTOS allows us to implement multitasking efficiently. Each task in FreeRTOS has its own stack, program counter, and registers. The tasks may run concurrently or be scheduled by the RTOS kernel.

Let’s consider a scenario where we need to blink an LED and monitor a button simultaneously. We can handle these tasks in separate FreeRTOS tasks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void ledTask(void *parameters) {
    pinMode(LED_PIN, OUTPUT);
    
    while (1) {
        digitalWrite(LED_PIN, HIGH);
        vTaskDelay(pdMS_TO_TICKS(500));
        digitalWrite(LED_PIN, LOW);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void buttonTask(void *parameters) {
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    while (1) {
        if (digitalRead(BUTTON_PIN) == LOW) {
            // Button pressed, do something
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

Here, we define ledTask and buttonTask as separate tasks, each performing its respective job. The vTaskDelay function is used to suspend the task for a given number of milliseconds.

In your setup function, you can create these tasks using xTaskCreate:

1
2
xTaskCreatePinnedToCore(ledTask, "LED Task", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(buttonTask, "Button Task", 2048, NULL, 2, NULL, 0);

By creating the tasks, we ensure that the LED and button tasks run concurrently without blocking each other.

Synchronization with FreeRTOS

Often, tasks need to synchronize their activities or share resources. FreeRTOS provides various synchronization primitives that allow tasks to communicate and cooperate efficiently.

Mutex

Mutexes (mutual exclusions) are the most commonly used synchronization primitive. They protect shared resources from simultaneous access by multiple tasks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

void task1(void *parameters) {
    while (1) {
        // Acquire the mutex
        if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) {
            // Access the shared resource
            // ...
            
            // Release the mutex
            xSemaphoreGive(mutex);
        }
    }
}

void task2(void *parameters) {
    while (1) {
        // Acquire the mutex
        if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) {
            // Access the shared resource
            // ...
            
            // Release the mutex
            xSemaphoreGive(mutex);
        }
    }
}

In this example, mutex is created using xSemaphoreCreateMutex(). Each task then acquires the mutex using xSemaphoreTake() before accessing the shared resource and releases it using xSemaphoreGive() afterward.

Semaphores

Semaphores are used for signaling between tasks. They ensure that tasks proceed based on specific events or conditions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary();

void task1(void *parameters) {
    while (1) {
        // Wait for the semaphore
        if (xSemaphoreTake(semaphore, portMAX_DELAY) == pdTRUE) {
            // Semaphore received, proceed
            // ...
        }
    }
}

void task2(void *parameters) {
    while (1) {
        // Signal the semaphore
        xSemaphoreGive(semaphore);
        
        // Delay before signaling again
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Here, we use xSemaphoreCreateBinary() to create the semaphore. Task 2 periodically signals the semaphore using xSemaphoreGive(), and Task 1 waits for the semaphore using xSemaphoreTake(). This allows Task 1 to proceed only when signaled by Task 2.

Queues

Queues enable communication between tasks by allowing them to send and receive messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
QueueHandle_t queue = xQueueCreate(5, sizeof(int));

void task1(void *parameters) {
    while (1) {
        int data;
        
        // Receive data from the queue
        if (xQueueReceive(queue, &data, portMAX_DELAY) == pdTRUE) {
            // Data received, process it
            // ...
        }
    }
}

void task2(void *parameters) {
    while (1) {
        int data = // ...;
        
        // Send data to the queue
        xQueueSend(queue, &data, portMAX_DELAY);
        
        // Delay before sending again
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

In this example, queue is created using xQueueCreate(). Task 2 sends data to the queue using xQueueSend(), and Task 1 receives data from the queue using xQueueReceive(). This allows tasks to exchange data or information asynchronously.

Conclusion

Using FreeRTOS on ESP32 allows us to efficiently develop multitasking applications with synchronization mechanisms. We explored multitasking with separate tasks as well as synchronization techniques using mutexes, semaphores, and queues. These tools empower us to design robust and responsive embedded systems.

In the next post, we will look at interrupt handling with FreeRTOS on ESP32. Stay tuned!

Happy coding!


➡️ Linux cat command


⬅️ Linux tail command


Go back to Posts.