ESP32/System/77. Dual core tasks
Your chip
Your language
System · 77 of 81

Dual core tasks

Two cores do not make a slow loop fast. What they do is stop one job blocking another - which matters because Wi-Fi lives on core 0 and is bursty, and anything with timing in it wants to be somewhere else.

/esp32/dual-core-tasks · arduino · S3

Which core, and what happens if you pick wrong

xTaskCreatePinnedToCore(..., 1)
±0.4 ms
Pin your task to
Work per pass30 ms
Timing wanders by
±0.4 ms
Watchdog
fed
Loop rate
33 Hz
Core 1, yielding once a pass — the arrangement that behaves. Two things to keep in mind: each task gets its own stack you must size yourself (4096 bytes is a common starting point, and overflowing it crashes in a way that looks random), and any variable shared with loop() needs a queue or a mutex, not just volatile.

The rules, short

  • Wi-Fi and Bluetooth live on core 0. loop() runs on core 1.
  • Pin timing-sensitive work to core 1. Sampling, stepping, LEDs, audio.
  • Every task must yield. vTaskDelay(1) at minimum, once per pass.
  • Share through a queue. Not through a global, however volatile it is.
  • Size the stack. 4096 bytes is a start; overflowing it crashes somewhere else entirely.

When not to bother

If your loop() is a state machine that never blocks, you do not need tasks. Most "I need the second core" problems are really a delay() that should have been a millis() comparison, and a second core will not fix a design that blocks — it will just move the blockage.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

A sampling task on core 1 and a queue back to loop. The queue is the part that matters - two cores sharing a variable without one is a race, and races on this chip look like corrupt readings rather than crashes.

pinned_task.ino
QueueHandle_t q;

void sampler(void *arg) {
  for (;;) {
    int v = analogRead(34);
    xQueueSend(q, &v, 0);          // never blocks, drops if full
    vTaskDelay(pdMS_TO_TICKS(10)); // yields - this is not delay()
  }
}

void setup() {
  Serial.begin(115200);
  q = xQueueCreate(64, sizeof(int));

  xTaskCreatePinnedToCore(
    sampler, "sampler",
    4096,        // stack in bytes - too small crashes elsewhere
    nullptr,
    2,           // priority. loop() runs at 1
    nullptr,
    1);          // core 1, away from Wi-Fi
}

void loop() {
  int v;
  while (xQueueReceive(q, &v, 0) == pdTRUE) Serial.println(v);
  delay(100);
}

4096 bytes of stack is a reasonable start. Too small and it crashes somewhere unrelated, which is the hardest bug on this page to find.

When it does not work

Task watchdog got triggered, and it names my task

The task never yields, so the idle task never runs, and the idle task is what feeds the watchdog. One vTaskDelay(1) per pass fixes it.

The board crashes somewhere unrelated to the change I made

Stack overflow in a task. Large locals, deep calls or a printf with a big buffer. Raise the stack size and move big buffers off it.

A shared variable is occasionally nonsense

Two cores wrote it at once. volatile is not a lock. Use a queue, a mutex, or a FreeRTOS notification.

My chip has one core

The C3 and C6 are single core. Tasks and queues still work - FreeRTOS just interleaves them - so the code above runs unchanged with the core argument ignored.

Where this goes next

Two cores means twice as many ways to hang. When one does, the board reboots and leaves an address — here is how to read it.

Crash dumps and the watchdog

Edit this page — content/esp32/dual-core-tasks.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum