Arduino core for ESP32 in depth: install it, understand it, stop fighting it

How the Arduino core for ESP32 really works: installation, setup and loop on FreeRTOS, millis vs delay, compatible libraries and the differences from classic Arduino.

Also available in: EN, ES

Series Introduction to the ESP32 Part 11 View the series →
Arduino core on the ESP32

The “Arduino core for ESP32” is why half the world uses this chip. It lets you write pinMode() and digitalWrite() like on an Arduino UNO, but underneath there is an ESP32 running FreeRTOS. Understanding that layer is the difference between copying examples and knowing what you are doing.

What it actually is

It is not an emulated Arduino. Since version 3.x, the core is a wrapper over ESP-IDF: your setup() and loop() run inside a real FreeRTOS task (called loopTask), on core 1, with 8 KB of stack by default.

That explains two very typical things:

  • If your loop() hangs, the watchdog fires and the board reboots.
  • If the project grows, you can create tasks with xTaskCreatePinnedToCore().

Installation (two ways)

Arduino IDE — add the board URL in Preferences:

https://espressif.github.io/arduino-esp32/package_esp32_index.json

Then install “esp32 by Espressif” from the Boards Manager and pick your board.

PlatformIO (my recommendation) — platformio.ini:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

setup() and loop() under the hood

void setup() {
  Serial.begin(115200);
  pinMode(2, OUTPUT);
}

void loop() {
  digitalWrite(2, !digitalRead(2));
  delay(1000);
}

It works, but keep in mind:

  • delay() blocks the task. With Wi-Fi or several sensors, use millis().
  • setup() runs once; if you want parallel work, create tasks there.
  • The watchdog watches the loop: take too long and it reboots.

Anti-blocking version:

unsigned long last = 0;
constexpr unsigned long INTERVAL = 1000;

void loop() {
  if (millis() - last >= INTERVAL) {
    last = millis();
    digitalWrite(2, !digitalRead(2));
  }
}

Libraries: not all of them work

  • Generic Arduino libraries usually work if they do not touch AVR registers.
  • Those using timers or specific interrupts may fail.
  • For sensors, look for the “ESP32” version or use the vendor library.
  • Wire (I2C) and SPI exist, but you can choose the pins.

Differences from an Arduino UNO

ThingArduino UNOESP32
Voltage5 V3.3 V
Bits832
EEPROM1 KBnone (use Preferences)
ADC10-bit12-bit (and ADC2 with Wi-Fi, careful)
Wi-Fi
Cores12

Saving something in the ESP32 “EEPROM” is done like this:

#include <Preferences.h>
Preferences prefs;

void setup() {
  prefs.begin("my-app", false);
  int boots = prefs.getInt("n", 0) + 1;
  prefs.putInt("n", boots);
  prefs.end();
}

Good practices

  1. Do not block the loop: use millis(), tasks or vTaskDelay.
  2. Serial at 115200, and wait a moment after begin() if needed.
  3. Do not abuse String: it fragments memory. Use char[] and snprintf.
  4. Split your code into files; a 2000-line .ino is painful.
  5. Debug with logs, not Serial.println everywhere.

When to move to ESP-IDF

When you need fine control over tasks and memory, precise timing, serious OTA partitions, or to squeeze power. The Arduino core hides a lot, and sometimes that is fine… until it is not.

Summary

  • It is a wrapper over ESP-IDF; your setup/loop are a FreeRTOS task.
  • delay() blocks; learn millis() from day one.
  • 3.3 V, no EEPROM (use NVS/Preferences), 12-bit ADC.
  • Not every Arduino library compiles: check compatibility.
  • Current core version: 3.3.11 (on ESP-IDF 5.5.5).

Next step: ESP-IDF in depth for when you need total control. Or go back to the series hub.

Related posts

↑↓ navigate ↵ open Esc close