ESP32 / ESP-IDF

Your Arduino `loop()` Runs One Task. ESP-IDF Runs Ten — Here's How Arduino Loop Vs ESP-IDF Multitasking using FreeRTOS

By Rajath Kumar K S May 1, 2026
Your Arduino `loop()` Runs One Task. ESP-IDF Runs Ten — Here's How
Rajath Kumar K S May 1, 2026 0 views 9 min

The Moment It Clicked for Me

I was running a demo at a training session — engineers from a large manufacturing company, people who had been writing code for years. I asked them to blink an LED every 500ms and simultaneously read a temperature sensor every 200ms and send data over UART every 1 second.

Using Arduino.

They stared at me like I'd asked them to ride a unicycle and juggle simultaneously.

Because in Arduino's model, you literally can't do that cleanly. Not without hacks. Not without your timing going sideways the moment one task takes a millisecond longer than expected.

Then I showed them the same thing in ESP-IDF with FreeRTOS. Three tasks, running in parallel, each minding its own business. Clean. Predictable. Production-grade.

That's the gap we're talking about today.


What Actually Happens Inside loop()

Let's be honest about what Arduino's loop() is: it's an infinite while(true) loop running on bare metal. No operating system. No scheduler. Just your code, running top to bottom, over and over.

void loop() {
  readSensor();      // takes 5ms
  sendUART();        // takes 10ms
  blinkLED();        // wants to happen every 500ms
  // ...and everything blocks everything else
}

This is called superloop architecture. It works perfectly for simple projects — blink an LED, read a button. But the moment your project has multiple things happening at different rates, you're in trouble.

Your LED wants to blink every 500ms. Your sensor wants to be read every 200ms. Your UART wants to send every 1 second. In a superloop, you have to manually track timing with millis() for each of these. And if one function takes too long — maybe your sensor is slow, maybe UART is backed up — everything else gets delayed.

This is not how production embedded systems work.

[Diagram: Side-by-side comparison of Arduino superloop vs FreeRTOS multi-task execution timeline. 
Show Arduino as a single horizontal bar with sequential blocks (sensor read → blink → UART → repeat).
Show FreeRTOS as three parallel horizontal bars (Task 1: Sensor, Task 2: Blink, Task 3: UART) 
with a vertical scheduler arrow showing context switches between them.]

![[blog_1_image_1.png]]


Enter ESP-IDF and FreeRTOS

The ESP32 is not just a beefed-up Arduino board. It's a dual-core 240MHz processor running FreeRTOS — a real-time operating system. ESP-IDF (Espressif IoT Development Framework) is the official SDK that gives you full access to everything the chip can do.

