Dual-Core ESP32: FreeRTOS Tasks and Core Pinning Explained
ESP32 Is Already Running FreeRTOS
Even simple Arduino sketches that look single-threaded are actually running on top of FreeRTOS (a real-time operating system) under the hood — your loop() function is itself just one FreeRTOS task among several the Arduino core sets up automatically (WiFi handling runs as its own task, for instance). Understanding this unlocks genuinely useful capability once you need more than the default single-loop model.
Why Use Both Cores Explicitly
ESP32 has two physical CPU cores. By default, Arduino's setup()/loop() runs on Core 1, with Core 0 handling WiFi/Bluetooth radio stack internally. For projects doing genuinely parallel work — reading sensors continuously while also handling network communication without either blocking the other — explicitly creating a second task pinned to the other core prevents one slow operation from stalling the other.
Creating a Task on a Specific Core
void sensorTask(void* parameter) { while (true) { float reading = readSensor(); // process/store reading vTaskDelay(100 / portTICK_PERIOD_MS); // yield — critical, see below } } void setup() { xTaskCreatePinnedToCore( sensorTask, // function to run "SensorTask", // name (for debugging) 4096, // stack size in bytes NULL, // parameter to pass in 1, // priority NULL, // task handle (optional, for later control) 0 // core to pin to (0 or 1) ); } void loop() { // this still runs on Core 1, independently of sensorTask on Core 0 handleNetworking(); }The vTaskDelay Requirement
Every task must yield periodically via vTaskDelay (or another blocking FreeRTOS call) — a task that spins forever without yielding starves other tasks on the same core and, at the highest priority levels, can trigger the watchdog timer to reset the whole device. This is the most common bug when people first start writing custom FreeRTOS tasks — a tight while(true) loop with no delay looks fine in testing but causes mysterious resets under real conditions.
Communicating Between Tasks Safely
Don't just share global variables carelessly between tasks running on different cores — use a proper FreeRTOS queue for passing data safely:
QueueHandle_t sensorQueue; void setup() { sensorQueue = xQueueCreate(10, sizeof(float)); xTaskCreatePinnedToCore(sensorTask, "Sensor", 4096, NULL, 1, NULL, 0); } void sensorTask(void* parameter) { while (true) { float reading = readSensor(); xQueueSend(sensorQueue, &reading, portMAX_DELAY); vTaskDelay(100 / portTICK_PERIOD_MS); } } void loop() { float reading; if (xQueueReceive(sensorQueue, &reading, 0) == pdTRUE) { // safely process the reading here on Core 1 } }When This Actually Matters
Most simple ESP32 projects genuinely don't need explicit multi-core task management — the default single-loop model with occasional delay() calls is fine for the majority of sensor/automation projects in this guide series. Reach for explicit FreeRTOS tasks when you have a real concurrency requirement: continuous high-frequency sensor sampling that can't tolerate gaps, combined with network operations that would otherwise introduce those gaps if run sequentially in a single loop.
Priority Levels
Higher priority numbers preempt lower ones — be conservative with high priorities (reserve them for genuinely time-critical work) since a misbehaving high-priority task can starve everything else, including the system's own internal tasks (WiFi, watchdog), leading to instability that's hard to diagnose without understanding this scheduling model.
Related Guides
- ESP32 I2S Audio: Playing and Recording Sound with a MAX98357A DAC and INMP441 Microphone
- Watchdog Timers for Arduino and ESP32: Hardware WDT, Task Watchdogs, and Recovering from Hangs
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers
- How to Hack Wi-Fi and Bluetooth with the Flipper Zero and Wi-Fi Marauder
- Correcting Beam Astigmatism in High-Power Diodes