freertos-colas-queues-basico

Colas en FreeRTOS: crear, enviar y recibir datos

  • 6 min

Una cola en FreeRTOS es un buffer seguro que permite enviar datos entre tareas sin compartir variables globales a lo loco.

Iniciamos un nuevo bloque en el curso. Hasta ahora, nuestras tareas eran islas solitarias; vivían, trabajaban y morían sin saber nada de sus vecinas. En un sistema real, las tareas necesitan colaborar.

Piensa en una tarea que lee un sensor de temperatura cada 100 ms y otra que debe mostrar el valor en una pantalla OLED o enviarlo por WiFi. ¿Cómo pasamos el dato de una a otra?

La tentación del principiante es usar una variable global.

// ¡PELIGRO! No hagáis esto en casa
int temperaturaGlobal; 

void TareaSensor() { temperaturaGlobal = leerSensor(); }
void TareaPantalla() { display.print(temperaturaGlobal); }
Copied!

Esto es una mala práctica terrible. Provoca condiciones de carrera (Race Conditions), corrupción de datos (si uno escribe mientras el otro lee la mitad del dato) y hace el código difícil de depurar.

Para solucionar esto de forma elegante y segura (Thread Safe), FreeRTOS nos regala su mecanismo de comunicación más robusto: las Colas (Queues).

¿Qué es una cola (Queue)?

Una cola es, conceptualmente, una tubería o cinta transportadora de datos. Funciona bajo el principio FIFO (First In, First Out): el primer dato que entra es el primero que sale.

Tiene dos características fundamentales que la hacen perfecta para RTOS:

  1. Protección de concurrencia: FreeRTOS gestiona internamente los bloqueos. No necesitas preocuparte de si dos tareas intentan escribir a la vez; el sistema pone orden.
  2. Capacidad de Bloqueo: Si una tarea intenta leer de una cola vacía, puede dormirse (entrar en Blocked) automáticamente hasta que llegue un dato. ¡Adiós al Polling ineficiente!

Creación de una cola

Antes de usarla, debemos crearla y definir sus dimensiones. Las colas en FreeRTOS tienen un tamaño fijo preasignado en la RAM.

Usamos la función xQueueCreate:

QueueHandle_t xQueueCreate( 
    UBaseType_t uxQueueLength, // Nº máximo de elementos que caben
    UBaseType_t uxItemSize     // Tamaño en bytes de CADA elemento
);
Copied!

Por ejemplo, para crear una cola que pueda guardar hasta 10 números enteros:

QueueHandle_t colaEnteros;

void setup() {
    // Cola de 10 huecos, cada hueco del tamaño de un int
    colaEnteros = xQueueCreate(10, sizeof(int));

    if(colaEnteros == NULL) {
        Serial.println("Error creando la cola (falta RAM)");
    }
}
Copied!

Es importante comprobar si devuelve NULL. Las colas consumen RAM del Heap, y si creas una cola muy grande, podrías quedarte sin memoria.

Enviando datos: xQueueSend

Para meter un dato en la cola, usamos xQueueSend.

BaseType_t xQueueSend(
    QueueHandle_t xQueue,       // El handle de la cola
    const void * pvItemToQueue, // Puntero al dato que queremos copiar
    TickType_t xTicksToWait     // Tiempo de espera si la cola está LLENA
);
Copied!

El concepto de “Copia por Valor”

Esto es crítico: FreeRTOS copia el dato dentro de la cola. No guarda una referencia a tu variable, sino que hace un memcpy del contenido.

  • Si envías un int, copia los 4 bytes del entero.
  • Esto es genial porque puedes usar variables locales en la función emisora, y aunque esa variable desaparezca al terminar la función, el dato ya está seguro en la cola.

El Tiempo de Espera (Timeout)

El tercer parámetro define qué hacer si la cola está llena (no caben más datos):

  • 0: Intenta escribir y si no puede, devuelve error inmediatamente.
  • pdMS_TO_TICKS(100): Espera 100ms a ver si se libera un hueco. Si no, desiste.
  • portMAX_DELAY: Espera sin límite cuando INCLUDE_vTaskSuspend está habilitado, como ocurre en la configuración habitual de ESP32.
