Resumen

Este codelab fue creado para estudiar los mecanismos de comunicación entre tareas en FreeRTOS. Con este recurso se espera que usted al finalizar esté en capacidad de:

  • Identificar, comprender y aplicar los diferentes mecanismos de sincronización y comunicación entre tareas disponibles en FreeRTOS. Los estudiantes aprenderán a utilizar colas, semáforos, mutexes y notificaciones de tareas para gestionar la concurrencia y coordinar tareas en Sistemas Embebidos.

Fecha de Creación:

2024/03/01

Última Actualización:

2024/03/01

Requisitos Previos:

Adaptado de:

Referencias:

https://esp32tutorials.com/

https://www.digikey.com/en/maker/projects/what-is-a-realtime-operating-system-rtos/28d8087f53844decafa5000d89608016

Escrito por:

Fredy Segura-Quijano

En FreeRTOS, la sincronización y la comunicación entre tareas son aspectos importantes para el funcionamiento correcto de sistemas concurrentes y la gestión eficiente de recursos compartidos. FreeRTOS proporciona varias herramientas (funciones) y mecanismos para lograr esto, incluyendo colas, semáforos, mutexes, y notificaciones de tareas. Estos mecanismos pueden evitar Condiciones de Carrera y garantizar la integridad de los datos.

¿Qué son las Condiciones de Carrera?

Una Condición de Carrera (race condition) es un comportamiento indeseable que ocurre en sistemas concurrentes cuando el resultado o el comportamiento del sistema depende del orden o la temporización de las ejecuciones de múltiples tareas o hilos. Las Condiciones de Carrera pueden llevar a errores difíciles de reproducir y diagnosticar, ya que dependen de la interleavación específica (ejecución entrelazada de múltiples tareas) de operaciones concurrentes.

Por ejemplo cuando dos tareas (o hilos) intentan incrementar una variable global compartida, puede pasar que si ambas tareas leen la variable al mismo tiempo, la incrementan y escriben el resultado de vuelta, es posible que una actualización se pierda, resultando en un valor incorrecto.

int counter = 0;

