freertos-multitarea-cambio-contexto

Multitasking in FreeRTOS: Scheduling and Context Switching

  • 5 min

The multitasking in an RTOS is the ability to distribute the CPU among several independent tasks, so the system can handle different parts of the program without getting blocked on a single one.

This does not necessarily mean that all tasks run physically at the same time. On a single-core microcontroller, what we have is an incredibly fast alternation between tasks. So fast that, to us, it seems like everything happens at once.

On a classic ESP32, we have two cores, so besides multitasking, we can also achieve some real parallelism. Other models in the family, like the ESP32-C3, have a single core. Even so, it’s best to first understand single-core multitasking, as it’s the foundation for everything else.

Why we need multitasking

In a sequential program, one function starts, finishes, and then the next one starts. This is simple, but it has an obvious problem: if one part takes a long time, everything else waits.

For example, we might have a system that needs to:

  • Read a sensor every 100ms.
  • Update a display every 500ms.
  • Handle serial communication.
  • Monitor an emergency button.

With a classic loop(), we end up writing a mix of millis(), flags, states, and checks scattered throughout the code. It works, yes. But as the program grows, the logic starts to get tangled.

With FreeRTOS, we can separate the program into tasks:

void SensorTask(void *pvParameters) {
  for(;;) {
    readSensor();
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void DisplayTask(void *pvParameters) {
  for(;;) {
    updateDisplay();
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}
Copied!

Each task has its own loop, its own rhythm, and its own responsibility. The Scheduler handles deciding which one runs at any given moment.

Cooperative Multitasking

In cooperative multitasking, a task keeps the CPU until it voluntarily decides to give it up. That is, the task says something like “I’m done for now, let the next one have a turn.”

This can happen when it calls functions like:

  • vTaskDelay()
  • taskYIELD()
  • a blocking wait on a queue, semaphore, or notification

The cooperative approach is simple and predictable, but it has a rather serious problem: it depends on all tasks behaving well.

If a task gets stuck in an infinite loop without blocking or yielding the CPU, the other tasks don’t get any execution time. And of course, our firmware goes from “elegant multitasking” to “one guy hogging the microphone at a meeting.”

void RudeTask(void *pvParameters) {
  for(;;) {
    // Never blocks, never waits, never yields
    doCalculations();
  }
}
Copied!

This task can prevent other tasks of equal or lower priority from running correctly. That’s why, in a real RTOS, yielding the CPU is not a decorative detail.

Preemptive Multitasking

FreeRTOS is typically used in preemptive multitasking mode. This means the Scheduler can interrupt a task even if it hasn’t voluntarily yielded the CPU.

The Scheduler looks at the tasks that are ready to run and selects the one with the highest priority. If a higher priority task becomes ready, the current task is paused, and the new one starts executing.

// Conceptual pseudocode
if (there_is_a_higher_priority_task_ready) {
  save_context_of_current_task();
  load_context_of_high_priority_task();
  execute_high_priority_task();
}
Copied!

This is what allows a critical task to respond quickly, even if another task was doing less important work.

For example:

  1. DisplayTask is drawing graphics.
  2. An urgent data packet arrives from a sensor.
  3. ControlTask, which has higher priority, is unblocked.
  4. FreeRTOS pauses DisplayTask and executes ControlTask.
  5. When appropriate, DisplayTask continues from where it left off.

That’s the beauty of it. We didn’t have to fill our code with manual checks. The RTOS handles the distribution.

What is a Context Switch

A context switch is the operation of saving the state of one task and restoring that of another so that the CPU can continue executing a different task.

When FreeRTOS switches tasks, it needs to save information such as:

  • Processor registers.
  • Program counter.
  • Stack pointer.
  • Internal state needed to resume the task.

Then it loads the state of another task, and the CPU continues as if nothing had happened.

For the task, it’s as if time had frozen. It was paused at a specific instruction, and when it resumes, it continues exactly from that point.

This process has a cost. Small, but it exists. That’s why it’s not advisable to create tasks for absolutely everything, or to force unnecessary context switches every couple of microseconds.

The System Tick

FreeRTOS normally uses a periodic interrupt called the tick. It’s like an internal metronome that allows the Kernel to measure the passage of time.

On each tick, the system can check:

  • If a task blocked by vTaskDelay() should wake up.
  • If there are tasks of equal priority that need to take turns.
  • If any time-based condition has changed.

The tick frequency depends on the configuration (configTICK_RATE_HZ). In Arduino-ESP32, it’s typically 1000 Hz, meaning one tick every 1 ms. On other ports or configurations, it may be different.

Don’t confuse the tick with a guarantee of absolute precision. An RTOS helps bound response times, but it doesn’t automatically turn any code into deterministic code. If we lock up the CPU or abuse critical sections, we can easily ruin that predictability ourselves.