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);
}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
);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
loopTaskusually 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
);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 asxTaskCreate).
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);
}What is happening here?
- In
setup, we callxTaskCreatePinnedToCoretwice. - Immediately, the Scheduler detects that new tasks are ready.
FastTaskwill write to the serial port every half second.SlowTaskwill appear every 2 seconds.- 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
- Forgetting the infinite loop: If you write a task as a regular function that ends (
}), the task finishes, returns, and the system crashes. - 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. - Stack Overflow: Defining a
usStackDepththat is too small. If you see strange errors or a “Guru Meditation Error”, try increasing the stack.