The direct answer for most makers hitting the workbench: the default priority for the Arduino loop() function on the ESP32 is 1. The FreeRTOS idle task runs at priority 0, while background system tasks (like the Wi-Fi and Bluetooth stacks) run at much higher priorities, typically 23 for the Wi-Fi task. If your custom tasks starve the loop() or fail to yield CPU time, you will inevitably hit task watchdog resets and dropped network connections.
Understanding ESP32 Arduino Default Task Priority
Under the hood of the Arduino ESP32 core lies FreeRTOS, a real-time operating system that manages the dual-core Xtensa LX6 processors. When you write standard Arduino code, the framework automatically creates a task called loopTask that runs your setup() and loop() functions.
By default, this loopTask is pinned to Core 1 (the APP_CPU) and assigned a priority of 1. FreeRTOS on the ESP32 is configured with configMAX_PRIORITIES set to 25. This gives you a range of 0 to 24 to work with. Here is how the default system priorities stack up:
- Priority 0: FreeRTOS Idle Task (runs when nothing else needs CPU time; handles power management).
- Priority 1: Arduino
loopTask(Core 1). - Priority 2-22: Available for your custom user tasks.
- Priority 23: Wi-Fi and Bluetooth system tasks (Core 0).
- Priority 24: RTOS Timer service task.
Parts List and Pin Mapping for Multi-Task Demo
To demonstrate proper priority assignment and core pinning, we will build a dual-task environmental monitor. One task will read a sensor on Core 0, while a heartbeat LED blinks on Core 1.
Required Hardware
| Component | Exact Variant / Model | Notes |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (30-pin) | Target board for this guide. Dual-core, 520KB SRAM. |
| Sensor | Adafruit BME280 I2C Breakout | Measures temp, humidity, pressure. Default I2C addr: 0x77 or 0x76. |
| Status LED | 5mm Green LED with 330Ω resistor | Heartbeat indicator for Core 1. |
| Alert LED | 5mm Red LED with 330Ω resistor | High-temp warning indicator driven by Core 0. |
Pin Mapping Table
| Function | ESP32 GPIO | Connected To |
|---|---|---|
| I2C SDA | GPIO 21 | BME280 SDA |
| I2C SCL | GPIO 22 | BME280 SCL |
| Status LED | GPIO 2 | Green LED Anode (via 330Ω) |
| Alert LED | GPIO 4 | Red LED Anode (via 330Ω) |
Complete Multi-Task Code with Priority Assignment
This code targets the ESP32-WROOM-32 DevKit V1. It explicitly creates two tasks using xTaskCreatePinnedToCore, assigns them specific priorities, and includes error handling for both I2C initialization and task creation failures.
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- Pin Definitions ---
#define PIN_STATUS_LED 2
#define PIN_ALERT_LED 4
#define I2C_SDA 21
#define I2C_SCL 22
// --- Task Handles ---
TaskHandle_t SensorTaskHandle = NULL;
TaskHandle_t HeartbeatTaskHandle = NULL;
// --- Sensor Object ---
Adafruit_BME280 bme;
// --- Core 0 Task: Sensor Reading (Priority 2) ---
void SensorTask(void *pvParameters) {
for(;;) {
float temp = bme.readTemperature();
// Error handling: BME280 returns NAN on I2C failure
if (isnan(temp)) {
Serial.println("[SensorTask] I2C Read Error!");
} else if (temp > 35.0) {
digitalWrite(PIN_ALERT_LED, HIGH);
} else {
digitalWrite(PIN_ALERT_LED, LOW);
}
// CRITICAL: Yield to lower priority tasks and reset watchdog
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// --- Core 1 Task: Heartbeat (Priority 1) ---
void HeartbeatTask(void *pvParameters) {
for(;;) {
digitalWrite(PIN_STATUS_LED, !digitalRead(PIN_STATUS_LED));
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
pinMode(PIN_STATUS_LED, OUTPUT);
pinMode(PIN_ALERT_LED, OUTPUT);
// Initialize I2C with explicit pins
Wire.begin(I2C_SDA, I2C_SCL);
if (!bme.begin(0x76)) { // Try 0x76, fallback to 0x77 if needed in production
Serial.println("[Setup] BME280 init failed. Check wiring. Halting.");
while(1) {
digitalWrite(PIN_ALERT_LED, HIGH); // Solid red indicates fatal init error
vTaskDelay(1000);
}
}
BaseType_t xReturned;
// Create Sensor Task: Stack 4096, Priority 2, Core 0
xReturned = xTaskCreatePinnedToCore(
SensorTask, "SensorTask", 4096, NULL, 2, &SensorTaskHandle, 0
);
if (xReturned != pdPASS) {
Serial.println("[Setup] Failed to create SensorTask. Out of heap?");
}
// Create Heartbeat Task: Stack 2048, Priority 1, Core 1
xReturned = xTaskCreatePinnedToCore(
HeartbeatTask, "HeartbeatTask", 2048, NULL, 1, &HeartbeatTaskHandle, 1
);
if (xReturned != pdPASS) {
Serial.println("[Setup] Failed to create HeartbeatTask.");
}
}
void loop() {
// The default Arduino loopTask runs at Priority 1 on Core 1.
// We use it here just for low-priority telemetry printing.
Serial.printf("[LoopTask] Free heap: %u bytes\n", ESP.getFreeHeap());
// Must delay to prevent starving other Priority 1 tasks on Core 1
vTaskDelay(pdMS_TO_TICKS(2000));
}
Debugging: "Task Watchdog Got Triggered" Error
When you mismanage task priorities or forget to yield, the ESP32's Task Watchdog Timer (TWDT) will step in and reboot the chip. You will see this exact error string in your serial monitor:
E (5432) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
Task: IDLE (core 0)
CPU 0: IDLE task (current state: blocked)
CPU 1: loopTask (current state: running)
If you encounter this crash, here are the first three things to check:
- Missing Yield/Delay in High-Priority Loops: Did you write a
while(1)orfor(;;)loop withoutvTaskDelay(),yield(), ordelay()? A task with priority 2 or higher will completely monopolize the core, starving the priority 0IDLEtask. The IDLE task is responsible for resetting the hardware watchdog; if it starves, the chip reboots. - Blocking I2C/SPI Calls Without Timeouts: The default Arduino
Wire.hlibrary can hang indefinitely if a sensor fails to ACK on the I2C bus. If your sensor task hangs insideWire.requestFrom(), it stops yielding. Always ensure your hardware pull-ups are correct, or use an I2C wrapper that implements bus timeouts. - Priority Inversion via Mutexes: If a low-priority task (Priority 1) holds a mutex lock, and a high-priority task (Priority 5) blocks waiting for that same mutex, a medium-priority task (Priority 3) can preempt the low-priority task. The low-priority task never releases the lock, the high-priority task waits forever, and the system deadlocks. Use
xSemaphoreCreateMutex()which includes priority inheritance to fix this.
For deeper architectural insights into how the ESP32 handles task scheduling and interrupts, refer to the official Espressif FreeRTOS Documentation. If you are modifying the core Arduino environment itself, the Arduino ESP32 Core GitHub repository is the definitive source for how main.cpp spawns the loopTask.
Extending and Simplifying Your ESP32 Build
Once you have stable tasks running without watchdog resets, you need to manage how they share data.
How to Extend: Use FreeRTOS Queues
Beginners often use global variables protected by mutexes to pass sensor data from Core 0 to Core 1. This is prone to race conditions. To extend your build safely, implement a FreeRTOS Queue. Create a queue in setup() using xQueueCreate(10, sizeof(float)). Your SensorTask pushes temperature readings using xQueueSend(), and your loopTask or a display task reads them using xQueueReceive(). This decouples the tasks and prevents lockups.
How to Simplify: Drop Back to the Arduino Loop
Multithreading introduces complexity. If your project only blinks LEDs, reads a sensor every 5 seconds, and hosts a basic web server, you likely do not need custom FreeRTOS tasks. Simplify your build by deleting xTaskCreatePinnedToCore entirely. Rely on the default loop() (Priority 1) and use non-blocking millis() state machines. The background Wi-Fi task (Priority 23) will automatically preempt your loop() when it needs to handle network traffic, keeping your connection stable without manual task management.
FAQ: ESP32 Task Priority Deep Dive
What happens if two ESP32 Arduino tasks have the same priority?
If two tasks on the same core share the same priority (e.g., both are Priority 1 on Core 1), FreeRTOS uses round-robin time slicing. The scheduler will alternate between them, giving each task a few milliseconds of CPU time before swapping context. However, this only works if both tasks yield control (via vTaskDelay or queue waits). If one task enters a tight calculation loop without yielding, it will consume its entire time slice repeatedly, starving the other task.
Can I change the default priority of the Arduino loop() task?
Yes, but it is rarely recommended. You can change it dynamically inside setup() by calling vTaskPrioritySet(NULL, 5); (passing NULL targets the currently running task). If you raise it above 1, your loop() will preempt other Priority 1 tasks. If you raise it above 23, you will break Wi-Fi. If you lower it to 0, it will only run when the system is completely idle, meaning your serial prints and sensor polls will become highly erratic.
Why does my ESP32 Wi-Fi drop when I run a high-priority task?
The Wi-Fi and Bluetooth stacks run as background tasks on Core 0 (PRO_CPU) at priority 23. If you pin a custom task to Core 0 at priority 24, or if you pin a task to Core 0 at priority 23 but forget to include vTaskDelay(), your custom task will starve the Wi-Fi task. The Wi-Fi stack will miss critical MAC-layer timing windows, causing the router to drop the ESP32's connection. Always leave Core 0 mostly free for system radios, or ensure your Core 0 tasks yield frequently.
How do I check the current stack high water mark for my ESP32 tasks?
Allocating too much stack memory wastes SRAM; allocating too little causes silent memory corruption and crashes. You can check the minimum amount of free stack space that has ever existed since the task started by calling uxTaskGetStackHighWaterMark(TaskHandle). If this function returns a low number (e.g., under 200 bytes), your task is dangerously close to a stack overflow, and you need to increase the stack size parameter in your xTaskCreatePinnedToCore call.






