zephyr-comunicacion-message-queues

Comunicación entre hilos: message queues y mailboxes

  • 4 min

Message queue es una cola FIFO que copia elementos de tamaño fijo entre productores y consumidores de forma segura frente a concurrencia.

Supongamos que un hilo lee un sensor de temperatura y otro muestra el valor en una pantalla. El primero necesita pasar un float o una estructura al segundo.

Si usamos una variable global protegida con Mutex, funciona, pero es poco elegante y propenso a errores si la frecuencia de datos es alta. Zephyr nos ofrece mecanismos mucho más potentes diseñados específicamente para mover datos: las Message Queues (Colas de Mensajes) y los Mailboxes.

Message queues (k_msgq)

Las Colas de Mensajes son, con diferencia, el mecanismo más utilizado. Funcionan como una tubería FIFO (First-In, First-Out).

  • Productor: Mete un dato por un lado (put).
  • Consumidor: Saca el dato por el otro (get).
  • Buffer: Si el consumidor está ocupado, los datos se quedan en la cola esperando (hasta que se llene).

Lo más importante de las Message Queues en Zephyr es que copian los datos. Cuando metes una variable en la cola, el sistema hace una copia exacta de su valor en el buffer interno. Esto significa que puedes reutilizar o destruir tu variable local inmediatamente después de enviarla sin miedo.

Implementación práctica

Vamos a pasar una estructura de datos completa de un hilo a otro.

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(demo_msgq, LOG_LEVEL_INF);

/* 1. Definimos el tipo de dato que vamos a pasar */
struct data_item_t {
    uint32_t id;
    float temperatura;
    float humedad;
};

/* 2. Definimos la Cola estáticamente */
/* K_MSGQ_DEFINE(nombre, tamaño_item, capacidad_maxima, alineacion) */
/* Aquí creamos una cola capaz de guardar hasta 10 lecturas */
K_MSGQ_DEFINE(my_msgq, sizeof(struct data_item_t), 10, 1);

/* Hilo Productor (Simula un sensor) */
void producer_thread(void *p1, void *p2, void *p3)
{
    struct data_item_t data;
    data.id = 0;

    while (1) {
        /* Simulamos lectura */
        data.id++;
        data.temperatura = 20.0f + (data.id % 5); // Valor inventado
        data.humedad = 50.0f;

        /* 3. Enviamos el dato a la cola */
        /* Si la cola está llena, K_NO_WAIT hace que falle inmediatamente.
           Podríamos usar K_FOREVER para esperar a que haya hueco. */
        if (k_msgq_put(&my_msgq, &data, K_NO_WAIT) != 0) {
            LOG_WRN("¡Cola llena! Perdiendo datos...");
        } else {
            LOG_INF("Enviado dato ID %u", data.id);
        }

        k_msleep(1000);
    }
}

/* Hilo Consumidor (Procesa o muestra los datos) */
void consumer_thread(void *p1, void *p2, void *p3)
{
    struct data_item_t received_data;

    while (1) {
        /* 4. Leemos de la cola */
        /* K_FOREVER: El hilo se duerme hasta que llegue algo. */
        k_msgq_get(&my_msgq, &received_data, K_FOREVER);

        LOG_INF("Recibido -> Temp: %.1f, Hum: %.1f", 
                received_data.temperatura, received_data.humedad);
    }
}

K_THREAD_DEFINE(producer_id, 1024, producer_thread, NULL, NULL, NULL, 7, 0, 0);
K_THREAD_DEFINE(consumer_id, 1024, consumer_thread, NULL, NULL, NULL, 7, 0, 0);
Copied!

El uso de k_msgq_get con K_FOREVER es un patrón de diseño excelente. El hilo consumidor no consume CPU mientras espera datos. El sistema se encarga de despertarlo en el instante exacto en que el productor hace el put.

Cada elemento se entrega a un solo consumidor. Si varios hilos esperan en la misma cola, se reparten los mensajes; no reciben una copia cada uno.

Mailboxes (k_mbox)

Si las Message Queues son tan buenas, ¿para qué queremos Mailboxes?

Las colas tienen una limitación: el tamaño del mensaje es fijo. Si a veces quieres enviar un byte y otras veces una imagen de 1KB, la cola es ineficiente (tendrías que dimensionarla para el caso más grande).

Los Mailboxes son más flexibles y permiten:

  1. Enviar mensajes de tamaño variable.
  2. Intercambio síncrono: El emisor puede bloquearse hasta que el receptor realmente haya recibido el mensaje (handshake).
  3. Filtrado por ID: Puedes enviar mensajes a “canales” específicos dentro del mismo Mailbox.

Los mailboxes ofrecen más control, a cambio de una API y un ciclo de vida del mensaje más complejos. Además, solo permiten intercambios entre hilos, no desde una ISR.

Empieza por una message queue cuando los mensajes sean pequeños y de tamaño fijo. Para mensajes grandes, copiar una estructura completa aumenta la latencia; puede ser preferible intercambiar punteros con una política clara de propiedad o utilizar otro objeto del kernel.

Limpiando la cola (k_msgq_purge)

A veces, la cola se llena de datos viejos que ya no sirven (por ejemplo, si el hilo consumidor se ha quedado colgado un rato). Podemos vaciarla instantáneamente con:

k_msgq_purge(&my_msgq);
Copied!

Esto descarta todos los elementos y resetea los contadores, permitiendo empezar de cero.

En el siguiente artículo veremos cómo delegar trabajo desde interrupciones mediante workqueues.