freertos-patron-productor-consumidor

Patrón productor-consumidor en FreeRTOS

  • 4 min

El patrón Productor-Consumidor es una forma de separar quien genera datos de quien los procesa usando una cola intermedia para desacoplar ambas tareas.

Es uno de los patrones más útiles en FreeRTOS porque aparece por todas partes. Un sensor produce lecturas, una tarea las consume. Una interrupción produce eventos, una tarea los procesa. Una conexión recibe mensajes, otra los interpreta. La idea es siempre la misma.

El problema

Supongamos que tenemos una tarea que lee un sensor cada 100ms y otra que envía esos datos por WiFi. Podríamos usar una variable global:

float temperatura;
Copied!

Y ya está, ¿no? Pues no tan rápido.

Si una tarea escribe mientras otra lee, podemos tener datos incoherentes. Además, si el envío por WiFi tarda más de la cuenta, la lectura del sensor queda mezclada con una lógica que no le corresponde.

Lo limpio es separar responsabilidades:

  • El productor lee el sensor y mete datos en una cola.
  • El consumidor espera datos y los procesa cuando llegan.

La cola hace de “bandeja de entrada”. Si el consumidor va un poco más lento, los mensajes se acumulan hasta el límite configurado.

Estructura del patrón

El esquema básico es este:

Tarea Productora  ->  Cola  ->  Tarea Consumidora
Copied!

En FreeRTOS, la cola nos da tres cosas muy valiosas:

  1. Comunicación segura entre tareas.
  2. Bloqueo eficiente, sin gastar CPU mientras esperamos.
  3. Buffer temporal, para absorber pequeñas diferencias de velocidad.

Ejemplo completo

Vamos a crear una cola de lecturas de sensor. El productor genera valores y el consumidor los imprime por el puerto serie.

struct LecturaSensor {
  uint32_t tiempo;
  float temperatura;
};

QueueHandle_t colaLecturas;

void TareaProductora(void *pvParameters) {
  for(;;) {
    LecturaSensor lectura;
    lectura.tiempo = millis();
    lectura.temperatura = leerTemperatura();

    xQueueSend(colaLecturas, &lectura, portMAX_DELAY);

    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void TareaConsumidora(void *pvParameters) {
  LecturaSensor lectura;

  for(;;) {
    if (xQueueReceive(colaLecturas, &lectura, portMAX_DELAY) == pdTRUE) {
      Serial.print("t=");
      Serial.print(lectura.tiempo);
      Serial.print(" temp=");
      Serial.println(lectura.temperatura);
    }
  }
}

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

  colaLecturas = xQueueCreate(10, sizeof(LecturaSensor));

  if (colaLecturas == NULL) {
    Serial.println("No se pudo crear la cola");
    return;
  }

  xTaskCreate(TareaProductora, "Productor", 2048, NULL, 1, NULL);
  xTaskCreate(TareaConsumidora, "Consumidor", 4096, NULL, 1, NULL);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}
Copied!

La tarea productora crea una estructura LecturaSensor y la envía por la cola. La tarea consumidora se queda bloqueada en xQueueReceive hasta que llega un dato. Mientras espera, no consume CPU.

Qué tamaño debe tener la cola

El tamaño de la cola depende de la diferencia entre la velocidad del productor y la del consumidor, y del tiempo máximo durante el que el consumidor puede quedar ocupado.

Si el productor genera datos cada 100ms y el consumidor tarda 20ms en procesarlos, una cola pequeña basta. Si el consumidor a veces se bloquea por WiFi o escritura en SD, necesitaremos más margen.

Una cola grande no arregla un consumidor demasiado lento. Solo retrasa el problema. Si la cola se llena constantemente, el sistema está produciendo más datos de los que puede procesar.

Podemos decidir qué hacer cuando la cola está llena:

  • Esperar con portMAX_DELAY.
  • Esperar un tiempo limitado.
  • Descartar el dato.
  • Sobrescribir el último valor con xQueueOverwrite() (solo en colas de longitud 1).

Si el productor usa portMAX_DELAY, la cola aplica backpressure: cuando se llena, el productor deja de mantener su periodo hasta que aparezca un hueco. Eso puede ser deseable, pero debemos decidirlo de forma explícita.

Variantes habituales

El patrón Productor-Consumidor tiene varias formas según el caso.

Un productor, un consumidor

Es el caso más simple. Una tarea genera datos y otra los procesa. Ideal para sensores, botones o adquisición de datos.

Varios productores, un consumidor

Varias tareas envían mensajes a una cola común. El consumidor centraliza el procesamiento.

Esto es muy útil para una tarea de logging:

Tarea Sensor  -> |
Tarea WiFi    -> | -> Cola Logs -> Tarea Logger
Tarea Motor   -> |
Copied!

Así evitamos que varias tareas escriban a la vez en el puerto serie, una SD o una pantalla.

Un productor, varios consumidores

Este caso requiere más cuidado. Una cola normal entrega cada mensaje a un solo consumidor. Si queremos que todos reciban el mismo dato, necesitaremos una cola por suscriptor o una arquitectura de publicación/suscripción. Un Event Group puede difundir señales representadas por bits, pero no transporta la lectura completa.