void task1(void *pvParameters) {
    for (;;) {
        counter++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void task2(void *pvParameters) {
    for (;;) {
        counter++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

En este ejemplo, task1 y task2 incrementan counter independientemente. Si ambas tareas leen el valor de counter al mismo tiempo, incrementan el valor y escriben el nuevo valor de vuelta, una de las actualizaciones se perderá, resultando en un incremento incorrecto. Sin embargo es bueno entender el detalle de lo que puede ocurrir, dado que las tareas se asume que tienen prioridad y no se ejecutan al mismo tiempo.

Aunque las tareas parecen ejecutarse independientemente, el problema surge cuando ambas acceden y modifican la variable contador casi al mismo tiempo. Un posible escenario sería:

  1. Tarea 1 lee el valor de contador (digamos que es 0).
  2. Tarea 1 incrementa el valor de contador a 1, pero no ha escrito el valor de vuelta aún.
  3. Tarea 2 se ejecuta antes de que Tarea 1 escriba el valor de vuelta y lee el valor de contador (que sigue siendo 0).
  4. Tarea 2 incrementa el valor de contador a 1.
  5. Ambas tareas escriben el valor de vuelta a contador. Ambas escriben 1, lo que significa que una actualización se ha perdido y contador solo se incrementa una vez en lugar de dos.

Este problema ocurre debido a que la lectura, modificación y escritura de la variable no son operaciones atómicas (no se ejecutan como una sola operación indivisible).

Una posible solución a las Condiciones de Carrera es el uso de mecanismos de sincronización como semáforos o mutex. A continuación se presenta el mismo código, con la propuesta de usar un Mutex (es un tipo de semáforo). Aunque el detalle de mutex lo estudiaremos más adelante, el propósito es dar una idea de su funcionalidad.

#include <Arduino.h>

int contador = 0;
SemaphoreHandle_t xMutex;

void tarea1(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
            contador++;
            Serial.print("Tarea 1 contador: ");
            Serial.println(contador);
            xSemaphoreGive(xMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void tarea2(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
            contador++;
            Serial.print("Tarea 2 contador: ");
            Serial.println(contador);
            xSemaphoreGive(xMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(1500));
    }
}

void setup() {
    Serial.begin(115200);

    // Crear el mutex antes de usarlo
    xMutex = xSemaphoreCreateMutex();
    if (xMutex != NULL) {
        xTaskCreate(tarea1, "Tarea 1", 1000, NULL, 1, NULL);
        xTaskCreate(tarea2, "Tarea 2", 1000, NULL, 1, NULL);
    } else {
        Serial.println("Error al crear el mutex.");
    }
}

void loop() {
    // No hacer nada aquí
}

Explicación del Código:

  1. Creación del Mutex:
  1. Uso del Mutex en las Tareas:

De esta forma los mutex aseguran que solo una tarea pueda acceder a la sección crítica del código a la vez. Los mutex son de fácil implementación y proveen un método seguro para la sincronización de tareas en sistemas multitarea.

Las colas (queues) en FreeRTOS son estructuras de datos utilizadas para la comunicación y sincronización entre tareas. Permiten que una tarea envíe datos a otra tarea de manera segura y eficiente, garantizando que los datos se entreguen en el orden en que se enviaron. Una cola es un sistema FIFO simple con lecturas y escrituras atómicas. Las "operaciones atómicas" son aquellas que no pueden ser interrumpidas por otras tareas durante su ejecución. Esto garantiza que otra tarea no pueda sobrescribir los datos, mientras que la tarea anterior no termine de realizar su funcionalidad completa.

Dentro de las ventajas que tiene el uso de colas están:

  1. Comunicación Segura: las colas proporcionan un mecanismo seguro para pasar datos entre tareas, evitando condiciones de carrera y garantizando la integridad de los datos.
  2. Orden de Entrega: los datos en las colas se entregan en el mismo orden en que se enviaron, lo que garantiza la consistencia en la comunicación.
  3. Flexibilidad: pueden contener datos de diferentes tipos, incluidos enteros, estructuras y punteros.
  4. Bloqueo y desbloqueo: las tareas pueden ser bloqueadas mientras esperan datos en la cola, liberando CPU para otras tareas.

Dentro de las desventajas que tiene el uso de colas están:

  1. Overhead de memoria: las colas requieren memoria adicional para almacenar los datos y la estructura de la cola.
  2. Latencia: el tiempo necesario para copiar datos dentro y fuera de la cola puede introducir latencia.
  3. Complejidad de implementación: Configurar y gestionar colas correctamente puede ser complejo en sistemas grandes y concurridos.

Ejemplo de Uso:

El objetivo del siguiente ejemplo usando FreeRTOS es crear dos tareas y dos colas. Las tareas deben realizar las siguientes funcionalidades:

Tarea A:

Tarea B:

Librerías:

#include <Arduino.h>

Configuración del Núcleo:

#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

Configuración de Parámetros y Pines:

static const uint8_t buf_len = 255;
static const char command[] = "delay ";
static const int delay_queue_len = 5;
static const int msg_queue_len = 5;
static const uint8_t blink_max = 100;
static const int led_pin = LED_BUILTIN;

Estructura de Mensaje:

typedef struct Message {
  char body[20];
  int count;
} Message;

Variables Globales:

static QueueHandle_t delay_queue;
static QueueHandle_t msg_queue;

Tarea CLI:

void doCLI(void *parameters) {
  Message rcv_msg;
  char c;
  char buf[buf_len];
  uint8_t idx = 0;
  uint8_t cmd_len = strlen(command);
  int led_delay;

  memset(buf, 0, buf_len);

  while (1) {
    if (xQueueReceive(msg_queue, (void *)&rcv_msg, 0) == pdTRUE) {
      Serial.print("Received message: ");
      Serial.print(rcv_msg.body);
      Serial.println(rcv_msg.count);
    }

    if (Serial.available() > 0) {
      c = Serial.read();
      if (idx < buf_len - 1) {
        buf[idx] = c;
        idx++;
      } else {
        Serial.println("Buffer overflow, resetting buffer");
        memset(buf, 0, buf_len);
        idx = 0;
      }

      if ((c == '\n') || (c == '\r')) {
        Serial.print("\r\n");
        if (memcmp(buf, command, cmd_len) == 0) {
          char* tail = buf + cmd_len;
          led_delay = atoi(tail);
          led_delay = abs(led_delay);
          if (xQueueSend(delay_queue, (void *)&led_delay, 10) != pdTRUE) {
            Serial.println("ERROR: Could not put item on delay queue.");
          }
        }
        memset(buf, 0, buf_len);
        idx = 0;
      } else {
        Serial.print(c);
      }
    } 
  }
}

Tarea de Parpadeo de LED:

void blinkLED(void *parameters) {
  Message msg;
  int led_delay = 500;
  uint8_t counter = 0;

  pinMode(LED_BUILTIN, OUTPUT);

  while (1) {
    if (xQueueReceive(delay_queue, (void *)&led_delay, 0) == pdTRUE) {
      strcpy(msg.body, "Message received ");
      msg.count = 1;
      xQueueSend(msg_queue, (void *)&msg, 10);
    }

    digitalWrite(led_pin, HIGH);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);

    counter++;
    if (counter >= blink_max) {
      strcpy(msg.body, "Blinked: ");
      msg.count = counter;
      if (xQueueSend(msg_queue, (void *)&msg, 10) != pdTRUE) {
        Serial.println("ERROR: Could not put item on msg queue.");
      } else {
        Serial.println("Message sent to msg_queue.");
      }
      counter = 0;
    }
  }
}

Configuración Inicial:

void setup() {
  Serial.begin(115200);
  vTaskDelay(1000 / portTICK_PERIOD_MS);
  Serial.println();
  Serial.println("---FreeRTOS Queue Solution---");
  Serial.println("Enter the command 'delay xxx' where xxx is your desired ");
  Serial.println("LED blink delay time in milliseconds");

  delay_queue = xQueueCreate(delay_queue_len, sizeof(int));
  msg_queue = xQueueCreate(msg_queue_len, sizeof(Message));

  if (delay_queue == NULL || msg_queue == NULL) {
    Serial.println("ERROR: Could not create one or more queues.");
    while (true); // Halt execution if queues are not created
  }

  xTaskCreatePinnedToCore(doCLI,
                          "CLI",
                          2048,
                          NULL,
                          1,
                          NULL,
                          app_cpu);

  xTaskCreatePinnedToCore(blinkLED,
                          "Blink LED",
                          2048,
                          NULL,
                          1,
                          NULL,
                          app_cpu);

  vTaskDelete(NULL);
}

Función loop:

void loop() {
  // No hacer nada
  // setup() y loop() se ejecutan en su propia tarea con prioridad 1 en el núcleo 1
  // en ESP32
}

Código completo: A continuación se presenta el código completo del uso de colas; para que se pueda validar en VSCode y programar la ESP32. No olvide ver el comportamiento del led de la tarjeta y el "serial monitor"; para validar funcionalidad. Active en el monitor serial la función "line ending=LF"; para que se generen los finales de línea y envíe datos del monitor serial a la tarjeta ESP32.

/*######################################################################
# C CODE.
######################################################################
# Copyright (C) 2024. F.E.Segura-Quijano (FES) fsegura@uniandes.edu.co
#  
# Este trabajo está licenciado bajo la Licencia: 
# Creative Commons Atribución-NoComercial 4.0 Internacional.
# Para ver una copia de esta licencia, visita 
# http://creativecommons.org/licenses/by-nc/4.0/ o envía una carta
# a Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
######################################################################*/

/*######################################################################
# Parte de este código se basa en los ejemplos FreeRTOS de:
# https://github.com/ShawnHymel/introduction-to-rtos/tree/main 
# Parte de este código fue generado con la asistencia de ChatGPT de OpenAI.
######################################################################*/

/*######################################################################
# LIBRARIES.
######################################################################*/
#include <Arduino.h>

// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// Settings
static const uint8_t buf_len = 255;     // Size of buffer to look for command
static const char command[] = "delay "; // Note the space!
static const int delay_queue_len = 5;   // Size of delay_queue
static const int msg_queue_len = 5;     // Size of msg_queue
static const uint8_t blink_max = 100;   // Num times to blink before message

// Pins (change this if your Arduino board does not have LED_BUILTIN defined)
static const int led_pin = LED_BUILTIN;

// Message struct: used to wrap strings (not necessary, but it's useful to see
// how to use structs here)
typedef struct Message {
  char body[20];
  int count;
} Message;

// Globals
static QueueHandle_t delay_queue;
static QueueHandle_t msg_queue;

/*######################################################################
# TASKs.
######################################################################*/
// Task: command line interface (CLI)
void doCLI(void *parameters) {
  Message rcv_msg;
  char c;
  char buf[buf_len];
  uint8_t idx = 0;
  uint8_t cmd_len = strlen(command);
  int led_delay;

  // Clear whole buffer
  memset(buf, 0, buf_len);

  // Loop forever
  while (1) {

    // See if there's a message in the queue (do not block)
    if (xQueueReceive(msg_queue, (void *)&rcv_msg, 0) == pdTRUE) {
      Serial.print("Received message: ");
      Serial.print(rcv_msg.body);
      Serial.println(rcv_msg.count);
    }

    // Read characters from serial
    if (Serial.available() > 0) {
      c = Serial.read();

      // Store received character to buffer if not over buffer limit
      if (idx < buf_len - 1) {
        buf[idx] = c;
        idx++;
      } else {
        // If buffer limit is reached, reset the buffer to prevent overflow
        Serial.println("Buffer overflow, resetting buffer");
        memset(buf, 0, buf_len);
        idx = 0;
      }

      // Print newline and check input on 'enter'
      if ((c == '\n') || (c == '\r')) {

        // Print newline to terminal
        Serial.print("\r\n");

        // Check if the first 6 characters are "delay "
        if (memcmp(buf, command, cmd_len) == 0) {

          // Convert last part to positive integer (negative int crashes)
          char* tail = buf + cmd_len; // <--- Corrección aplicada aquí
          led_delay = atoi(tail);
          led_delay = abs(led_delay);

          // Send integer to other task via queue
          if (xQueueSend(delay_queue, (void *)&led_delay, 10) != pdTRUE) {
            Serial.println("ERROR: Could not put item on delay queue.");
          }
        }

        // Reset receive buffer and index counter
        memset(buf, 0, buf_len);
        idx = 0;

      // Otherwise, echo character back to serial terminal
      } else {
        Serial.print(c);
      }
    } 
  }
}


// Task: flash LED based on delay provided, notify other task every 20 blinks
void blinkLED(void *parameters) {
  Message msg;
  int led_delay = 500;
  uint8_t counter = 0;

  // Set up pin
  pinMode(LED_BUILTIN, OUTPUT);

  // Loop forever
  while (1) {

    // See if there's a message in the queue (do not block)
    if (xQueueReceive(delay_queue, (void *)&led_delay, 0) == pdTRUE) {
      // Best practice: use only one task to manage serial comms
      strcpy(msg.body, "Message received ");
      msg.count = 1;
      xQueueSend(msg_queue, (void *)&msg, 10);
    }

    // Blink
    digitalWrite(led_pin, HIGH);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);

    // If we've blinked 100 times, send a message to the other task
    counter++;
    if (counter >= blink_max) {
      
      // Construct message and send
      strcpy(msg.body, "Blinked: ");
      msg.count = counter;
      if (xQueueSend(msg_queue, (void *)&msg, 10) != pdTRUE) {
        Serial.println("ERROR: Could not put item on msg queue.");
      } else {
        Serial.println("Message sent to msg_queue.");
      }

      // Reset counter
      counter = 0;
    }
  }
}

/*######################################################################
# SETUP.
######################################################################*/
void setup() {
  // Configure Serial
  Serial.begin(115200);

  // Wait a moment to start (so we don't miss Serial output)
  vTaskDelay(1000 / portTICK_PERIOD_MS);
  Serial.println();
  Serial.println("---FreeRTOS Queue Solution---");
  Serial.println("Enter the command 'delay xxx' where xxx is your desired ");
  Serial.println("LED blink delay time in milliseconds");

  // Create queues
  delay_queue = xQueueCreate(delay_queue_len, sizeof(int));
  msg_queue = xQueueCreate(msg_queue_len, sizeof(Message));

 // Check if the queues were created successfully
  if (delay_queue == NULL || msg_queue == NULL) {
    Serial.println("ERROR: Could not create one or more queues.");
    while (true); // Halt execution if queues are not created
  }

  // Start CLI task
  xTaskCreatePinnedToCore(doCLI,
                          "CLI",
                          2048,
                          NULL,
                          1,
                          NULL,
                          app_cpu);

  // Start blink task
  xTaskCreatePinnedToCore(blinkLED,
                          "Blink LED",
                          2048,
                          NULL,
                          1,
                          NULL,
                          app_cpu);

  // Delete "setup and loop" task
  vTaskDelete(NULL);
}

/*######################################################################
# LOOP.
######################################################################*/
void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

A continuación presentamos una captura de pantalla del monitor serial donde se puede ver la interacción con el programa del ejemplo:

[Programación y pruebas con el código completo ejemplo uso de colas.]

Casos de uso de las colas

En FreeRTOS, los semáforos son herramientas de sincronización utilizadas para controlar el acceso a recursos compartidos y coordinar la ejecución de tareas. Los semáforos pueden ser binarios (que solo tienen dos estados, tomado y disponible) o de conteo (que permiten contar eventos o recursos disponibles). También existen los mutexes, que son un tipo especial de semáforo utilizado para exclusión mutua.

Dentro de las ventajas que tiene el uso de semáforos están:

  1. Sincronización: permiten la sincronización efectiva entre tareas y entre tareas e interrupciones.
  2. Control de acceso: controlan el acceso a recursos compartidos, evitando condiciones de carrera.
  3. Simplicidad: son relativamente simples de usar e implementar.
  4. Versatilidad: pueden ser utilizados para múltiples propósitos, como exclusión mutua, señales y contadores de eventos.

Dentro de las desventajas que tiene el uso de semáforos están:

  1. Overhead: introducen un cierto overhead en la gestión de recursos y el cambio de contexto.
  2. Complejidad en sistemas grandes: en sistemas grandes y complejos, la correcta implementación y gestión de semáforos puede volverse complicada.
  3. Posibles bloqueos: Mal uso de semáforos puede llevar a bloqueos (deadlocks) si no se gestionan correctamente.

Ejemplo de uso de semáforos binarios:

Para controlar dos tareas que acceden a un recurso compartido, como un archivo o una sección de memoria, se puede usar un mutex para asegurar de que solo una tarea puede acceder al recurso a la vez.

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>
#include <Arduino.h>

SemaphoreHandle_t xMutex;

void vTask1(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // Acceder al recurso compartido
            Serial.println("Tarea 1 accediendo al recurso compartido");
            vTaskDelay(pdMS_TO_TICKS(500)); // Simular trabajo
            Serial.println("Tarea 1 liberando el recurso compartido");
            xSemaphoreGive(xMutex);
            vTaskDelay(pdMS_TO_TICKS(1000)); // Esperar antes de intentar acceder de nuevo
        }
    }
}

void vTask2(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // Acceder al recurso compartido
            Serial.println("Tarea 2 accediendo al recurso compartido");
            vTaskDelay(pdMS_TO_TICKS(500)); // Simular trabajo
            Serial.println("Tarea 2 liberando el recurso compartido");
            xSemaphoreGive(xMutex);
            vTaskDelay(pdMS_TO_TICKS(1500)); // Esperar antes de intentar acceder de nuevo
        }
    }
}

