When makers search for "FreeRTOS Arduino," they are almost always looking for how to run real-time operating system tasks on the ESP32 using the Arduino IDE. Native 8-bit AVR boards like the Arduino Uno lack the RAM and flash to run a practical RTOS. The ESP32, however, runs FreeRTOS natively under the hood of its Arduino core, exposing dual-core multitasking, queues, and semaphores directly through standard Arduino sketches.
This guide walks through building a robust dual-core sensor polling and WiFi-ready architecture. We will pin a BME280 environmental sensor read to Core 0, leaving Core 1 free for WiFi/MQTT stack operations, and thoroughly debug the most common crashes you will encounter.
Project Spec Sheet & Parts List
To replicate this build exactly, source the following specific variants. Using clone boards with different GPIO strapping pin behaviors (like the 30-pin ESP32-S variants) may require adjusting the I2C pin definitions in the code.
| Component | Exact Variant / Model | Estimated Cost (2026) |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (38-pin, Type-C or Micro-USB) | $6.00 - $8.50 |
| Sensor | Adafruit BME280 I2C / SPI (STEMMA QT / Qwiic variant, Product ID 2652) | $19.95 |
| Wiring | 28 AWG silicone stranded wire, 4-pin JST-SH STEMMA QT cable | $5.00 |
| Pull-up Resistors | 4.7kΩ 1/4W metal film (Only if using raw breakout, Adafruit board has onboard 10kΩ) | $0.10 |
Pin Mapping & Hardware Wiring
The ESP32-WROOM-32 DevKit V1 exposes default I2C pins on GPIO 21 (SDA) and GPIO 22 (SCL). While the Arduino Wire library allows software remapping of these pins, sticking to the hardware defaults ensures the most stable I2C bus timing, which is critical when multiple FreeRTOS tasks are competing for bus access.
| BME280 STEMMA QT Pin | ESP32 DevKit V1 Pin | Notes |
|---|---|---|
| VIN (or 3Vo) | 3V3 | Do NOT use 5V; the BME280 logic level is strictly 3.3V. |
| GND | GND | Ensure a solid common ground to prevent I2C ACK failures. |
| SDA | GPIO 21 | Default I2C Data line on ESP32-WROOM-32. |
| SCL | GPIO 22 | Default I2C Clock line on ESP32-WROOM-32. |
Complete FreeRTOS Arduino Code (ESP32 Dual-Core)
This code targets the ESP32 Dev Module board selection in the Arduino IDE (ensure ESP32 core v2.0.14 or v3.x is installed). It creates a dedicated task pinned to Core 0 for sensor reading, utilizing xTaskCreatePinnedToCore. Core 1 is left to handle the Arduino loop() and background WiFi/RTOS daemons.
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- Pin Definitions ---
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define LED_STATUS_PIN 2 // Built-in LED on most DevKit V1 boards
// --- Task Handles & Parameters ---
TaskHandle_t SensorTaskHandle = NULL;
const int SENSOR_TASK_STACK_SIZE = 4096;
const int SENSOR_TASK_PRIORITY = 1;
const int SENSOR_TASK_CORE = 0;
Adafruit_BME280 bme;
// --- Sensor Task Function (Pinned to Core 0) ---
void sensorReadTask(void *pvParameters) {
// Initialize I2C inside the task to ensure it runs on Core 0's context
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
if (!bme.begin(0x77, &Wire)) {
Serial.println("[ERROR] Could not find a valid BME280 sensor, check wiring!");
// Halt task gracefully if hardware fails
vTaskDelete(NULL);
return;
}
Serial.println("[INFO] BME280 initialized successfully on Core 0.");
for (;;) {
float temp = bme.readTemperature();
float humidity = bme.readHumidity();
Serial.printf("[Core %d] Temp: %.2f C | Humidity: %.2f %%\n",
xPortGetCoreID(), temp, humidity);
// Toggle LED to show task is alive
digitalWrite(LED_STATUS_PIN, !digitalRead(LED_STATUS_PIN));
// Check stack high water mark every 10 cycles
UBaseType_t highWater = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("[DEBUG] Sensor Task Stack High Water Mark: %u bytes\n", highWater);
// CRITICAL: Yield to prevent Task Watchdog Trigger
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("\n--- FreeRTOS Dual-Core ESP32 Boot ---");
pinMode(LED_STATUS_PIN, OUTPUT);
// Create Task pinned to Core 0
BaseType_t xReturned = xTaskCreatePinnedToCore(
sensorReadTask, // Function to implement the task
"SensorReadTask", // Name of the task
SENSOR_TASK_STACK_SIZE, // Stack size in bytes
NULL, // Task input parameter
SENSOR_TASK_PRIORITY, // Priority of the task
&SensorTaskHandle, // Task handle
SENSOR_TASK_CORE // Core where the task should run
);
if (xReturned != pdPASS) {
Serial.println("[FATAL] Failed to create Sensor Task. Insufficient heap?");
ESP.restart();
}
}
void loop() {
// Core 1 handles this loop and background WiFi/BT stacks.
// Always include a delay in loop() to feed the Core 1 IDLE task.
vTaskDelay(pdMS_TO_TICKS(1000));
}
Debugging: First Three Things to Check When It Fails
When migrating from bare-metal Arduino to FreeRTOS on the ESP32, you will inevitably hit a crash. Here is exactly how to diagnose the two most common fatal errors.
1. The Task Watchdog Error
Exact Error String: E (12345) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
Ranked Causes & Fixes:
- Missing
vTaskDelay()oryield(): FreeRTOS relies on the IDLE task to reset the hardware watchdog. If your task runs an infinitewhile(1)loop without yielding, the IDLE task never runs. Fix: AddvTaskDelay(1)inside your loop. - Blocking I2C/SPI operations: A locked-up I2C bus (due to missing pull-ups) will cause
Wire.requestFrom()to block indefinitely. Fix: Set I2C timeout usingWire.setWireTimeout(50000, true)(50ms timeout). - Core 1 Loop Hogging: Forgetting to put a delay in the main Arduino
loop()function starves the Core 1 background tasks. Fix: Always putvTaskDelay(pdMS_TO_TICKS(10))at the end ofloop().
2. The Guru Meditation Panic
Exact Error String: Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1).
Ranked Causes & Fixes:
- Interrupt Service Routine (ISR) taking too long: If you have an interrupt attached to a pin, and the ISR executes
delay()or prints to Serial, it will trigger the Interrupt WDT. Fix: Keep ISRs under 50 microseconds. UsexQueueSendFromISRto pass data to a task. - Stack Overflow: If your task allocates large local arrays (e.g.,
char buffer[2048]), it overflows the allocated FreeRTOS stack, corrupting memory and causing a panic. Fix: IncreaseSENSOR_TASK_STACK_SIZEor move large buffers to the heap usingmallocorps_malloc.
Extending and Simplifying the Build
How to Simplify: If you do not need strict dual-core isolation, delete the xTaskCreatePinnedToCore block entirely. Move the BME280 initialization into setup() and the reading logic into the standard Arduino loop(). The ESP32 Arduino core still runs FreeRTOS in the background to manage WiFi, but your code will behave like a standard single-threaded sketch.
How to Extend: To pass data safely from Core 0 (Sensor) to Core 1 (WiFi MQTT publish), implement a FreeRTOS Queue.
Add QueueHandle_t sensorQueue = xQueueCreate(5, sizeof(float)); in your global scope. Inside the sensor task, replace the Serial print with xQueueSend(sensorQueue, &temp, pdMS_TO_TICKS(10));. In your loop() on Core 1, use xQueueReceive() to pull the temperature and publish it via MQTT without ever blocking the sensor polling rate.
For comprehensive API references, consult the official Espressif ESP-IDF FreeRTOS documentation and the FreeRTOS reference manual.
FreeRTOS Arduino FAQ
Can I run FreeRTOS on an Arduino Uno or Nano?
Technically yes, using third-party libraries like FreeRTOS-Arduino, but practically no. The ATmega328P has only 2KB of SRAM. A single FreeRTOS task requires a minimum stack allocation (usually 128-256 bytes just for context switching), leaving almost no memory for your actual application logic, strings, or buffers. For RTOS capabilities, upgrade to an ESP32, Raspberry Pi Pico (RP2040), or Teensy 4.0.
How do I check FreeRTOS stack high water marks on ESP32?
Use the uxTaskGetStackHighWaterMark(TaskHandle_t xTask) function. Passing NULL returns the water mark for the currently executing task. The returned value is the number of unused bytes remaining in the stack. If this number drops below 200 bytes during operation, your task is at high risk of a stack overflow crash and you must increase the stack size parameter in xTaskCreate.
Why does my ESP32 reboot with a Guru Meditation Error in FreeRTOS?
A Guru Meditation Error is the ESP32's hardware exception handler catching a fatal fault. In FreeRTOS contexts, this is almost always caused by a stack overflow (writing past the allocated task memory), an illegal memory access (dereferencing a null pointer), or an Interrupt Watchdog timeout (an ISR taking longer than 300ms). Check your serial monitor backtrace and use the ESP32 Exception Decoder tool to map the hex addresses to your specific lines of code.
Does enabling WiFi on the ESP32 interfere with my FreeRTOS tasks?
Yes, if not managed correctly. The ESP32 WiFi and Bluetooth stacks run as high-priority background FreeRTOS tasks, primarily on Core 1. If you pin a CPU-intensive task to Core 1 without yielding, you will starve the WiFi stack, leading to dropped connections and Task watchdog errors. Always pin heavy sensor or DSP tasks to Core 0, and leave Core 1 for the loop() and network operations.






