Introduction to RTOS with embedded systems¶
FreeRTOS Instructions¶
Task definition¶
TaskHandle_tis a typedef (alias) for a pointer type used by FreeRTOS to reference a task. It’s essentially a handle that uniquely identifies a task within the RTOS scheduler.handle_task_nameis the variable name you’re declaring. It will store the handle of a task after you create it.- You usually declare the handle as a global variable
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t task_function,
const char * const task_description,
const uint32_t memory_in_words,
void *parameters,
UBaseType_t task_priority,
TaskHandle_t *handle_task_name,
const BaseType_t core_ID
);
task_function: The task function (void task_function(void *parameters)). The entry point for the task. It usually contains an infinite loop (while(1)).task_description: Descriptive task name (for debugging). E.g.,"Task 1".memory_in_words: Stack size in words (on ESP32 - 32-bit words, 1 word = 4 bytes). If you think in bytes, divide by 4. Example: 4096 bytes → 4096 / 4 = 1024 words. It depends on what the task does (printf, floating point, deep call chains, libraries, etc.). If the memory is set too high, you'll waste RAM. If the memory is set two low you'll find an error like***ERROR*** A stack overflow in task task_description has been detected.parameters: Pointer passed to the task (orNULL). We'll always useNULLin the course.task_priority: Task priority (higher number = higher priority). Guidelines: Keep typical app tasks around 1–5. Avoid competing with critical system tasks (Wi‑Fi/BLE) unless necessary. Only use very high priority for time-critical loops (e.g., your WNN control loop), and keep them efficient. Pitfall: A high-priority task that doesn’t block can starve others. Ensure it yields via vTaskDelay, queues, or event groups. We'll only use priorities 0 or 1.handle_task_name: Receives the created task’s handle. Use it to later suspend, resume, delete, change affinity (IDF APIs), or query stack.core_ID: The CPU core the task is pinned to. On ESP32 0 or 1 to select the core. Arduino-ESP32 often runs the loopTask and Wi‑Fi on core 1; pinning your real-time/control task to core 0 can improve determinism.
Minimal example¶
TaskHandle_t HandleTaskName;
void setup(){
xTaskCreatePinnedToCore(
taskFunction, // Function name that implements the task
"task description", // Task description
2048/4, // Memory (in bytes) assigned to this task (don't set too high)
NULL, // Task input parameter (none)
0, // Task priority 0
&HandleTaskName, // Handle of the task
0 // Core where task 1 will run (this one in core 0)
);
}
void loop(){
// empty
}
void taskFunction(void *parameters)
{
// Local constants/variables for this task (can share names across tasks without interfering)
// some code...
while (1) // infinite loop
{
// some code...
}
}
Periodic tasks¶
TickType_tis the unsigned integer type FreeRTOS uses for tick counts (system time in ticks). On ESP32 and most 32-bit ports it’s 32‑bit. Each tick is a fixed time quantum defined by the RTOS tick rate.xLastWakeTimeis a variable that stores the reference tick for your task’s last wake-up. You initialize it once before your periodic loop and then pass it by reference tovTaskDelayUntil()so FreeRTOS can wake the task at precise intervals (reduces jitter and drift vs.vTaskDelay()).
portTICK_PERIOD_MSis A constant that equals the duration of one tick in milliseconds. It is derived fromconfigTICK_RATE_HZ:portTICK_PERIOD_MS = 1000 / configTICK_RATE_HZ. E.g., ifconfigTICK_RATE_HZ = 1000, thenportTICK_PERIOD_MS = 1 ms(1 tick = 1 ms). IfconfigTICK_RATE_HZ = 100, thenportTICK_PERIOD_MS = 10 ms(1 tick = 10 ms). By default, 1 tick = 1ms.xPeriodis the period in ticks that you want the task to run at, computed from a desired period in milliseconds.period_in_msis the period in ms (here you can write the period of the task in ms).- In practice, you can use the macro
pdMS_TO_TICKS(period_in_ms)(preferred), which handles rounding and avoids integer mistakes:
vTaskDelayUntil(&xLastWakeTime, xPeriod)is is a FreeRTOS API call used inside a task’s loop to make it run at a fixed, periodic rate with minimal drift. It puts the current task to sleep until the next absolute wake-up time based onxLastWakeTimeandxPeriod. UnlikevTaskDelay()(which sleeps relative to “now”),vTaskDelayUntil()targets exact tick instants(t0, t0 + T, t0 + 2T, …), keeping a stable cadence even if individual iterations vary slightly in execution time.
Minimal example¶
TaskHandle_t HandleTaskName;
void setup(){
xTaskCreatePinnedToCore(
taskFunction, // Function name that implements the task
"task description", // Task description
2048/4, // Memory (in bytes) assigned to this task (don't set too high)
NULL, // Task input parameter (none)
0, // Task priority 0
&HandleTaskName, // Handle of the task
0 // Core where task 1 will run (this one in core 0)
);
}
void loop(){
// empty
}
void taskFunction(void *parameters)
{
// Local constants/variables for this task (can share names across tasks without interfering)
const TickType_t xPeriod = pdMS_TO_TICKS(10); // 10 ms period
TickType_t xLastWakeTime = xTaskGetTickCount(); // initialize once
// some code...
while (1) // infinite loop
{
// some code...
// Sleep until the next absolute release time (no drift)
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
Exercises¶
Exercise 8: Multicore app reading from two analog sensors¶
Read from two analog sensors using both cores of the ESP32 (one sensor per core). One sensor is read at 1Hz, and the other at 2Hz.

Download the solution here
Exercise 9: Simple multicore app with two buttons and two LEDs¶
Use one core to handle the red button and red LED, and the other core to handle the green button and green LED. One the button is pressed, the LED changes its state.

Download the solution here
Exercise 10: Multicore app with two buttons and two LEDs, and periodic tasks¶
With the same diagram as in exercise 9, when the red button is pressed, the red LED blinks at 3Hz. When the green button is pressed, the green LED blinks at 2Hz. When a button is pressed again, its corresponding LED turns off.
Download the solution here