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:
- No hay que crearlas: No existe
xNotificationCreate. Ya están ahí. - No necesitan un objeto separado: El estado forma parte del TCB, aunque naturalmente ocupa espacio dentro de él.
- 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ística | Cola / Semáforo | Task Notification |
|---|---|---|
| Velocidad | Depende del objeto y del contenido | Normalmente menor coste |
| Uso de RAM | Objeto y, en una cola, buffer | Estado integrado en el TCB |
| Destinatarios | Múltiples | Solo uno |
| Capacidad | Cola FIFO configurable | Un valor, contador o máscara por entrada |
| Facilidad | API estándar | API 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() {}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
);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 (comoxTaskNotifyGive).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() {}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:
- 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.
- 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. - 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.