freertos-task-notifications-notificaciones

Task Notifications en FreeRTOS

  • 5 min

Una Task Notification es un canal ligero de aviso asociado directamente a una tarea, pensado para sincronizar o enviar valores simples sin crear objetos extra.

Ya hemos usado colas para pasar datos. FreeRTOS también ofrece semáforos y grupos de eventos para sincronizar tareas, pero todos son objetos independientes.

También tienen un “precio”: son objetos independientes que hay que crear, consumen RAM del Heap y requieren más gestión por parte del Kernel.

¿Qué pasa si solo quiero avisar a una tarea de que “ha ocurrido algo”? ¿O enviarle un número? ¿Realmente necesito crear una cola completa para eso?

La respuesta es NO. FreeRTOS tiene una herramienta pensada justo para ese caso: las Task Notifications (Notificaciones a Tareas).

¿Qué son las task notifications?

A diferencia de las colas o semáforos, que son objetos externos a las tareas (una cola existe aunque nadie la use), una Notificación pertenece directamente a una Tarea.

Cada tarea tiene en su TCB uno o más valores de notificación de 32 bits, según configTASK_NOTIFICATION_ARRAY_ENTRIES. La configuración tradicional usa una sola entrada.

Esto significa que:

  1. No hay que crearlas: No existe xNotificationCreate. Ya están ahí.
  2. No necesitan un objeto separado: El estado forma parte del TCB, aunque naturalmente ocupa espacio dentro de él.
  3. Tienen poco coste: En muchos casos necesitan menos RAM y procesamiento que una cola o un semáforo equivalente.

Comparación con una cola o un semáforo

CaracterísticaCola / SemáforoTask Notification
VelocidadDepende del objeto y del contenidoNormalmente menor coste
Uso de RAMObjeto y, en una cola, bufferEstado integrado en el TCB
DestinatariosMúltiplesSolo uno
CapacidadCola FIFO configurableUn valor, contador o máscara por entrada
FacilidadAPI estándarAPI específica

¿Cómo funcionan?

El concepto es enviar un evento directamente a una tarea específica, sabiendo su Handle.

Cuando enviamos una notificación a una tarea, podemos manipular esos 32 bits internos de varias formas, lo que permite que las Notificaciones actúen como:

  • Semáforo Binario: “Despierta, ha pasado algo”.
  • Semáforo Contador: “Han pasado 5 cosas”.
  • Buzón de valor (Mailbox): “Toma este valor de 32 bits”.
  • Grupo de Eventos: “Ha ocurrido el evento A (bit 0) y el C (bit 2)”.

Como semáforo (Give / Take)

El uso más común es como un “toque” para despertar a una tarea. Es ideal para sincronizar una tarea con una Interrupción (ISR) o con otra tarea.

Usamos las macros simplificadas: xTaskNotifyGive (dar) y ulTaskNotifyTake (recibir).

Ejemplo: Tarea esperando un botón

// Variable global para el Handle de la tarea que debe despertar
TaskHandle_t hTareaLED = NULL;

void TareaLED(void *pvParameters) {
    for(;;) {
        // 1. Esperar notificación.
        // pdTRUE = limpia el contador a 0 al salir.
        // portMAX_DELAY = Espera infinita
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // Si pasamos aquí, es que alguien nos ha avisado
        Serial.println("¡Recibido! Parpadeando LED.");
        digitalWrite(LED_BUILTIN, HIGH);
        vTaskDelay(pdMS_TO_TICKS(200));
        digitalWrite(LED_BUILTIN, LOW);
    }
}

