freertos-gestion-memoria-stack

How to Size a Task Stack in FreeRTOS

  • 6 min

The Task Stack is the private memory area where a task stores its local variables, function calls, and context while it runs.

In the previous article we created our first tasks and, almost without thinking, assigned a value of 2048 to their stack size. Why 2048? Why not 100? Or 10,000?

RAM memory is one of the most delicate resources in an embedded system. If you allocate too much memory to a task, you are wasting RAM that you could use for other things. If you allocate too little, the program will crash catastrophically.

Today we will understand what the Stack is, how to calculate the appropriate size, and how to use FreeRTOS tools to sleep soundly.

What is a task stack?

When we create a task with xTaskCreate, the operating system reserves a contiguous block of memory in RAM exclusively for it. This block is its Stack.

Each task needs its own Stack because, when interrupted by the Scheduler, it needs a private place to store:

  1. Local Variables: All variables you declare inside the task function (and those of any functions it calls).
  2. Return Addresses: When the task calls a subfunction, it needs to know where to return.
  3. CPU Context: When the Scheduler pauses the task, it saves the processor registers here.

The maximum stack size is fixed when the task is created: FreeRTOS does not expand it if it becomes too small. If the task exceeds the limit, it can corrupt memory and eventually cause a crash.

The Great Confusion: Bytes or Words?

This is one of the points that causes the most confusion when switching platforms.

The usStackDepth parameter of the xTaskCreate function has different units depending on where you use it:

  • FreeRTOS Vanilla (ARM, AVR, etc.): The size is specified in Words. On a 32-bit architecture, 1 Word = 4 Bytes. If you set 100, you reserve 400 Bytes.
  • ESP32 (Arduino / ESP-IDF): The size is specified in BYTES. If you set 100, you reserve 100 Bytes.

For this course (ESP32): The number you enter is BYTES. This is critical. If you copy an example from the internet designed for an STM32 that says StackSize = 128 (words), and you use it on your ESP32, you will be reserving only 128 bytes. That’s not enough to even save the context. It will crash immediately.

The Feared Stack Overflow

A Stack Overflow occurs when a task tries to write more data than fits in its allocated stack.

Since there are typically no hardware-enforced bounds in C/C++, the task will keep writing to the next memory address. And what’s there?

  • Another task’s Stack.
  • Global variables.
  • Kernel data itself.

The result is erratic behavior, random reboots, or the famous ESP32 error: Guru Meditation Error: Core 1 panic'ed (Stack Protection Fault).

Common Causes

Large local arrays: For example, char buffer[2048]; inside a function. If its lifetime allows, use static storage or allocate memory on the heap, always checking the result.

Recursive functions: Each recursive call consumes memory. In embedded systems, recursion is dangerous.

Excessive use of printf / Serial.print: These functions use large internal buffers. A task that uses printf typically needs at least 2048 bytes (or more) of stack.

Tool: High Water Mark

Calculating the exact size “on paper” is almost impossible due to the complexity of libraries. The professional strategy is: Estimate high, Measure, and Reduce.

FreeRTOS offers a very useful function: uxTaskGetStackHighWaterMark().

This function returns the minimum amount of free space that has remained in the Stack since the task started. It’s like the high tide mark on the beach (but in reverse, it tells us how much “breathing room” we have left).

  • In standard FreeRTOS, the result is expressed in words of StackType_t.
  • In ESP-IDF FreeRTOS, the result is expressed in bytes, just like the size passed to xTaskCreate.
  • If it returns 1000 on an ESP32, it means that at the worst observed moment, 1000 bytes were left over. You might be able to reduce the stack.
  • If it returns 50: Danger! You are at the limit. Any small change could cause an overflow.

Practical Example: Adjusting the Stack

Let’s create a task, “stress” it with variables, and see how much memory is left over.

TaskHandle_t taskHandle = NULL;

__attribute__((noinline)) void useLocalBuffer() {
  volatile uint8_t buffer[1000];

  for (int i = 0; i < 1000; i++) {
    buffer[i] = (uint8_t)i;
  }
}

void ConsumerTask(void *pvParameters) {
  UBaseType_t highWaterMark;

  highWaterMark = uxTaskGetStackHighWaterMark(NULL);
  Serial.printf("Task Start. Free: %d bytes\n", highWaterMark);

  // The helper function adds a 1000-byte buffer to the stack.
  useLocalBuffer();
  highWaterMark = uxTaskGetStackHighWaterMark(NULL);
  Serial.printf("After using buffer. Free: %d bytes\n", highWaterMark);
  
  Serial.println("Hello world from a stressed task");

  highWaterMark = uxTaskGetStackHighWaterMark(NULL);
  Serial.printf("After printing. Free: %d bytes\n", highWaterMark);

  for(;;) {
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  // Create the task with 2048 bytes of Stack
  xTaskCreatePinnedToCore(
    ConsumerTask, "TestTask",
    2048,  // Stack in BYTES (ESP32)
    NULL, 1, &taskHandle, 1
  );
}

void loop() {}
Copied!

Analysis of the Result

When you run this, you will see that the mark decreases after calling useLocalBuffer(). The exact values depend on the compiler, optimization settings, and the core version, so you should not assume a specific number.

The measurement only captures the worst usage observed up to that point. Test all execution paths and leave sufficient margin for exceptional cases, interrupts, and future modifications.

Automatic Overflow Detection

The ESP32 and FreeRTOS have protection mechanisms. In the file FreeRTOSConfig.h (preconfigured on ESP32) there is the option configCHECK_FOR_STACK_OVERFLOW.

Depending on the port and the chosen option, the kernel checks the stack pointer and can also verify the pattern written at the end of the reserved memory. Additionally, the ESP32 uses its own protection mechanisms, such as the stack canary.

These checks help detect the problem, but they do not replace proper sizing: an overflow can corrupt memory before the kernel gets a chance to check it.

If you see on the serial monitor: Stack canary watchpoint triggered (loopTask) It means you have run out of memory. Increase the Stack size of that task!

Tips for Healthy Code

  1. Avoid large variables on the Stack: If you need a 5 KB buffer, consider using static storage or dynamic allocation. If you use malloc, check the result and clearly define who frees the memory.
  2. Be careful with Serial: If a task only blinks an LED, it needs very little stack (perhaps 768 bytes). If you add a Serial.println, you will need to increase it to 2048 bytes or more.
  3. Final Tuning: During development, give it plenty of memory (e.g., 4096). When you finish the project, use HighWaterMark to adjust the size and reclaim wasted RAM.