void setup() {
    Serial.begin(115200);

    // Crear el mutex antes de usarlo
    xMutex = xSemaphoreCreateMutex();
    if (xMutex != NULL) {
        xTaskCreate(vTask1, "Tarea 1", 1000, NULL, 1, NULL);
        xTaskCreate(vTask2, "Tarea 2", 1000, NULL, 1, NULL);
    }

    // Iniciar el scheduler
    vTaskStartScheduler();
}

void loop() {
    // No hacer nada aquí
}

Note que este ejemplo es similar al explicado previamente con las Condiciones de Carrera.

Explicación del Ejemplo

Ejemplo de uso de semáforos binarios y conteo:

En el siguiente ejemplo se busca lograr una coordinación de Tareas con Semáforos Binarios y de Semáforos de Conteo. Así, se proponen tres tareas:

Los semáforos a utilizar se proponen así: un semáforo binario se usa para señalar la disponibilidad de datos en el buffer, y un semáforo de conteo se usa para llevar la cuenta de la cantidad de datos procesados.

A continuación se presenta una posible implementación para resolver el ejemplo:

/*######################################################################
# C CODE.
######################################################################
# Copyright (C) 2024. F.E.Segura-Quijano (FES) fsegura@uniandes.edu.co
#  
# Este trabajo está licenciado bajo la Licencia: 
# Creative Commons Atribución-NoComercial 4.0 Internacional.
# Para ver una copia de esta licencia, visita 
# http://creativecommons.org/licenses/by-nc/4.0/ o envía una carta
# a Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
######################################################################*/

