freertos-multitarea-cambio-contexto

Multitarea en FreeRTOS: planificación y cambio de contexto

  • 5 min

La multitarea en un RTOS es la capacidad de repartir la CPU entre varias tareas independientes, de forma que el sistema pueda atender varias partes del programa sin quedar bloqueado en una sola.

Esto no significa necesariamente que todas las tareas se ejecuten físicamente al mismo tiempo. En un microcontrolador de un solo núcleo, lo que tenemos es una alternancia rapidísima entre tareas. Tan rápida que, para nosotros, parece que todo ocurre a la vez.

En un ESP32 clásico tenemos dos núcleos, así que además de multitarea podemos tener cierto paralelismo real. Otros modelos de la familia, como ESP32-C3, tienen un solo núcleo. Aun así, conviene entender primero la multitarea en un núcleo, porque es la base de todo lo demás.

Por qué necesitamos multitarea

En un programa secuencial, una función empieza, termina, y luego empieza la siguiente. Esto es sencillo, pero tiene un problema evidente: si una parte tarda mucho, todo lo demás espera.

Por ejemplo, podemos tener un sistema que necesita:

  • Leer un sensor cada 100ms.
  • Actualizar una pantalla cada 500ms.
  • Atender una comunicación serie.
  • Vigilar un botón de emergencia.

Con un loop() clásico, terminamos escribiendo una mezcla de millis(), flags, estados y comprobaciones por todas partes. Funciona, sí. Pero en cuanto el programa crece, la lógica empieza a enredarse.

Con FreeRTOS podemos separar el programa en tareas:

void TareaSensor(void *pvParameters) {
  for(;;) {
    leerSensor();
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void TareaPantalla(void *pvParameters) {
  for(;;) {
    actualizarPantalla();
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}
Copied!

Cada tarea tiene su propio bucle, su propio ritmo y su propia responsabilidad. El Scheduler se encarga de decidir cuál se ejecuta en cada momento.

Multitarea cooperativa

En la multitarea cooperativa, una tarea conserva la CPU hasta que decide cederla voluntariamente. Es decir, la tarea dice algo como “ya he terminado por ahora, que pase la siguiente”.

Esto puede ocurrir cuando llama a funciones como:

  • vTaskDelay()
  • taskYIELD()
  • una espera bloqueante sobre una cola, semáforo o notificación

El enfoque cooperativo es sencillo y predecible, pero tiene un problema bastante serio: depende de que todas las tareas se porten bien.

Si una tarea se queda en un bucle infinito sin bloquearse ni ceder CPU, las demás no reciben tiempo de ejecución. Y claro, nuestro firmware pasa de “multitarea elegante” a “un señor acaparando el micrófono en una reunión”.

void TareaMaleducada(void *pvParameters) {
  for(;;) {
    // Nunca bloquea, nunca espera, nunca cede
    hacerCalculos();
  }
}
Copied!

Esta tarea puede impedir que otras tareas de igual o menor prioridad se ejecuten correctamente. Por eso, en un RTOS real, ceder CPU no es un detalle decorativo.

Multitarea apropiativa

FreeRTOS se usa normalmente en modo multitarea apropiativa (preemptive multitasking). Esto significa que el Scheduler puede interrumpir una tarea aunque esta no haya cedido voluntariamente la CPU.

El Scheduler mira las tareas que están listas para ejecutarse y elige la de mayor prioridad. Si aparece una tarea más prioritaria, la tarea actual se pausa y la nueva entra a ejecutarse.

// Pseudocódigo conceptual
si (hay_tarea_mas_prioritaria_lista) {
  guardar_contexto_tarea_actual();
  cargar_contexto_tarea_prioritaria();
  ejecutar_tarea_prioritaria();
}
Copied!

Esto es lo que permite que una tarea crítica responda rápido, aunque otra tarea estuviera haciendo trabajo menos importante.

Por ejemplo:

  1. TareaPantalla está dibujando gráficos.
  2. Llega un dato urgente de un sensor.
  3. Se desbloquea TareaControl, que tiene más prioridad.
  4. FreeRTOS pausa TareaPantalla y ejecuta TareaControl.
  5. Cuando procede, TareaPantalla continúa donde estaba.

Ahí está la gracia. No hemos tenido que llenar nuestro código de comprobaciones manuales. El reparto lo hace el RTOS.

Qué es un cambio de contexto

Un cambio de contexto es la operación de guardar el estado de una tarea y restaurar el de otra para que la CPU pueda continuar ejecutando una tarea diferente.

Cuando FreeRTOS cambia de tarea, necesita guardar información como:

  • Registros del procesador.
  • Contador de programa.
  • Puntero de pila (stack pointer).
  • Estado interno necesario para retomar la tarea.

Luego carga el estado de otra tarea, y la CPU continúa como si nada hubiera pasado.

Para la tarea, es como si el tiempo se hubiera congelado. La pausaron en una instrucción concreta y, cuando vuelve a ejecutarse, continúa exactamente desde ahí.

Este proceso tiene un coste. Pequeño, pero existe. Por eso no conviene crear tareas para absolutamente todo, ni forzar cambios de contexto innecesarios cada dos microsegundos.

El tick del sistema

FreeRTOS usa normalmente una interrupción periódica llamada tick. Es como un metrónomo interno que permite al Kernel medir el paso del tiempo.

Cada tick, el sistema puede comprobar:

  • Si una tarea bloqueada por vTaskDelay() ya debe despertarse.
  • Si hay tareas de igual prioridad que deben turnarse.
  • Si alguna condición temporal ha cambiado.

La frecuencia del tick depende de la configuración (configTICK_RATE_HZ). En Arduino-ESP32 se usan normalmente 1000 Hz, es decir, un tick cada 1 ms. En otros puertos o configuraciones puede ser diferente.

No confundas el tick con una garantía de precisión absoluta. Un RTOS ayuda a acotar tiempos de respuesta, pero no convierte automáticamente cualquier código en determinista. Si bloqueamos la CPU o abusamos de secciones críticas, nos podemos cargar esa previsibilidad nosotros solitos.