FreeRTOS gives you tasks — think of them as mini-programs running simultaneously, each with their own stack, their own priority, their own timing. The FreeRTOS scheduler decides which task runs when, switching between them so fast it feels like true parallelism (on the ESP32's dual core, you actually get true parallelism for two tasks at once).

Here's the key mental shift:

Arduino thinks in functions. ESP-IDF thinks in tasks.


Let's Build It — 3 Tasks Running Simultaneously

We'll create three tasks:

  1. LED Blink Task — blinks every 500ms
  2. Sensor Read Task — "reads" a simulated sensor every 200ms
  3. UART Log Task — logs system status every 1 second

Hardware Required

  • Seeed Studio XIAO ESP32-S3 (or any ESP32 variant)
  • Built-in LED (or external LED on GPIO 21 with 330Ω resistor)
  • USB cable
  • ESP-IDF v5.x installed
[Photo: XIAO ESP32-S3 board on a clean white surface, showing the USB-C port and the onboard LED.
Crisp product-style shot. No breadboard clutter needed for this example.]

Project Structure

multitask_demo/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt
│   └── main.c

The Code

// main/main.c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define LED_PIN GPIO_NUM_21
static const char *TAG = "MULTITASK";

// Task 1: Blink LED every 500ms
void led_blink_task(void *pvParameters) {
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_PIN),
        .mode = GPIO_MODE_OUTPUT,
    };
    gpio_config(&io_conf);

    while (1) {
        gpio_set_level(LED_PIN, 1);
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(LED_PIN, 0);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// Task 2: Read sensor every 200ms (simulated)
void sensor_read_task(void *pvParameters) {
    int sensor_value = 0;

    while (1) {
        // Simulate sensor read
        sensor_value = (sensor_value + 1) % 100;
        ESP_LOGI(TAG, "[SENSOR] Value: %d", sensor_value);
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// Task 3: Log system status every 1 second
void uart_log_task(void *pvParameters) {
    int log_count = 0;

    while (1) {
        ESP_LOGI(TAG, "[STATUS] Log #%d | Heap Free: %lu bytes", 
                 log_count++, esp_get_free_heap_size());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void) {
    ESP_LOGI(TAG, "Starting Multi-Task Demo on ESP32");

    // Create tasks — notice the stack size and priority parameters
    xTaskCreate(led_blink_task,    "LED_Blink",  2048, NULL, 3, NULL);
    xTaskCreate(sensor_read_task,  "Sensor_Read", 2048, NULL, 2, NULL);
    xTaskCreate(uart_log_task,     "UART_Log",    4096, NULL, 1, NULL);

    // app_main itself becomes a low-priority task and can end here
    // FreeRTOS scheduler takes over
}

CMakeLists.txt (project root)

cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(multitask_demo)

CMakeLists.txt (main/)

idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS ".")

Build and Flash

# Set your target (XIAO ESP32-S3 uses esp32s3)
idf.py set-target esp32s3

# Build
idf.py build

# Flash + Monitor (replace with your actual port)
idf.py -p /dev/ttyUSB0 flash monitor

What You'll See

I (320) MULTITASK: Starting Multi-Task Demo on ESP32
I (330) MULTITASK: [SENSOR] Value: 1
I (530) MULTITASK: [SENSOR] Value: 2
I (730) MULTITASK: [SENSOR] Value: 3
I (830) MULTITASK: [STATUS] Log #0 | Heap Free: 284000 bytes
I (930) MULTITASK: [SENSOR] Value: 4
...

The LED is blinking independently. The sensor is reading every 200ms. The status log fires every second. All at the same time. No blocking. No timing hacks.

[Screenshot: ESP-IDF monitor terminal showing the interleaved output — SENSOR logs at 200ms intervals,
STATUS logs at 1s intervals. Shows the real-time nature clearly. Dark terminal, colored log output.]

Breaking Down xTaskCreate() — The One Function That Changes Everything

xTaskCreate(
    led_blink_task,    // Function pointer — the task code
    "LED_Blink",       // Name (for debugging in Task Manager)
    2048,              // Stack size in bytes — how much memory this task gets
    NULL,              // Parameter to pass into the task (pvParameters)
    3,                 // Priority — higher number = higher priority
    NULL               // Task handle (NULL if you don't need to reference it later)
);

Stack size is something Arduino developers never think about because there's only one stack. In FreeRTOS, each task has its own stack. Too small → stack overflow → crash. Too big → wasted RAM. 2048 bytes is a safe starting point for simple tasks. If your task uses printf, local arrays, or calls deep functions, increase it.

Priority matters when two tasks are both ready to run. Higher priority wins. The IDLE task runs at priority 0 — never go below that. Keep your priorities sensible: critical tasks get higher numbers.

vTaskDelay(pdMS_TO_TICKS(500)) is the FreeRTOS way to "wait." It tells the scheduler: "I'm done for 500ms, give CPU time to someone else." This is fundamentally different from delay(500) in Arduino, which burns CPU cycles doing nothing useful. vTaskDelay yields. delay() wastes.


The vTaskDelay vs delay() Problem — This One Matters in Production

In Arduino:

delay(500);  // CPU sits here doing NOTHING. All other "tasks"? Blocked.

In FreeRTOS:

vTaskDelay(pdMS_TO_TICKS(500));  // This task sleeps. Scheduler gives CPU to others.

For a hobbyist blinking an LED, this doesn't matter. For a system where you're reading a sensor, handling Bluetooth, managing a display, and sending data to a cloud endpoint — it's the difference between a system that works and a system that constantly misses events.

In India's manufacturing sector, I've seen PLCs replaced with ESP32-based solutions. Those systems run 24/7. A blocking delay() in a critical sensor task isn't acceptable there. FreeRTOS tasks with proper vTaskDelay are.


Task Communication — How Tasks Talk to Each Other

Tasks don't run in isolation in real systems. They share data. FreeRTOS gives you several tools for this:

Queues — The Right Way to Share Data Between Tasks

#include "freertos/queue.h"

QueueHandle_t sensor_queue;

void sensor_producer_task(void *pvParameters) {
    int value;
    while (1) {
        value = read_sensor();  // your actual sensor read
        xQueueSend(sensor_queue, &value, pdMS_TO_TICKS(10));
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

void display_consumer_task(void *pvParameters) {
    int received_value;
    while (1) {
        if (xQueueReceive(sensor_queue, &received_value, pdMS_TO_TICKS(300))) {
            update_display(received_value);
        }
    }
}

void app_main(void) {
    sensor_queue = xQueueCreate(10, sizeof(int));  // Queue holds 10 integers
    xTaskCreate(sensor_producer_task, "Producer", 2048, NULL, 2, NULL);
    xTaskCreate(display_consumer_task, "Consumer", 4096, NULL, 1, NULL);
}

This is thread-safe. No global variable races. No data corruption. This is how production IoT firmware is written.

[Diagram: Queue communication flow. Two boxes labeled "Sensor Task" and "Display Task" 
with an arrow going through a "Queue (10 slots)" box in the middle. 
Shows producer pushing data in, consumer pulling data out. Clean, minimal diagram.]

![[blog_1_image_4.png]]

Why This Matters for You

India is building. Smart agriculture. Industry 4.0 factories in Pune and Surat. Smart meters rolling out across UP and Maharashtra under RDSS. Smart city infrastructure from Kochi to Lucknow.

Every one of these systems needs embedded firmware that can handle multiple things simultaneously — reading sensors, communicating over 4G/NB-IoT, managing local storage, updating OTA, and doing it all without a crash.

The companies doing this seriously — Bosch, Siemens, Tata Elxsi, Wipro's embedded division, dozens of product startups in Bengaluru and Hyderabad — they're not writing Arduino sketches for production. They're using ESP-IDF, Zephyr, or bare-metal RTOS. If you want to be hireable in this space or build products that actually work at scale, you need to understand FreeRTOS.

An ESP32-S3 module costs ₹250–400. The development tools are free. There is no excuse not to learn this.


Key Takeaways

  1. Arduino's loop() is a superloop — one task, sequential, blocking. Good for hobby projects. Not for production.
  2. FreeRTOS tasks are independent execution units — each has its own stack, priority, and timing. The scheduler handles the rest.
  3. vTaskDelay() yields the CPUdelay() wastes it. In a multi-task system, this is not optional knowledge.
  4. Queues are the safe way to share data between tasks. Global variables without protection = data races = random crashes.
  5. The ESP32 runs FreeRTOS natively — you're not adding an OS on top, you're using what's already there. ESP-IDF is built around it.
  6. This is production-grade thinking — the same patterns used in IoT products shipping to thousands of users across India and globally.

What's Next

If this clicked for you, the next step is getting hands-on with a real board and building something that actually does multiple things at once — sensor fusion, BLE + Wi-Fi simultaneous, or TinyML inference running alongside data collection.

That's exactly what we cover in depth at our 2-Day Hands-On Workshop: "AI on the Edge with IoT — Using ESP32 & IDF".

Register here → edgeai.analogdata.io

We use the Seeed Studio XIAO ESP32-S3, ESP-IDF v5.x, and on Day 2 we run actual TinyML models on the edge. Limited seats. Bengaluru-based.


More from Analog Data:


Share this article

Frequently Asked Questions

Quick answers to common questions

ESP32 Resources

Master EdgeAI, IoT & Embedded

Master Embedded Systems & IoT by building production-ready firmware — not toy demos. ESP-IDF, FreeRTOS, TinyML, and AWS IoT.

Category

Sub: ESP-IDF

Projects and deep dives on Espressif's ESP32 family of microcontrollers covering ESP-IDF, FreeRTOS, Wi-Fi, BLE, TinyML, and production-grade firmware development.

Author

IoT & AI Practitioner, Principal Developer

Weekly signal, no noise

Get Analog Data in your inbox

Practical breakdowns on AI, edge, infra, and playbooks from the team that ships them. One concise email each week.

What you get

New articles, teardown summaries, and code snippets.

What you don’t

No spam. No forwarded press. Unsubscribe anytime.

LLMs & RAG Edge & IoT Infra & Observability Playbooks

Join the list

1,350+ subscribers

We respect your inbox. Unsubscribe anytime.