/*######################################################################
# Parte de este código se basa en los ejemplos FreeRTOS de:
# https://github.com/ShawnHymel/introduction-to-rtos/tree/main 
# Parte de este código fue generado con la asistencia de ChatGPT de OpenAI.
######################################################################*/

/*######################################################################
# LIBRARIES.
######################################################################*/
#include <Arduino.h>

// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

#define BUFFER_SIZE 10

// Buffers and indices
int buffer[BUFFER_SIZE];
int bufferIndex = 0;

// Semaphores
SemaphoreHandle_t xDataSemaphore;
SemaphoreHandle_t xCountSemaphore;
int dataCount = 0;

/*######################################################################
# TASKs.
######################################################################*/
// Producer Task
void vProducerTask(void *pvParameters) {
    while (1) {
        // Produce data
        buffer[bufferIndex] = random(100);
        Serial.print("Produced: ");
        Serial.println(buffer[bufferIndex]);

        // Signal that data is available
        xSemaphoreGive(xDataSemaphore);

        // Increment buffer index
        bufferIndex = (bufferIndex + 1) % BUFFER_SIZE;

        // Delay for a random time to simulate data production
        vTaskDelay(pdMS_TO_TICKS(random(500, 1000)));
    }
}

// Consumer Task 1
void vConsumerTask1(void *pvParameters) {
    int data;
    while (1) {
        // Wait for data to be available
        if (xSemaphoreTake(xDataSemaphore, portMAX_DELAY) == pdTRUE) {
            // Consume data
            bufferIndex = (bufferIndex == 0) ? BUFFER_SIZE - 1 : bufferIndex - 1;
            data = buffer[bufferIndex];
            Serial.print("Consumer 1 processed: ");
            Serial.println(data);

            // Signal that data has been processed
            xSemaphoreGive(xCountSemaphore);
        }
    }
}

