El Stack de una tarea es la zona de memoria privada donde una tarea guarda sus variables locales, llamadas y contexto mientras se ejecuta.
En el artículo anterior creamos nuestras primeras tareas y, casi sin pensar, asignamos un valor de 2048 al tamaño de su pila. ¿Por qué 2048? ¿Por qué no 100? ¿O 10.000?
La memoria RAM es uno de los recursos más delicados en un sistema embebido. Si asignas demasiada memoria a una tarea, estarás desperdiciando RAM que podrías usar para otras cosas. Si asignas muy poca, el programa se estrellará de forma catastrófica.
Hoy vamos a entender qué es el Stack, cómo calcular el tamaño adecuado y cómo usar las herramientas de FreeRTOS para dormir tranquilos.
¿Qué es el stack de una tarea?
Cuando creamos una tarea con xTaskCreate, el sistema operativo reserva un bloque de memoria contigua en la RAM exclusivamente para ella. Este bloque es su Stack (Pila).
Cada tarea necesita su propio Stack porque, al ser interrumpida por el Scheduler, necesita un lugar privado donde guardar:
- Variables Locales: Todas las variables que declaras dentro de la función de la tarea (y las de las funciones que esta llame).
- Direcciones de Retorno: Cuando la tarea llama a una subfunción, necesita saber a dónde volver.
- Contexto de CPU: Cuando el Scheduler pausa la tarea, guarda los registros del procesador aquí.
El tamaño máximo de la pila queda fijado al crear la tarea: FreeRTOS no la amplía si se queda pequeña. Si la tarea supera el límite, puede corromper memoria y acabar provocando un fallo.
La gran confusión: ¿bytes o palabras?
Este es uno de los puntos que más confusión provoca al cambiar de plataforma.
El parámetro usStackDepth de la función xTaskCreate tiene unidades distintas según dónde lo uses:
- FreeRTOS Vanilla (ARM, AVR, etc.): El tamaño se especifica en Words (Palabras). En una arquitectura de 32 bits, 1 Word = 4 Bytes. Si pones
100, reservas 400 Bytes. - ESP32 (Arduino / ESP-IDF): El tamaño se especifica en BYTES. Si pones
100, reservas 100 Bytes.
Para este curso (ESP32): El número que pones son BYTES.
Esto es crítico. Si copias un ejemplo de Internet pensado para un STM32 que pone StackSize = 128 (palabras), y lo pones en tu ESP32, estarás reservando solo 128 bytes. Eso no alcanza ni para guardar el contexto. Crasheará inmediatamente.
El temido stack overflow
Un Stack Overflow (Desbordamiento de Pila) ocurre cuando una tarea intenta escribir más datos de los que caben en su pila asignada.
Como en C/C++ no hay límites de seguridad por hardware (normalmente), la tarea seguirá escribiendo en la dirección de memoria siguiente. ¿Y qué hay ahí?
- El Stack de otra tarea.
- Variables globales.
- Datos del propio Kernel.
El resultado es un comportamiento errático, reinicios aleatorios o el famoso error del ESP32: Guru Meditation Error: Core 1 panic'ed (Stack Protection Fault).
Causas comunes
Arrays locales grandes: Por ejemplo, char buffer[2048]; dentro de una función. Si su vida útil lo permite, usa almacenamiento estático o reserva memoria en el heap comprobando siempre el resultado.
Funciones recursivas: Cada llamada recursiva consume memoria. En embebidos, la recursividad es peligrosa.
Uso excesivo de printf / Serial.print: Estas funciones usan buffers internos grandes. Una tarea que use printf suele necesitar al menos 2048 bytes (o más) de pila.
Herramienta: High Water Mark
Calcular el tamaño exacto “a papel y lápiz” es casi imposible debido a la complejidad de las librerías. La estrategia profesional es: Estimar al alza, Medir y Reducir.
FreeRTOS nos ofrece una función muy útil: uxTaskGetStackHighWaterMark().
Esta función nos devuelve el mínimo espacio libre que ha quedado en el Stack desde que la tarea arrancó. Es como la marca de la marea alta en la playa (pero al revés, nos dice cuánto “aire” nos queda).
- En FreeRTOS estándar, el resultado se expresa en palabras de
StackType_t. - En ESP-IDF FreeRTOS, el resultado se expresa en bytes, igual que el tamaño pasado a
xTaskCreate. - Si devuelve
1000en ESP32, significa que en el peor momento observado sobraron 1000 bytes. Quizá puedas reducir la pila. - Si devuelve
50: ¡Peligro! Estás al límite. Cualquier cambio pequeño podría provocar un overflow.
Ejemplo Práctico: Ajustando el Stack
Vamos a crear una tarea, “estresarla” con variables y ver cuánta memoria nos sobra.
TaskHandle_t tareaHandle = NULL;
__attribute__((noinline)) void usarBufferLocal() {
volatile uint8_t buffer[1000];
for (int i = 0; i < 1000; i++) {
buffer[i] = (uint8_t)i;
}
}
void TareaConsumidora(void *pvParameters) {
UBaseType_t highWaterMark;
highWaterMark = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Inicio Tarea. Libre: %d bytes\n", highWaterMark);
// La función auxiliar añade un buffer de 1000 bytes a la pila.
usarBufferLocal();
highWaterMark = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Tras usar el buffer. Libre: %d bytes\n", highWaterMark);
Serial.println("Hola mundo desde una tarea con carga");
highWaterMark = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Después de imprimir. Libre: %d bytes\n", highWaterMark);
for(;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void setup() {
Serial.begin(115200);
delay(1000);
// Creamos la tarea con 2048 bytes de Stack
xTaskCreatePinnedToCore(
TareaConsumidora, "TareaTest",
2048, // Stack en BYTES (ESP32)
NULL, 1, &tareaHandle, 1
);
}
void loop() {}Análisis del resultado
Al ejecutar esto verás que la marca disminuye después de llamar a usarBufferLocal(). Los valores exactos dependen del compilador, de la optimización y de la versión del core, así que no conviene asumir una cifra concreta.
La medida solo recoge el peor uso observado hasta ese momento. Prueba todos los caminos de ejecución y deja un margen suficiente para casos excepcionales, interrupciones y futuras modificaciones.
Detección Automática de Overflow
El ESP32 y FreeRTOS tienen mecanismos de protección. En el archivo FreeRTOSConfig.h (preconfigurado en ESP32) existe la opción configCHECK_FOR_STACK_OVERFLOW.
Según el port y la opción elegida, el kernel comprueba el puntero de pila y también puede verificar el patrón escrito al final de la memoria reservada. Además, ESP32 usa mecanismos de protección propios, como el stack canary.
Estas comprobaciones ayudan a detectar el problema, pero no sustituyen a un dimensionamiento correcto: un desbordamiento puede corromper memoria antes de que el kernel llegue a comprobarlo.
Si ves en el monitor serie:
Stack canary watchpoint triggered (loopTask)
Significa que te has quedado corto de memoria. ¡Sube el tamaño del Stack de esa tarea!
Consejos para un código saludable
- Evita variables grandes en el Stack: Si necesitas un buffer de 5 KB, valora usar almacenamiento estático o asignación dinámica. Si recurres a
malloc, comprueba el resultado y define con claridad quién libera la memoria. - Cuidado con
Serial: Si una tarea solo hace parpadear un LED, necesita muy poca pila (quizás 768 bytes). Si le añades unSerial.println, necesitarás subir a 2048 bytes o más. - Ajuste final: Durante el desarrollo, dale memoria de sobra (ej. 4096). Cuando acabes el proyecto, usa
HighWaterMarkpara ajustar el tamaño y recuperar RAM desperdiciada.