void TareaBoton(void *pvParameters) {
    pinMode(0, INPUT_PULLUP); // Botón BOOT del ESP32
    
    for(;;) {
        if(digitalRead(0) == LOW) {
            // Debounce cutre
            vTaskDelay(pdMS_TO_TICKS(50));
            
            if(digitalRead(0) == LOW) {
                Serial.println("Botón pulsado. Enviando notificación...");
                
                // 2. Enviamos la notificación DIRECTAMENTE a la tarea
                xTaskNotifyGive(hTareaLED);
                
                // Esperar a que suelte
                while(digitalRead(0) == LOW) {
                    vTaskDelay(pdMS_TO_TICKS(10));
                }
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_BUILTIN, OUTPUT);

    // Creamos la tarea y guardamos su Handle (CRÍTICO)
    xTaskCreate(TareaLED, "LED", 2048, NULL, 1, &hTareaLED);
    xTaskCreate(TareaBoton, "Btn", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

Fíjate en la simplicidad. No hemos creado ningún objeto semáforo en setup(). Usamos el Handle de la tarea para “tocarle el hombro”.

Enviar valores con xTaskNotify

También podemos usar esos 32 bits para enviar datos. Por ejemplo, enviar un comando o un estado.

La función genérica es:

BaseType_t xTaskNotify( 
    TaskHandle_t xTaskToNotify, // A quién
    uint32_t ulValue,           // El valor (dato o máscara de bits)
    eNotifyAction eAction       // QUÉ hacer con ese valor
);
Copied!

El parámetro eAction es la clave. Define cómo modificamos los 32 bits de la tarea destino:

  • eSetBits: Hace un OR lógico (útil para flags).
  • eIncrement: Incrementa el contador (como xTaskNotifyGive).
  • eSetValueWithOverwrite: Sobrescribe el valor anterior con el nuevo.
  • eSetValueWithoutOverwrite: Escribe solo si el valor anterior ya fue leído.

Ejemplo: Enviando comandos

TaskHandle_t hMotor = NULL;

// Definimos comandos
#define CMD_PARAR    0x00
#define CMD_AVANZAR  0x01
#define CMD_RETROCEDER 0x02

void TareaMotor(void *param) {
    uint32_t comandoRecibido;
    
    for(;;) {
        // Esperamos indefinidamente a que llegue una notificación
        // Guardamos el valor recibido en 'comandoRecibido'
        xTaskNotifyWait(
            0x00,             // Bits a limpiar al entrar (ninguno)
            0xFFFFFFFF,       // Bits a limpiar al salir (todos -> reset)
            &comandoRecibido, // Dónde guardar el valor
            portMAX_DELAY     // Tiempo de espera
        );
        
        switch(comandoRecibido) {
            case CMD_AVANZAR: Serial.println("Motor: AVANZANDO"); break;
            case CMD_RETROCEDER: Serial.println("Motor: ATRAS"); break;
            case CMD_PARAR: Serial.println("Motor: STOP"); break;
        }
    }
}

void setup() {
    Serial.begin(115200);
    xTaskCreate(TareaMotor, "Motor", 2048, NULL, 1, &hMotor);
    
    vTaskDelay(pdMS_TO_TICKS(1000));
    
    // Enviamos comandos sobrescribiendo el valor
    xTaskNotify(hMotor, CMD_AVANZAR, eSetValueWithOverwrite);
    vTaskDelay(pdMS_TO_TICKS(2000));
    xTaskNotify(hMotor, CMD_PARAR, eSetValueWithOverwrite);
}

void loop() {}
Copied!

Cuándo no usarlas

Aunque son maravillosas, las Task Notifications no pueden reemplazar a las Colas y Semáforos en el 100% de los casos. Tienen limitaciones importantes:

  1. Cada notificación pertenece a una tarea: Puede haber varios emisores, pero solo su tarea propietaria espera esa entrada. Si necesitas despertar a varias tareas con los mismos bits, valora un Event Group.
  2. No almacenan histórico: Si envías 5 valores seguidos (eSetValueWithOverwrite) antes de que la tarea receptora los lea, solo leerá el último. Los 4 anteriores se pierden. Si necesitas un buffer (FIFO) donde no se pierda nada, usa una Cola.
  3. Cada entrada solo contiene 32 bits: No puedes enviar una estructura completa. Enviar un puntero es posible, pero vuelve a introducir el problema de la propiedad y la vida del buffer.