// Consumer Task 2
void vConsumerTask2(void *pvParameters) {
    int data;
    while (1) {
        // Wait for data to be available
        if (xSemaphoreTake(xDataSemaphore, portMAX_DELAY) == pdTRUE) {
            // Consume data
            bufferIndex = (bufferIndex == 0) ? BUFFER_SIZE - 1 : bufferIndex - 1;
            data = buffer[bufferIndex];
            Serial.print("Consumer 2 processed: ");
            Serial.println(data);

            // Signal that data has been processed
            xSemaphoreGive(xCountSemaphore);
        }
    }
}

/*######################################################################
# SETUP.
######################################################################*/
void setup() {
  // Configure Serial
      Serial.begin(115200);

    // Create the semaphores
    xDataSemaphore = xSemaphoreCreateBinary();
    xCountSemaphore = xSemaphoreCreateCounting(BUFFER_SIZE, 0);

    // Check if the semaphores were created successfully
    if (xDataSemaphore != NULL && xCountSemaphore != NULL) {
        // Create tasks
        xTaskCreate(vProducerTask, "Producer", 1000, NULL, 1, NULL);
        xTaskCreate(vConsumerTask1, "Consumer 1", 1000, NULL, 1, NULL);
        xTaskCreate(vConsumerTask2, "Consumer 2", 1000, NULL, 1, NULL);

    } else {
        Serial.println("Error creating semaphores.");
    }
}

