freertos-estados-tarea-ciclo-vida

Estados de una tarea en FreeRTOS

  • 6 min

Los estados de una tarea son las situaciones internas por las que pasa una tarea dentro del Scheduler mientras espera, se ejecuta, se bloquea o queda suspendida.

En el artículo anterior vimos cómo las prioridades deciden qué tarea se ejecuta. Pero para entender realmente cómo funciona FreeRTOS, tenemos que mirar “bajo el capó” y ver en qué estado puede estar cada tarea.

Una tarea no es algo binario que simplemente “funciona” o “no funciona”. A lo largo de su vida, pasa por una máquina de estados gestionada por el Scheduler. Entender este flujo es la diferencia entre un firmware que funciona de milagro y uno robusto.

Hoy vamos a ver los cuatro estados fundamentales de una tarea en FreeRTOS.

La máquina de estados

Imagina una sala de espera de un médico (el procesador).

Running (Ejecutando): El paciente que está dentro de la consulta con el médico.

Ready (Listo): Los pacientes en la sala de espera que podrían entrar ya, pero están esperando su turno (porque hay alguien más importante o llegaron más tarde).

Blocked (Bloqueado): Pacientes que no están en la sala de espera porque están esperando resultados de una analítica. No tiene sentido que ocupen una silla hasta que lleguen los resultados.

Suspended (Suspendido): Pacientes a los que hemos mandado a casa indefinidamente hasta que les llamemos.

Una tarea suele alternar entre Ready y Running. Cuando espera tiempo o un evento pasa a Blocked; cuando termina la espera, vuelve a Ready.

Estado Running (ejecutando)

Es el estado más simple: La tarea está usando la CPU en este preciso instante.

  • En un sistema de un solo núcleo (Single Core), solo puede haber una tarea en Running a la vez.
  • En un ESP32 de dos núcleos puede haber un máximo de dos, una por núcleo.

Cuando una tarea está en Running, ejecuta sus instrucciones secuencialmente hasta que:

  • El Scheduler la corta (se le acaba su tiempo o llega alguien con más prioridad).
  • Ella misma decide esperar (vTaskDelay).

Estado Ready (listo)

Aquí están las tareas que quieren trabajar pero no pueden porque la CPU está ocupada por otra tarea de igual o mayor prioridad.

  • Están “despiertas” y listas para entrar en acción.
  • El Scheduler revisa esta lista constantemente. En cuanto la CPU queda libre, elige a la tarea Ready con mayor prioridad y la pasa a Running.

Estado Blocked (bloqueado)

Este es, paradójicamente, el estado más importante en un RTOS.

Una tarea entra en estado Blocked cuando está esperando a que ocurra un evento temporal o externo.

  • Temporal: “Espérate 100ms” (vTaskDelay).
  • Evento: “Espera a que llegue un dato a una cola” o “Espera a que se libere este semáforo”.

Lo importante: Cuando una tarea está Blocked, no consume ciclos de CPU. El Scheduler la saca de la lista de tareas revisables. Es como si no existiera. Esto permite que la CPU se dedique a otras tareas o, si todas están bloqueadas, entre en modo de bajo consumo (IDLE).

Si usáramos un bucle de consulta constante (polling), estaríamos gastando CPU en estado Running. Con una cola, un semáforo o una notificación podemos pedir al sistema: “despiértame cuando ocurra el evento” y pasar a Blocked.

Estado Suspended (suspendido)

Es un estado de “hibernación profunda”. Una tarea en este estado no está esperando nada (ni tiempo ni eventos). Simplemente está “congelada” porque nosotros lo hemos ordenado explícitamente.

  • Solo se entra llamando a vTaskSuspend().
  • Solo se sale llamando a vTaskResume().

Es útil si queremos desactivar temporalmente una funcionalidad del dispositivo (ej. apagar la gestión de sensores porque tenemos poca batería) sin destruir la tarea y perder su estado.

Transiciones durante el ciclo de vida

Veamos cómo se mueve una tarea por estos estados con un ejemplo de código real.

TaskHandle_t hTareaLED = NULL;

void TareaLED(void *pvParameters) {
    // 1. Al crearse, la tarea nace en estado READY.
    // Si tiene prioridad suficiente, el Scheduler la pasa a RUNNING.
    
    for(;;) {
        // --- ESTADO: RUNNING ---
        digitalWrite(LED_BUILTIN, HIGH);
        Serial.println("LED ON");

        // Ahora queremos esperar 1 segundo.
        // Llamamos a vTaskDelay.
        
        // --- Transición: RUNNING -> BLOCKED ---
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        // Durante este segundo, la tarea "desaparece" de la CPU.
        // Otras tareas pueden ejecutarse.
        
        // Al pasar 1000ms, el sistema lanza un evento interno.
        // --- Transición: BLOCKED -> READY ---
        
        // Si somos la tarea con más prioridad:
        // --- Transición: READY -> RUNNING ---
        
        digitalWrite(LED_BUILTIN, LOW);
        Serial.println("LED OFF");
        
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
Copied!

El caso de vTaskSuspend

Vamos a ver cómo congelar una tarea desde otra.

void TareaControl(void *param) {
    for(;;) {
        if(digitalRead(BOTON_PAUSA) == LOW) {
            // El usuario pulsa el botón
            Serial.println("Pausando la tarea del LED...");
            
            // --- Transición: (Cualquiera) -> SUSPENDED ---
            vTaskSuspend(hTareaLED); 
        } 
        else {
            // --- Transición: SUSPENDED -> READY ---
            vTaskResume(hTareaLED);
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}
Copied!

Cuidado con vTaskSuspend. Si suspendes una tarea que tenía “cogido” un recurso compartido (como un Mutex), ese recurso quedará bloqueado indefinidamente y nadie más podrá usarlo. Generalmente es mejor usar mecanismos de sincronización que suspensiones brutas.

La tarea Idle: ¿qué pasa cuando todos duermen?

Si diseñamos bien nuestro sistema, la mayor parte del tiempo nuestras tareas deberían estar en estado Blocked esperando cosas. ¿Qué hace el procesador si todas las tareas están bloqueadas a la vez?

FreeRTOS crea automáticamente una tarea al arrancar llamada IDLE Task.

  • Tiene prioridad 0 (la más baja posible).
  • Siempre está en estado Ready (o Running si nadie más quiere CPU).
  • Proporciona un contexto ejecutable cuando no hay otro trabajo y realiza tareas internas de mantenimiento.

Aunque parece inútil, es importante:

  1. Mantiene al Scheduler en un estado válido y permite que el port ponga la CPU en bajo consumo cuando está configurado.
  2. Se encarga de limpiar la memoria de las tareas que han sido borradas con vTaskDelete.
  3. Permite ejecutar “Hooks” (ganchos) para poner el microcontrolador en modo Sleep y ahorrar batería.