int medicion = 25;
// Enviar copiando el valor de 'medicion'. Espera 0 ticks si está llena.
xQueueSend(colaEnteros, &medicion, 0); 
Copied!

Recibiendo datos: xQueueReceive

Para sacar datos, usamos xQueueReceive. Esta función extrae el dato de la cola (lo borra de ella) y lo copia en nuestro buffer local.

BaseType_t xQueueReceive(
    QueueHandle_t xQueue,   // El handle
    void *pvBuffer,         // Dónde guardar el dato recibido
    TickType_t xTicksToWait // Tiempo de espera si la cola está VACÍA
);
Copied!

Aquí xTicksToWait marca cuánto estamos dispuestos a esperar. Normalmente querremos poner portMAX_DELAY para que la tarea consumidora no gaste CPU si no hay datos. Se despertará sola en cuanto llegue algo.

Ejemplo práctico: el contador

Vamos a implementar el patrón clásico Productor-Consumidor.

  • Productor: Genera números incrementales y los envía.
  • Consumidor: Espera números y los imprime.
// 1. Definimos el Handle global
QueueHandle_t colaSimple;

void TareaProductor(void *pvParameters) {
  int contador = 0;
  for(;;) {
    // Incrementamos
    contador++;
    
    Serial.printf("Productor: Enviando %d\n", contador);
    
    // Enviamos a la cola.
    // Usamos &contador porque pide un puntero al dato, 
    // pero recuerda: copia el VALOR de contador dentro de la cola.
    xQueueSend(colaSimple, &contador, portMAX_DELAY);
    
    vTaskDelay(pdMS_TO_TICKS(1000)); // Esperamos 1 seg
  }
}

void TareaConsumidor(void *pvParameters) {
  int datoRecibido = 0;
  for(;;) {
    // Esta línea BLOQUEA la tarea hasta que llegue algo.
    // No consume CPU mientras espera.
    if(xQueueReceive(colaSimple, &datoRecibido, portMAX_DELAY) == pdTRUE) {
      // Si llegamos aquí, es que hemos recibido algo
      Serial.printf("--- Consumidor: Recibido %d \n", datoRecibido);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // 2. Creamos la cola antes de las tareas
  colaSimple = xQueueCreate(10, sizeof(int));
  
  if(colaSimple == NULL) {
    Serial.println("Error creando cola");
    for(;;) {
      delay(1000);
    }
  }

  // 3. Creamos las tareas
  xTaskCreate(TareaProductor, "Productor", 2048, NULL, 1, NULL);
  xTaskCreate(TareaConsumidor, "Consumidor", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

¿Qué ocurre si el productor es más rápido?

Si cambiamos el delay del Productor a 100ms y el Consumidor tarda 500ms en procesar:

  1. La cola se irá llenando (1, 2, 3…).
  2. Al llegar a 10 elementos (el tamaño que definimos), la cola se llena.
  3. Como pusimos portMAX_DELAY en el xQueueSend del Productor, el Productor se bloqueará al intentar meter el 11.
  4. El sistema se autorregula. El Productor se ve forzado a bajar su ritmo al del Consumidor.

Tipos de datos soportados

En este ejemplo hemos usado int. Pero xQueueCreate acepta cualquier tamaño fijo:

  • sizeof(byte)
  • sizeof(float)
  • sizeof(bool)

¿Y si queremos enviar una estructura compleja (struct) con varios datos (temperatura, humedad, id_sensor)? ¿O un String largo?

Aquí la cosa cambia. Como FreeRTOS hace “copia por valor”, copiar estructuras gigantes es ineficiente y lento.

No envíes un String, un std::string ni otro objeto con recursos internos como si fuera una estructura plana. La cola hace una copia byte a byte, no llama a constructores ni gestiona la propiedad de esa memoria.