/*######################################################################
# LOOP.
######################################################################*/
void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

Explicación del Código:

Definición de Semáforos:

Tareas:

Control de Buffer:

Creación de Semáforos y Tareas:

A continuación presentamos una captura de pantalla del monitor serial donde se puede ver la interacción con el programa del ejemplo:

Casos de uso de los semáforos:

¿Cuál es entonces la diferencia entre un mutex y un semáforo binario?

Un mutex es una herramienta de sincronización que asegura que solo una tarea (o hilo) pueda acceder a un recurso compartido en un momento dado. El propósito principal de un mutex es la exclusión mutua. Un mutex tiene un concepto de propiedad, es decir que la tarea que toma el mutex debe ser la misma que lo libera. Garantiza que solo una tarea tenga acceso al recurso protegido por el mutex a la vez. Un mutex es ideal para proteger recursos críticos, como variables globales, secciones de código o dispositivos de hardware. Un mutex puede soportar mecanismos de herencia de prioridad para evitar la inversión de prioridad.

Un semáforo binario es una herramienta de sincronización que puede tener solo dos estados: tomado (1) o disponible (0). Se usa principalmente para la señalización y sincronización entre tareas o entre tareas e interrupciones. A diferencia de un mutex, un semáforo binario no tiene un concepto de propiedad, es decir que cualquier tarea puede tomar (decrementar) o dar (incrementar) el semáforo. Es útil para la sincronización simple entre tareas o para señalizar eventos. Aunque puede usarse para exclusión mutua, no es su propósito principal y no tiene soporte para herencia de prioridad. Un semáforo binario aunque puede usarse para exclusión mutua, no es su propósito principal y no tiene soporte para herencia de prioridad.

