Letâs be honest. Weâve all been there. You start a project thinking, âI just need to blink an LED and read a temperature sensor.â Fast forward three days, and your loop() function is a 600-line monster of delay(), millis() checks, and nested if statements that look like a staircase to hell. It is the definition of spaghetti code.
You try to add a simple button press, and suddenly your sensor readings freeze, your LEDs stop blinking, and somewhere in the distance, a capacitor pops and lets out the magic blue smoke out of pure spite. Your Arduino Uno is sweating. You are sweating. Why is doing two things at once so impossibly hard?
Because standard Arduino code is strictly single-threaded. It does one thing, then the next, then the next.
But you arenât using an Uno anymore, are you? Youâve graduated to the ESP32. And the ESP32 has a secret weapon built right in: FreeRTOS. Itâs time to stop juggling and start delegating.
FreeRTOS (Real-Time Operating System) is a tiny operating system that manages your microcontrollerâs resources. Instead of writing one massive loop(), you write small, independent âTasks.â
The ESP32 actually has two physical processor cores. FreeRTOS acts like a traffic cop, assigning your Tasks to these cores and switching between them so fast that they appear to run simultaneously. This is true multi-tasking.
delay() Guilt: In a standard sketch, delay(1000) pauses the entire microcontroller. In FreeRTOS, vTaskDelay() only pauses that specific task, letting the rest of your code run at full speed.Letâs write a simple program that blinks an LED on Core 0 and prints to the Serial Monitor from Core 1.
// Define the LED pin
const int ledPin = 2;
// Task handles
TaskHandle_t Task1;
TaskHandle_t Task2;
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
// Create Task 1 (Blink LED)
xTaskCreatePinnedToCore(
Task1code, /* Task function */
"Task1", /* Name of task */
10000, /* Stack size of task */
NULL, /* Parameter of the task */
1, /* Priority of the task */
&Task1, /* Task handle */
0); /* Pin task to core 0 */
// Create Task 2 (Print to Serial)
xTaskCreatePinnedToCore(
Task2code, "Task2", 10000, NULL, 1, &Task2, 1);
}
// Task 1: Blink the LED
void Task1code( void * pvParameters ){
for(;;){
digitalWrite(ledPin, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS); // FreeRTOS version of delay()
digitalWrite(ledPin, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// Task 2: Print to Serial
void Task2code( void * pvParameters ){
for(;;){
Serial.println("Task 2 running on Core 1!");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void loop() {
// We don't need the loop() anymore! FreeRTOS handles it.
}
xTaskCreatePinnedToCore(): This is the magic function. It tells the ESP32 to create a new task.Task1code: This is the actual function that will run endlessly (notice the for(;;) loop inside it).vTaskDelay(): This is critical! You MUST use vTaskDelay() instead of delay(). This tells the FreeRTOS scheduler, âHey, Iâm waiting for 500ms. Go run another task while I wait.âportTICK_PERIOD_MS: FreeRTOS measures time in âticksâ. This little bit of math converts milliseconds into the ticks the RTOS understands.vTaskDelay() or wait for an event. If you have an empty, infinite while(1) loop, it will hog the CPU core and trigger the Watchdog Timer to crash and reboot your board.10000 bytes of memory. If your task does a lot of heavy lifting (like Fetching Live Web Data), you might need more. If it crashes, increase the stack size.Stop writing spaghetti code. Embrace the traffic cop.