💡 Electron Parade
← Back to Academy

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.

What is FreeRTOS?

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.

Why You Need It

Your First FreeRTOS Sketch

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.
}

Breaking Down the Code

  1. xTaskCreatePinnedToCore(): This is the magic function. It tells the ESP32 to create a new task.
  2. Task1code: This is the actual function that will run endlessly (notice the for(;;) loop inside it).
  3. 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.”
  4. portTICK_PERIOD_MS: FreeRTOS measures time in “ticks”. This little bit of math converts milliseconds into the ticks the RTOS understands.

The Golden Rules of FreeRTOS

  1. Never let a task run empty: A task must always have a 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.
  2. Mind your Stack Size: We gave our tasks 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.