Una tarea en FreeRTOS es una función independiente que el Scheduler puede ejecutar, pausar y reanudar como parte de un sistema multitarea.
Ya hemos visto la teoría y sabemos cómo se integra el sistema en nuestro ESP32, así que ha llegado el momento de escribir código de verdad. Nuestro objetivo hoy es aprender a definir estas tareas y decirle al Scheduler que las ponga en marcha.
En un RTOS, una tarea es como un pequeño programa independiente que cree que tiene la CPU para él solo.
Anatomía de una tarea
Antes de llamar a ninguna función de creación, necesitamos definir qué va a hacer la tarea. En C/C++, una tarea se implementa como una función que no devuelve nada (void) y recibe un puntero vacío (void*) como parámetro.
Tiene una estructura muy característica que debemos respetar:
void TareaEjemplo(void *pvParameters) {
// 1. Inicialización de la tarea (se ejecuta una vez)
int variableLocal = 0;
// 2. Bucle infinito
for(;;) {
// Código de la tarea (lo que se repite)
Serial.println("Hola desde la tarea");
// 3. ¡IMPORTANTE! Ceder el control
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 4. Si salimos del bucle, debemos borrar la tarea
vTaskDelete(NULL);
}Una tarea nunca debe hacer return. Llegar al final incumple el contrato de la API y el port puede abortar la aplicación. Si quieres que la tarea termine, elimínala explícitamente con vTaskDelete(NULL).
La función xTaskCreate
Para instanciar esa función como una tarea real dentro del sistema operativo, usamos la API xTaskCreate. Es una de las funciones más importantes que vas a ver en este curso.
Su firma puede asustar al principio por la cantidad de parámetros, pero vamos a desgranarlos uno a uno:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // Función que implementa la tarea
const char * const pcName, // Nombre descriptivo (para depuración)
configSTACK_DEPTH_TYPE usStackDepth, // Tamaño del Stack
void *pvParameters, // Parámetros para la tarea
UBaseType_t uxPriority, // Prioridad
TaskHandle_t *pxCreatedTask // Handle (manejador) para controlarla
);pvTaskCode
Es simplemente el nombre de la función que hemos creado arriba. Si tu función se llama TareaParpadear, aquí pones TareaParpadear.
pcName
Un nombre en texto plano, como "Blink LED". FreeRTOS no lo usa para nada funcional, es solo para que nosotros, los humanos, podamos identificar la tarea al depurar.
usStackDepth (ojo aquí)
Es la cantidad de memoria que reservamos para la pila (variables locales, llamadas a funciones, etc.) de esta tarea.
- En FreeRTOS estándar (Vanilla): Se especifica en Words (palabras). En un sistema de 32 bits, 1 word = 4 bytes.
- En ESP32 (ESP-IDF/Arduino): Se especifica en BYTES.
Para el ESP32, un valor seguro para empezar suele ser 2048 (bytes). Si ponemos muy poco, tendremos un Stack Overflow y el micro se reiniciará. En el próximo artículo profundizaremos en cómo calcular esto bien.
pvParameters
Un puntero void* para pasar argumentos a la tarea al crearla. Si no necesitamos pasar nada, ponemos NULL. Es muy útil para reutilizar la misma función de código para múltiples tareas (ej. la misma función blink para 3 LEDs distintos).
uxPriority
Un número entero que define la importancia.
- En FreeRTOS, a mayor número, mayor prioridad.
- La tarea
loopTaskde Arduino suele correr a prioridad 1.
pxCreatedTask
Un puntero para guardar el “ID” (Handle) de la tarea. Nos sirve si luego queremos pausarla, cambiar su prioridad o borrarla desde otra tarea. Si no lo necesitamos, ponemos NULL.
ESP32: xTaskCreatePinnedToCore
En un ESP32 de dos núcleos, xTaskCreate crea una tarea sin afinidad. El Scheduler decide en qué núcleo puede ejecutarse y puede migrarla entre distintas activaciones.
Si queremos forzar que una tarea corra siempre en un núcleo específico (por ejemplo, para dejar el Core 0 libre para WiFi), usamos la variante específica de Espressif:
xTaskCreatePinnedToCore(
Function, Name, Stack, Params, Priority, Handle,
xCoreID // <--- Nuevo parámetro extra
);El parámetro xCoreID puede ser:
0: Protocol CPU (WiFi/BT/System).1: App CPU (Donde corre tu código normalmente).tskNO_AFFINITY: Deja que el Scheduler elija (igual quexTaskCreate).
Ejemplo completo: multitarea real
Vamos a ver un ejemplo donde creamos dos tareas que corren en paralelo a velocidades distintas, independientes del loop().
// Handle para controlar las tareas (opcional, por si quisiéramos borrarlas luego)
TaskHandle_t Tarea1_Handle = NULL;
TaskHandle_t Tarea2_Handle = NULL;
// --- Definición de las Tareas ---
// Tarea 1: Imprime rápido
void TareaImprimirRapido(void *parameter) {
for(;;) { // Bucle infinito
Serial.println("Tarea Rápida: Ejecutando en Core " + String(xPortGetCoreID()));
// Bloqueamos la tarea 500ms
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// Tarea 2: Imprime lento
void TareaImprimirLento(void *parameter) {
for(;;) {
Serial.println("--- Tarea Lenta: Ejecutando en Core " + String(xPortGetCoreID()));
// Bloqueamos la tarea 2000ms
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// --- Setup ---
void setup() {
Serial.begin(115200);
delay(1000); // Esperar a que el monitor serie arranque
Serial.println("Iniciando FreeRTOS Demo...");
// Creamos la Tarea 1 en el Core 1
xTaskCreatePinnedToCore(
TareaImprimirRapido, // Función
"TareaRapida", // Nombre
2048, // Stack size (bytes en ESP32)
NULL, // Parámetros
1, // Prioridad
&Tarea1_Handle, // Handle
1 // Core ID
);
// Creamos la Tarea 2 en el Core 0 (por variar)
xTaskCreatePinnedToCore(
TareaImprimirLento,
"TareaLenta",
2048,
NULL,
1,
&Tarea2_Handle,
0
);
Serial.println("Tareas creadas. El loop() sigue su vida...");
}
void loop() {
// El loop también es una tarea.
// Podemos dejarlo vacío o usarlo para cosas de baja prioridad.
delay(10000);
}¿Qué está pasando aquí?
- En el
setup, llamamos axTaskCreatePinnedToCoredos veces. - Inmediatamente, el Scheduler detecta que hay nuevas tareas listas.
- La
TareaRapidaescribirá por el puerto serie cada medio segundo. - La
TareaLentaaparecerá cada 2 segundos. - Mientras tanto, el
loop()original sigue existiendo, pero ahora es solo un actor más en la película, no el director.
Fíjate en vTaskDelay(pdMS_TO_TICKS(500)). No usamos delay().
vTaskDelay pone la tarea actual en estado Blocked, dejando que el núcleo ejecute otras tareas o la tarea Idle. Si usáramos un bucle while para esperar, estaríamos acaparando CPU inútilmente.
Errores comunes al empezar
- Olvidar el bucle infinito: Si escribes una tarea como una función normal que acaba (
}), la tarea termina, retorna y el sistema falla. - No ceder CPU (Starvation): Si creas una tarea de alta prioridad con un
while(1)que nunca se bloquea, puede dejar sin CPU a las tareas menos prioritarias del mismo núcleo, incluida Idle. El Task Watchdog puede detectar esta situación y provocar un reinicio, según su configuración. - Stack Overflow: Definir un
usStackDepthdemasiado pequeño. Si ves errores raros o “Guru Meditation Error”, prueba a aumentar la pila.