freertos-creacion-tareas-xtaskcreate

How to create tasks in FreeRTOS with xTaskCreate

  • 6 min

A task in FreeRTOS is an independent function that the Scheduler can execute, pause, and resume as part of a multitasking system.

We’ve already covered the theory and know how the system integrates into our ESP32, so it’s time to write real code. Our goal today is to learn how to define these tasks and tell the Scheduler to start them.

In an RTOS, a task is like a small independent program that believes it has the CPU all to itself.

Anatomy of a task

Before calling any creation function, we need to define what the task will do. In C/C++, a task is implemented as a function that returns nothing (void) and receives a void pointer (void*) as a parameter.

It has a very characteristic structure that we must respect:

void ExampleTask(void *pvParameters) {
    // 1. Task initialization (executed once)
    int localVariable = 0;

    // 2. Infinite loop
    for(;;) {
        // Task code (what repeats)
        Serial.println("Hello from the task");
        
        // 3. IMPORTANT! Yield control
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
    
    // 4. If we exit the loop, we must delete the task
    vTaskDelete(NULL); 
}
Copied!

A task must never return. Reaching the end violates the API contract and the port may abort the application. If you want the task to end, delete it explicitly with vTaskDelete(NULL).

The xTaskCreate function

To instantiate that function as a real task within the operating system, we use the xTaskCreate API. It’s one of the most important functions you’ll see in this course.

Its signature can be intimidating at first due to the number of parameters, but let’s break them down one by one:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,   // Function implementing the task
    const char * const pcName,   // Descriptive name (for debugging)
    configSTACK_DEPTH_TYPE usStackDepth, // Stack size
    void *pvParameters,          // Parameters for the task
    UBaseType_t uxPriority,      // Priority
    TaskHandle_t *pxCreatedTask  // Handle to control it
);
Copied!

pvTaskCode

It’s simply the name of the function we created above. If your function is called BlinkTask, you put BlinkTask here.

pcName

A plain text name, like "Blink LED". FreeRTOS doesn’t use it for anything functional, it’s only for us humans to identify the task during debugging.

usStackDepth (watch out here)

This is the amount of memory we reserve for the stack (local variables, function calls, etc.) of this task.

  • In standard FreeRTOS (Vanilla): It is specified in Words. On a 32-bit system, 1 word = 4 bytes.
  • On ESP32 (ESP-IDF/Arduino): It is specified in BYTES.

For the ESP32, a safe starting value is usually 2048 (bytes). If we set it too low, we’ll get a Stack Overflow and the microcontroller will reboot. We’ll delve into how to calculate this properly in the next article.

pvParameters

A void* pointer to pass arguments to the task when creating it. If we don’t need to pass anything, we set NULL. This is very useful for reusing the same code function for multiple tasks (e.g., the same blink function for 3 different LEDs).

uxPriority

An integer that defines importance.

  • In FreeRTOS, the higher the number, the higher the priority.
  • Arduino’s loopTask usually runs at priority 1.

pxCreatedTask

A pointer to save the task’s “ID” (Handle). It’s useful if we later want to suspend it, change its priority, or delete it from another task. If we don’t need it, we set NULL.

ESP32: xTaskCreatePinnedToCore

On a dual-core ESP32, xTaskCreate creates a task without affinity. The Scheduler decides which core can execute it and can migrate it between different activations.

If we want to force a task to run always on a specific core (for example, to leave Core 0 free for WiFi), we use the Espressif-specific variant:

xTaskCreatePinnedToCore(
    Function, Name, Stack, Params, Priority, Handle, 
    xCoreID // <--- New extra parameter
);
Copied!

The xCoreID parameter can be:

  • 0: Protocol CPU (WiFi/BT/System).
  • 1: App CPU (Where your code usually runs).
  • tskNO_AFFINITY: Let the Scheduler choose (same as xTaskCreate).

Complete example: real multitasking

Let’s look at an example where we create two tasks that run in parallel at different speeds, independent of loop().

// Handle to control the tasks (optional, in case we want to delete them later)
TaskHandle_t Task1_Handle = NULL;
TaskHandle_t Task2_Handle = NULL;

// --- Task Definition ---

// Task 1: Prints fast
void PrintFastTask(void *parameter) {
  for(;;) { // Infinite loop
    Serial.println("Fast Task: Running on Core " + String(xPortGetCoreID()));
    // Block the task for 500ms
    vTaskDelay(pdMS_TO_TICKS(500)); 
  }
}

// Task 2: Prints slow
void PrintSlowTask(void *parameter) {
  for(;;) {
    Serial.println("--- Slow Task: Running on Core " + String(xPortGetCoreID()));
    // Block the task for 2000ms
    vTaskDelay(pdMS_TO_TICKS(2000));
  }
}

// --- Setup ---

void setup() {
  Serial.begin(115200);
  delay(1000); // Wait for the serial monitor to start
  
  Serial.println("Starting FreeRTOS Demo...");

  // Create Task 1 on Core 1
  xTaskCreatePinnedToCore(
    PrintFastTask,      // Function
    "FastTask",         // Name
    2048,               // Stack size (bytes on ESP32)
    NULL,               // Parameters
    1,                  // Priority
    &Task1_Handle,      // Handle
    1                   // Core ID
  );

  // Create Task 2 on Core 0 (for variety)
  xTaskCreatePinnedToCore(
    PrintSlowTask,
    "SlowTask",
    2048,
    NULL,
    1,
    &Task2_Handle,
    0
  );
  
  Serial.println("Tasks created. The loop() continues its life...");
}

void loop() {
  // The loop is also a task.
  // We can leave it empty or use it for low-priority things.
  delay(10000); 
}
Copied!

What is happening here?

  1. In setup, we call xTaskCreatePinnedToCore twice.
  2. Immediately, the Scheduler detects that new tasks are ready.
  3. FastTask will write to the serial port every half second.
  4. SlowTask will appear every 2 seconds.
  5. Meanwhile, the original loop() still exists, but it’s now just another actor in the play, not the director.

Notice vTaskDelay(pdMS_TO_TICKS(500)). We don’t use delay(). vTaskDelay puts the current task into the Blocked state, allowing the core to execute other tasks or the Idle task. If we used a while loop to wait, we would be wasting CPU time.

Common mistakes when starting out

  1. Forgetting the infinite loop: If you write a task as a regular function that ends (}), the task finishes, returns, and the system crashes.
  2. Not yielding CPU (Starvation): If you create a high-priority task with a while(1) that never blocks, it can starve lower-priority tasks on the same core, including Idle. The Task Watchdog can detect this situation and cause a reboot, depending on its configuration.
  3. Stack Overflow: Defining a usStackDepth that is too small. If you see strange errors or a “Guru Meditation Error”, try increasing the stack.