Herencia de Prioridad en Mutexes.

La herencia de prioridad es un mecanismo utilizado en sistemas de tiempo real para evitar un problema conocido como inversión de prioridad. Este mecanismo permite que una tarea con alta prioridad "herede" temporalmente la prioridad de una tarea con menor prioridad que posee un mutex, con el fin de prevenir bloqueos o retardos innecesarios en el sistema.

La inversión de prioridad ocurre cuando una tarea de alta prioridad se ve bloqueada esperando un recurso que está siendo utilizado por una tarea de baja prioridad, y una tarea de prioridad media impide que la tarea de baja prioridad libere el recurso. Esto puede llevar a que la tarea de alta prioridad no cumpla sus plazos de tiempo real, lo cual es crítico en sistemas de tiempo real.

Ejemplo de Inversión de Prioridad:

Herencia de Prioridad: La herencia de prioridad resuelve este problema al elevar temporalmente la prioridad de la tarea que posee el mutex (Tarea A) al nivel de la tarea más alta que está esperando por el mutex (Tarea B). Esto asegura que Tarea A se ejecute y libere el mutex lo antes posible, permitiendo que Tarea B continúe su ejecución. En FreeRTOS, los mutexes soportan herencia de prioridad de manera automática.

Las Notificaciones de Tareas en FreeRTOS son un mecanismo ligero y eficiente para la comunicación y sincronización entre tareas. Cada tarea tiene un array de valores de notificación que otras tareas pueden utilizar para enviarle datos o señales. Las notificaciones de tareas pueden considerarse una alternativa más rápida y sencilla a las colas y semáforos para ciertas aplicaciones.

Dentro de las ventajas que tiene el uso de notificación de tareas están:

  1. Bajo Overhead: las notificaciones de tareas son muy rápidas y utilizan menos recursos que las colas y semáforos, ya que no requieren memoria dinámica.
  2. Simplicidad: el uso de notificaciones de tareas es más simple en comparación con las colas y semáforos.
  3. Multifuncionalidad: pueden ser usadas tanto para la señalización (similar a un semáforo) como para la transferencia de datos (similar a una cola).
  4. Eficiencia: ideal para aplicaciones que requieren una señalización rápida y de baja latencia entre tareas.

Dentro de las desventajas que tiene el uso de notificación de tareas están:

  1. Capacidad Limitada: cada tarea solo puede manejar una notificación a la vez, lo que puede ser una limitación en aplicaciones más complejas.
  2. Uso Exclusivo: las notificaciones no pueden ser compartidas entre múltiples tareas. Cada notificación está vinculada a una tarea específica.
  3. Funcionalidad Básica: no ofrecen todas las características avanzadas de las colas y semáforos, como la gestión de prioridad y la capacidad de contener múltiples datos.

Ejemplo de Uso:

#include <Arduino.h>

// Handles de las tareas
TaskHandle_t xProducerTaskHandle = NULL;
TaskHandle_t xConsumerTaskHandle = NULL;

void vProducerTask(void *pvParameters) {
    uint32_t ulValueToNotify = 0;

    while (1) {
        // Produce un valor de datos
        ulValueToNotify++;

        // Notifica a la tarea consumidora
        xTaskNotifyGive(xConsumerTaskHandle);

        // Simula un retardo en la producción de datos
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vConsumerTask(void *pvParameters) {
    uint32_t ulNotificationValue;

    while (1) {
        // Espera la notificación de la tarea productora
        ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        // Procesa los datos recibidos
        Serial.print("Dato recibido: ");
        Serial.println(ulNotificationValue);
    }
}

void setup() {
    Serial.begin(115200);

    // Crear las tareas
    xTaskCreate(vProducerTask, "Productor", 1000, NULL, 1, &xProducerTaskHandle);
    xTaskCreate(vConsumerTask, "Consumidor", 1000, NULL, 1, &xConsumerTaskHandle);

}

void loop() {
    // No hacer nada aquí
}

Explicación del Código

Definición de las Tareas:

Notificaciones de Tareas:

Configuración y Ejecución:

Casos de uso de las notificaciones de tareas: