ESP32 FreeRTOS Tutorial: Multitasking with Tasks, Queues & Semaphores (2026 Complete Guide)

 

ESP32 FreeRTOS Tutorial: Multitasking with Tasks, Queues & Semaphores (2026 Complete Guide)

✍️ — Embedded Systems & IoT Engineer  |  📅 June 28, 2026  |  🕐 14 min read  |  🏷️ ESP32 · FreeRTOS · RTOS · Arduino IDE


If you've ever tried to read a sensor, update an OLED display, and maintain a WiFi/MQTT connection at the same time — all in a single Arduino loop() — you already know the problem. Timing breaks. The display stutters. The MQTT client disconnects. The sensor misses a reading while the display is refreshing.

The fix isn't better timing tricks. It's using the real-time operating system that's already running inside your ESP32 right now. FreeRTOS is built into every copy of ESP-IDF and Arduino Core 3.x, and every ESP32 project you've ever written is already using it — you just haven't talked to it directly yet.

This guide changes that. You'll go from zero to running multiple concurrent tasks with safe data passing and shared-resource protection — with copy-paste code tested on ESP-IDF 5.3 and Arduino Core 3.x.

📌 TL;DR — What You'll Learn
  • Tasks: Create parallel threads with xTaskCreatePinnedToCore()
  • Queues: Pass sensor data safely between tasks with xQueueSend() / xQueueReceive()
  • Semaphores: Synchronize task execution and handle ISR events
  • Mutexes: Protect shared resources like Serial, I2C, or SPI buses
  • Event Groups: Coordinate multiple system flags in one variable

What Is FreeRTOS and Why Is It Already on Your ESP32?

FreeRTOS (Free Real-Time Operating System) is a lightweight RTOS kernel originally written by Richard Barry and now maintained by Amazon Web Services. Espressif forked it for ESP32 as ESP-FreeRTOS, with custom extensions for dual-core operation, WiFi task management, and inter-core communication.

The critical thing to understand: you cannot turn FreeRTOS off on an ESP32. Even a blank Arduino sketch has FreeRTOS running underneath it. Your setup() runs once and your loop() runs inside a task called loopTask pinned to Core 1 at priority 1. The WiFi stack lives in separate tasks on Core 0.

Once you understand this, you stop fighting the system and start using it.

ESP32 FreeRTOS Core Concepts: Quick Reference Table

ConceptWhat It DoesKey FunctionUse When
TaskA function that runs independentlyxTaskCreatePinnedToCore()Any concurrent job
QueueThread-safe FIFO bufferxQueueCreate()Passing data between tasks
Binary SemaphoreOne-shot signal flagxSemaphoreCreateBinary()ISR-to-task signaling
MutexOwnership-based lockxSemaphoreCreateMutex()Protecting shared resources
Counting SemaphoreToken pool counterxSemaphoreCreateCounting()Rate limiting, resource pools
Event Group24 independent bit flagsxEventGroupCreate()Multi-condition coordination

Part 1: Creating Your First ESP32 FreeRTOS Task

A FreeRTOS task is any function with the signature void myTask(void *pvParameters) that never returns — it either runs in an infinite loop or calls vTaskDelete(NULL) when done.

The Two Task Creation Functions

On ESP32 you'll use xTaskCreatePinnedToCore() for almost everything. It's the ESP32-specific version that lets you assign tasks to Core 0 or Core 1:


// ── Example 1: Two tasks running at the same time ─────────────────
// Works in Arduino Core 3.x (paste into your .ino file)

// Task 1: Reads a sensor every 500 ms on Core 1
void sensorTask(void *pvParameters) {
    const TickType_t delay = pdMS_TO_TICKS(500);  // 500 ms

    for (;;) {
        float temperature = readTemperature();  // your sensor function
        Serial.printf("[Core %d] Temp: %.2f °C\n",
                      xPortGetCoreID(), temperature);
        vTaskDelay(delay);  // yield CPU — NEVER use delay() in tasks
    }
}

// Task 2: Blinks an LED every 200 ms on Core 1
void ledTask(void *pvParameters) {
    const int LED_PIN = 2;
    pinMode(LED_PIN, OUTPUT);
    const TickType_t delay = pdMS_TO_TICKS(200);

    for (;;) {
        digitalWrite(LED_PIN, HIGH);
        vTaskDelay(delay);
        digitalWrite(LED_PIN, LOW);
        vTaskDelay(delay);
    }
}

void setup() {
    Serial.begin(115200);

    // xTaskCreatePinnedToCore(function, name, stackSize, param, priority, handle, core)
    xTaskCreatePinnedToCore(
        sensorTask,   // function to run
        "SensorTask", // human-readable name (for debugging)
        4096,         // stack size in bytes — 4 KB is a safe starting point
        NULL,         // parameter to pass (NULL = none)
        1,            // priority (0 = lowest, 25 = highest on ESP32)
        NULL,         // task handle (NULL if you don't need to control it later)
        1             // core: 0 = protocol core, 1 = application core
    );

    xTaskCreatePinnedToCore(
        ledTask,
        "LEDTask",
        2048,   // LED task needs less stack
        NULL,
        1,      // same priority as sensor task
        NULL,
        1
    );

    // Note: In Arduino, you do NOT need to start the scheduler —
    // it's already running. Just create your tasks in setup().
}

void loop() {
    // leave empty — your logic lives in tasks now
    vTaskDelay(pdMS_TO_TICKS(1000));
}

ESP32 Core Assignment Strategy (2026)

CoreDefault UseRecommended ForAvoid
Core 0WiFi + BT stack, system tasksMQTT keepalive, BLE scanning, NTP syncBlocking I2C, heavy computation
Core 1Arduino loop(), user tasksSensors, display updates, control logicLong WiFi operations without yield
💡 2026 Best Practice — In ESP-IDF 5.x, the WiFi stack can safely run tasks on Core 0 alongside user code. Pin time-critical tasks (motor control, pulse counting) to Core 1, and assign network tasks to Core 0. This eliminates WiFi-induced jitter in your sensor reads.

Part 2: FreeRTOS Queues — Safe Data Transfer Between Tasks

The most common FreeRTOS mistake is using a global variable to share data between tasks. Global variables work until they don't — the moment two tasks read/write the same variable without protection, you get corrupted data, silent bugs, and crashes that appear randomly under load.

Queues solve this completely. A queue is a thread-safe FIFO buffer managed by the FreeRTOS kernel. One task writes data in; another task reads it out. The kernel handles all the locking internally.


// ── Example 2: Queue — Sensor task feeds display task ─────────────

#include <Arduino.h>

// Define the data structure to pass through the queue
typedef struct {
    float temperature;
    float humidity;
    uint32_t timestamp;
} SensorData_t;

// Queue handle — global so both tasks can access it
QueueHandle_t sensorQueue;

// PRODUCER: reads sensor, sends to queue
void sensorProducerTask(void *pvParameters) {
    SensorData_t reading;
    const TickType_t period = pdMS_TO_TICKS(2000);  // read every 2 s

    for (;;) {
        reading.temperature = readBME280Temp();    // your sensor function
        reading.humidity    = readBME280Humidity();
        reading.timestamp   = millis();

        // Send to queue — wait up to 100 ms if queue is full
        if (xQueueSend(sensorQueue, &reading, pdMS_TO_TICKS(100)) != pdPASS) {
            Serial.println("[Sensor] Queue full — reading dropped");
        }

        vTaskDelay(period);
    }
}

// CONSUMER: receives data from queue, updates OLED
void displayConsumerTask(void *pvParameters) {
    SensorData_t received;

    for (;;) {
        // Block here until data arrives — portMAX_DELAY = wait forever
        if (xQueueReceive(sensorQueue, &received, portMAX_DELAY) == pdPASS) {
            Serial.printf("[Display] %.1f°C  %.0f%%RH  (t=%lu ms)\n",
                          received.temperature,
                          received.humidity,
                          received.timestamp);
            updateOLED(received);  // your display function
        }
    }
}

void setup() {
    Serial.begin(115200);

    // Create queue: holds up to 5 SensorData_t structs
    sensorQueue = xQueueCreate(5, sizeof(SensorData_t));

    if (sensorQueue == NULL) {
        Serial.println("FATAL: Queue creation failed — out of memory");
        while(1);  // halt
    }

    xTaskCreatePinnedToCore(sensorProducerTask,  "Sensor",  4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(displayConsumerTask, "Display", 4096, NULL, 1, NULL, 1);
}

void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }
⚠️ Key Rule — Inside an ISR (interrupt service routine), you must use the ISR-safe versions: xQueueSendFromISR() and xQueueReceiveFromISR(). Never call the regular versions from an ISR — it will crash the system.

Part 3: Binary Semaphores — Task Synchronization & ISR Signaling

A binary semaphore is a single-bit flag: it's either given (1) or taken (0). It's perfect for situations where one task or interrupt needs to signal another task that something happened — a button press, a GPIO interrupt, a timer event.


// ── Example 3: Binary Semaphore — Button ISR wakes a handler task ──

#include <Arduino.h>

SemaphoreHandle_t buttonSemaphore;

#define BUTTON_PIN 0  // BOOT button on most ESP32 dev boards

// ISR: called on button press — runs in interrupt context
void IRAM_ATTR buttonISR() {
    BaseType_t higherPriorityTaskWoken = pdFALSE;

    // Give the semaphore from ISR — use the ISR-safe version
    xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken);

    // If giving the semaphore unblocked a higher-priority task,
    // request a context switch immediately after the ISR
    portYIELD_FROM_ISR(higherPriorityTaskWoken);
}

// Task: waits for button press signal, then does work
void buttonHandlerTask(void *pvParameters) {
    for (;;) {
        // Block here indefinitely until the semaphore is given
        if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY) == pdTRUE) {
            Serial.println("[Button] Press detected — handling event");
            // Debounce and handle the button event safely here
            // (not in the ISR, where you have stack limitations)
            handleButtonPress();
        }
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    // Create the binary semaphore — starts in "taken" (0) state
    buttonSemaphore = xSemaphoreCreateBinary();

    if (buttonSemaphore == NULL) {
        Serial.println("FATAL: Semaphore creation failed");
        while(1);
    }

    // Attach ISR on falling edge (button pulled low when pressed)
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

    xTaskCreatePinnedToCore(buttonHandlerTask, "BtnHandler", 3072, NULL, 3, NULL, 1);
}

void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }

Part 4: Mutex — Protecting Shared Resources

If more than one task uses Serial, an I2C bus, an SPI device, or any shared variable, you need a mutex. Unlike a binary semaphore, only the task that took the mutex can give it back. This prevents the double-release bugs that plague semaphore misuse.


// ── Example 4: Mutex — two tasks share Serial without corruption ────

SemaphoreHandle_t serialMutex;

void taskA(void *pvParameters) {
    for (;;) {
        // Take the mutex — wait up to 50 ms
        if (xSemaphoreTake(serialMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
            Serial.println("[Task A] Sensor data: 23.5°C");
            xSemaphoreGive(serialMutex);  // MUST give after taking
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void taskB(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(serialMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
            Serial.println("[Task B] Network status: connected");
            xSemaphoreGive(serialMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(1500));
    }
}

void setup() {
    Serial.begin(115200);
    serialMutex = xSemaphoreCreateMutex();

    xTaskCreatePinnedToCore(taskA, "TaskA", 3072, NULL, 1, NULL, 1);
    xTaskCreatePinnedToCore(taskB, "TaskB", 3072, NULL, 1, NULL, 1);
}

void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }
🚫 Never call xSemaphoreTake() from an ISR. Mutexes are not ISR-safe. For ISR-to-task signaling, always use binary semaphores with the FromISR variants.

Part 5: Event Groups — Multi-Flag System Coordination

Event groups let you track up to 24 boolean flags in a single 32-bit variable. They're perfect for system startup sequences, where multiple subsystems need to initialize before the main application runs.


// ── Example 5: Event Groups — Startup coordinator ─────────────────

#include "freertos/event_groups.h"

EventGroupHandle_t startupEvents;

// Define bit flags (each task sets its own bit when ready)
#define WIFI_READY_BIT   (1 << 0)   // Bit 0
#define SENSOR_READY_BIT (1 << 1)   // Bit 1
#define DISPLAY_READY_BIT (1 << 2)  // Bit 2
#define ALL_READY_BITS   (WIFI_READY_BIT | SENSOR_READY_BIT | DISPLAY_READY_BIT)

void wifiInitTask(void *pvParameters) {
    Serial.println("[WiFi] Connecting...");
    WiFi.begin(SSID, PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        vTaskDelay(pdMS_TO_TICKS(250));
    }
    Serial.println("[WiFi] Connected!");
    xEventGroupSetBits(startupEvents, WIFI_READY_BIT);
    vTaskDelete(NULL);  // init done — delete this task
}

void sensorInitTask(void *pvParameters) {
    Serial.println("[Sensor] Initializing BME280...");
    if (!bme.begin(0x76)) {
        Serial.println("[Sensor] FAILED");
    } else {
        Serial.println("[Sensor] Ready");
    }
    xEventGroupSetBits(startupEvents, SENSOR_READY_BIT);
    vTaskDelete(NULL);
}

void mainApplicationTask(void *pvParameters) {
    Serial.println("[Main] Waiting for all subsystems...");

    // Wait until ALL three bits are set — or until 10 s timeout
    EventBits_t bits = xEventGroupWaitBits(
        startupEvents,
        ALL_READY_BITS,  // wait for these bits
        pdFALSE,         // don't clear bits on exit
        pdTRUE,          // wait for ALL bits (pdFALSE = wait for ANY)
        pdMS_TO_TICKS(10000)  // 10 s timeout
    );

    if ((bits & ALL_READY_BITS) == ALL_READY_BITS) {
        Serial.println("[Main] All systems ready — starting main loop");
    } else {
        Serial.println("[Main] Timeout — some systems failed to initialize");
    }

    // Main loop runs here...
    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void setup() {
    Serial.begin(115200);
    startupEvents = xEventGroupCreate();

    xTaskCreatePinnedToCore(wifiInitTask,    "WiFiInit",   4096, NULL, 2, NULL, 0);
    xTaskCreatePinnedToCore(sensorInitTask,  "SensorInit", 3072, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(mainApplicationTask, "Main",   6144, NULL, 1, NULL, 1);
}

void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }

Full Project: Multi-Sensor IoT Node with FreeRTOS (Production Template)

This template is what a real battery-powered IoT sensor node looks like in 2026 using FreeRTOS properly — four tasks, a queue, a mutex, and a semaphore working together. Pair it with the ESP32 Sleep Modes guide to add deep sleep between readings.


// ── Production Template: 4-Task IoT Sensor Node ───────────────────
// Tasks: Sensor Read → MQTT Publish → WiFi Watchdog → LED Status
// Inter-task: Queue (sensor→MQTT) + Mutex (Serial) + Semaphore (MQTT sync)

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>

// ── Configuration ─────────────────────────────
#define WIFI_SSID     "YOUR_SSID"
#define WIFI_PASS     "YOUR_PASSWORD"
#define MQTT_BROKER   "192.168.1.100"
#define MQTT_PORT     1883
#define MQTT_TOPIC    "iot/node01/sensors"
#define STATUS_LED    2
#define SENSOR_PERIOD pdMS_TO_TICKS(10000)  // 10 s between readings

// ── Shared Handles ────────────────────────────
QueueHandle_t     mqttQueue;       // sensor → mqtt task
SemaphoreHandle_t serialMutex;     // protect Serial output
SemaphoreHandle_t mqttReadySem;    // signal: mqtt connected

typedef struct {
    float temperature;
    float humidity;
    uint32_t timestamp;
} SensorPayload_t;

// Helper: thread-safe Serial print
void safePrint(const char* msg) {
    if (xSemaphoreTake(serialMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
        Serial.print(msg);
        xSemaphoreGive(serialMutex);
    }
}

// ── Task 1: Sensor Read (Core 1, Priority 2) ──
void sensorReadTask(void *pv) {
    SensorPayload_t data;
    for (;;) {
        data.temperature = 23.5 + (random(-50, 50) / 100.0f);  // replace with real sensor
        data.humidity    = 60.0f;
        data.timestamp   = millis();
        xQueueSend(mqttQueue, &data, pdMS_TO_TICKS(200));
        safePrint("[Sensor] Reading sent to queue\n");
        vTaskDelay(SENSOR_PERIOD);
    }
}

// ── Task 2: MQTT Publish (Core 0, Priority 2) ─
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);

void mqttPublishTask(void *pv) {
    SensorPayload_t payload;
    char jsonBuf[128];

    mqttClient.setServer(MQTT_BROKER, MQTT_PORT);

    for (;;) {
        // Reconnect if needed
        while (!mqttClient.connected()) {
            safePrint("[MQTT] Connecting...\n");
            if (mqttClient.connect("ESP32Node01")) {
                safePrint("[MQTT] Connected\n");
                xSemaphoreGive(mqttReadySem);
            } else {
                vTaskDelay(pdMS_TO_TICKS(3000));
            }
        }
        mqttClient.loop();

        // Publish any queued sensor data
        if (xQueueReceive(mqttQueue, &payload, pdMS_TO_TICKS(100)) == pdPASS) {
            snprintf(jsonBuf, sizeof(jsonBuf),
                     "{\"t\":%.2f,\"h\":%.2f,\"ts\":%lu}",
                     payload.temperature, payload.humidity, payload.timestamp);
            mqttClient.publish(MQTT_TOPIC, jsonBuf);
            safePrint("[MQTT] Published\n");
        }

        vTaskDelay(pdMS_TO_TICKS(50));  // small yield
    }
}

// ── Task 3: WiFi Watchdog (Core 0, Priority 3) ─
void wifiWatchdogTask(void *pv) {
    for (;;) {
        if (WiFi.status() != WL_CONNECTED) {
            safePrint("[WiFi] Lost connection — reconnecting\n");
            WiFi.disconnect();
            WiFi.begin(WIFI_SSID, WIFI_PASS);
            int retries = 0;
            while (WiFi.status() != WL_CONNECTED && retries < 20) {
                vTaskDelay(pdMS_TO_TICKS(500));
                retries++;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(15000));  // check every 15 s
    }
}

// ── Task 4: LED Status (Core 1, Priority 1) ───
void ledStatusTask(void *pv) {
    pinMode(STATUS_LED, OUTPUT);
    for (;;) {
        bool connected = (WiFi.status() == WL_CONNECTED && mqttClient.connected());
        if (connected) {
            // Slow blink = connected
            digitalWrite(STATUS_LED, HIGH); vTaskDelay(pdMS_TO_TICKS(100));
            digitalWrite(STATUS_LED, LOW);  vTaskDelay(pdMS_TO_TICKS(1900));
        } else {
            // Fast blink = disconnected
            digitalWrite(STATUS_LED, HIGH); vTaskDelay(pdMS_TO_TICKS(100));
            digitalWrite(STATUS_LED, LOW);  vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
}

void setup() {
    Serial.begin(115200);

    // Create synchronization primitives
    mqttQueue    = xQueueCreate(10, sizeof(SensorPayload_t));
    serialMutex  = xSemaphoreCreateMutex();
    mqttReadySem = xSemaphoreCreateBinary();

    // Connect WiFi before starting tasks
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) delay(300);
    Serial.println("[Setup] WiFi connected: " + WiFi.localIP().toString());

    // Create tasks
    xTaskCreatePinnedToCore(sensorReadTask,    "Sensor",   4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(mqttPublishTask,   "MQTT",     6144, NULL, 2, NULL, 0);
    xTaskCreatePinnedToCore(wifiWatchdogTask,  "WiFiWdog", 3072, NULL, 3, NULL, 0);
    xTaskCreatePinnedToCore(ledStatusTask,     "LED",      2048, NULL, 1, NULL, 1);

    Serial.println("[Setup] All tasks created");
}

void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }
📎 Related Guide: This production template pairs perfectly with ESP32 OTA Update Tutorial — add OTA capability to any of these tasks so you can update firmware wirelessly without reflashing.

7 Common ESP32 FreeRTOS Mistakes (And Exact Fixes)

1. Using delay() Instead of vTaskDelay()

delay() blocks the CPU and prevents the FreeRTOS scheduler from running other tasks. vTaskDelay() suspends only the current task and lets others run. Rule: never use delay() inside a FreeRTOS task.


❌  delay(1000);                          // blocks entire scheduler
✅  vTaskDelay(pdMS_TO_TICKS(1000));      // suspends only this task

2. Stack Overflow

The #1 cause of random ESP32 reboots with FreeRTOS. When a task's stack fills up, the kernel triggers a fatal error. Use the diagnostic function to measure real stack usage:


// Add this in your task to check stack health
UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Stack free (min ever): %u bytes\n", highWaterMark * 4);
// If this is under 256 bytes, increase your stack size

3. Calling Blocking Functions from ISRs

Never call xQueueSend(), xSemaphoreTake(), Serial.print(), or any blocking function inside an ISR. Always use the FromISR variants and offload work to a handler task via semaphore or queue.

4. Creating Tasks Before Initializing Shared Resources

Create your queues, mutexes, and semaphores before you call xTaskCreatePinnedToCore(). Tasks start immediately and may try to use the shared resource before it's created.

5. Forgetting to Feed the Watchdog Timer

The ESP32 hardware watchdog resets the chip if a task monopolizes the CPU for too long. Any task running more than ~5 seconds without a context switch triggers this. Always include a vTaskDelay() even if the delay is just 1 ms — it yields to the scheduler and pets the watchdog.

6. Using the Same Priority for All Tasks

Equal-priority tasks run in round-robin. This is fine for most tasks, but time-critical operations (interrupt handlers, watchdogs) should run at a higher priority. Suggested hierarchy:

  • Priority 5: Watchdog tasks
  • Priority 3: WiFi reconnect, real-time control
  • Priority 2: Sensor reading, MQTT publishing
  • Priority 1: Display updates, LED status
  • Priority 0: Idle tasks only (system use)

7. Not Null-Checking Handle Returns

xQueueCreate(), xSemaphoreCreateMutex(), and xTaskCreatePinnedToCore() all return NULL if the ESP32 runs out of heap memory. Always check the return value or you'll get a hard-to-debug null pointer crash later.

FreeRTOS Memory: Stack vs Heap on ESP32

ItemTypical SizeWhereTune With
Task stack (simple task)2048 – 4096 bytesHeap3rd param of xTaskCreate
Task stack (WiFi + JSON)6144 – 8192 bytesHeap3rd param of xTaskCreate
Queue buffer (10 × struct)~200 – 2000 bytesHeapxQueueCreate length param
Total heap available~250 KB (post-WiFi)SRAMESP.getFreeHeap()

Monitor heap health in production by periodically calling Serial.printf("Free heap: %u\n", ESP.getFreeHeap());. If you see this number decreasing over time, you have a memory leak — typically a task or queue that's created but never deleted.

📎 Next Step: Once your FreeRTOS architecture is solid, reduce power by adding Deep Sleep between sensor reads or securing all data in transit with TLS/SSL on your MQTT connection.

Frequently Asked Questions

Does ESP32 have FreeRTOS built in?

Yes. FreeRTOS is built directly into the ESP32 SDK (ESP-IDF). When you write an Arduino sketch for ESP32, FreeRTOS is already running underneath — your setup() and loop() functions each run as separate FreeRTOS tasks on Core 1 by default. You can create additional tasks on top of this at any time.

What is the maximum number of tasks in ESP32 FreeRTOS?

There is no hard-coded task limit. The practical ceiling is set by available RAM. Each task requires stack memory (minimum 1024 bytes, typically 2048–8192 bytes). With 520 KB of internal SRAM on the original ESP32, you can realistically run 20–40 concurrent tasks depending on stack sizes, after accounting for the WiFi stack and system tasks.

What is the difference between a semaphore and a mutex in ESP32 FreeRTOS?

binary semaphore is used for signaling — one task or ISR gives it, and another task takes it. Any task can give it or take it. A mutex is used for ownership — only the task that took the mutex can give it back. This prevents a second task from accidentally releasing a lock it doesn't own. Rule of thumb: semaphores for events and synchronization; mutexes for protecting shared data (Serial, I2C, SPI).

How do I pin an ESP32 FreeRTOS task to a specific core?

Use xTaskCreatePinnedToCore() and set the last parameter: 0 for Core 0 (WiFi/BT protocol core), 1 for Core 1 (application core). Example: xTaskCreatePinnedToCore(myTask, "MyTask", 4096, NULL, 1, NULL, 1); pins to Core 1. If you use xTaskCreate() instead, FreeRTOS schedules the task on whichever core is less busy.

What causes a stack overflow in ESP32 FreeRTOS and how do I fix it?

A stack overflow occurs when a task uses more stack memory than allocated during xTaskCreatePinnedToCore(). Deep function call chains, large local arrays, and recursive functions are the common causes. Fix: run uxTaskGetStackHighWaterMark(NULL) inside the task to see the minimum free stack bytes ever recorded. Set your stack size to that value × 4 bytes, plus a 20% safety margin.

Conclusion: When to Use Which FreeRTOS Primitive

FreeRTOS on ESP32 is not an advanced feature — it's the foundation. Every real IoT product you encounter runs an RTOS, because single-threaded code cannot reliably handle WiFi, sensors, displays, and user input simultaneously.

The decision tree is simple:

  • Need to run two things at once → Task
  • Need to pass data between tasks → Queue
  • Need to signal from an ISR to a task → Binary Semaphore
  • Need to protect a shared bus or resource → Mutex
  • Need to wait for multiple conditions → Event Group

Start with the 4-task production template above, measure your stack watermarks after a day of runtime, and tune from there. Your IoT project stops glitching the moment you give each job its own task.

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.