Embedded

ESP32 Programming Arduino IDE

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: Install the Espressif "esp32" package through the Arduino Boards Manager, pick the right board and port, and remember that your loop() is just one FreeRTOS task (priority 1, pinned to core 1) running on top of ESP-IDF, so most "mystery" crashes are really task, stack, watchdog or pin-capability problems.

    Why the ESP32 is not "just another Arduino"

    An Arduino Uno runs one thread on an 8-bit AVR at 16 MHz, with 2 KB of RAM. An ESP32 is a dual-core 32-bit Xtensa LX6 (or single-core RISC-V on the C3) at up to 240 MHz with roughly 520 KB of internal SRAM, Wi-Fi, Bluetooth and an external SPI flash that holds your code. Because of that, the Arduino environment on ESP32 is a thin layer on top of a real operating system. Understanding the layer underneath is what separates a blinking-LED tutorial from firmware you can ship. If you are still choosing which header pin to use, read our ESP32 pin diagram guide first, because several pins are reserved for flash or boot strapping.

    Step 1: Install the core through the Boards Manager

    The core is not bundled with the Arduino IDE. You add it once:

    • Open File → Preferences and paste the Espressif package index URL into "Additional boards manager URLs": https://espressif.github.io/arduino-esp32/package_esp32_index.json.
    • Open Tools → Board → Boards Manager, search "esp32" and install the package published by Espressif Systems. It downloads the Xtensa toolchain, the prebuilt ESP-IDF libraries and esptool, several hundred MB in total.
    • Select your board (for a generic DevKit V1 use "ESP32 Dev Module"), then pick the serial port. Most boards use a CP2102 or CH340 USB-UART bridge, and a missing driver is the number one reason the port list is empty.

    Key fact: the Arduino core for ESP32 is not a standalone runtime. It wraps ESP-IDF, which already provides FreeRTOS, the Wi-Fi stack and the drivers. Anything you can do in IDF, you can call from an Arduino sketch.

    Step 2: What actually happens when you press Upload

    Pressing Upload triggers a five-stage pipeline, shown below. The compile stage turns your sketch into an ELF file and then a binary image; the bootloader, partition table and app are separate images placed at fixed flash offsets (bootloader at 0x1000, partition table at 0x8000, app at 0x10000 on the classic ESP32).

    Upload flow: sketch to running firmware 1. Compile.ino to ELF 2. Link binapp.bin image 3. ResetDTR / RTS 4. esptoolwrites flash 5. BootloopTask runs Stage 3 pulls GPIO0 low and pulses EN, so the ROM bootloader enters download mode. If your board lacks the auto-reset circuit, hold BOOT while "Connecting..." appears. CourseTron · coursetron.com
    The five stages of an Arduino upload on ESP32. Download mode is selected purely by the level of GPIO0 at reset.

    Two pitfalls dominate here. First, "Failed to connect to ESP32: Wrong boot mode" means GPIO0 was not low at reset: hold BOOT, tap EN, release BOOT once writing starts. Second, a flaky USB cable or a bridge that cannot hold 921600 baud causes mid-upload failures; dropping Tools → Upload Speed to 460800 or 115200 fixes it. Also, keep the Serial Monitor baud rate equal to your Serial.begin() value, because the ROM boot log prints at 115200 and garbage characters at the wrong rate are the classic symptom.

    Step 3: FreeRTOS is already running your sketch

    Before setup() executes, the IDF startup code has already launched FreeRTOS. The Arduino core then creates a task named loopTask with an 8192-byte stack, priority 1, pinned to core 1 (the APP_CPU). That task calls setup() once and then loop() forever. The Wi-Fi and Bluetooth stacks live mostly on core 0 (the PRO_CPU) at much higher priorities, which is why your sketch can sit in a tight loop and the network still works.

    Who runs where on a default Arduino-ESP32 build Core 0 (PRO_CPU) Core 1 (APP_CPU) Wi-Fi / BT stack (high priority) lwIP tcpip task IDLE0 (feeds watchdog) loopTask: setup() then loop() Your xTaskCreatePinnedToCore tasks IDLE1 (feeds watchdog) A task that never blocks starves IDLE and trips the task watchdog (5 s default). CourseTron · coursetron.com
    Default task placement. Orange tasks belong to the radio and network stack, green tasks to your code.

    Even delay(500) is not a busy-wait here. It ends up in vTaskDelay, which blocks your task and lets the scheduler run others, at a tick rate of 1000 Hz (1 ms resolution). That is why delay() is friendly on ESP32 but a while(1); with no yield is deadly: IDLE never runs, the task watchdog fires after about 5 seconds, and you get a backtrace and reboot.

    Creating your own tasks

    TaskHandle_t sensorTask;
    
    void sensorLoop(void *arg) {
      const TickType_t period = pdMS_TO_TICKS(100);   // 10 Hz
      TickType_t last = xTaskGetTickCount();
      for (;;) {
        int raw = analogRead(34);                      // ADC1 pin, safe with Wi-Fi
        Serial.println(raw);
        vTaskDelayUntil(&last, period);                // fixed period, no drift
      }
    }
    
    void setup() {
      Serial.begin(115200);
      xTaskCreatePinnedToCore(sensorLoop, "sensor", 4096, NULL, 2, &sensorTask, 1);
    }
    void loop() { vTaskDelay(portMAX_DELAY); }         // nothing to do here

    Note the stack size is in bytes on ESP32 (unlike vanilla FreeRTOS, where it is in words). A task that calls Serial.printf with floats or builds String objects can easily need 4 KB or more; use uxTaskGetStackHighWaterMark(NULL) to see how much headroom is left. For communication between tasks, prefer queues (xQueueSend) over global variables; a shared variable that is larger than 32 bits or updated from an ISR needs a mutex or critical section to avoid torn reads. We cover scheduling theory further in our FreeRTOS tasks and queues explained article.

    Common pitfalls and their real causes

    SymptomReal causeFix
    "Guru Meditation Error: Core 1 panic'ed (LoadProhibited)"Null or dangling pointer, or stack overflow in a taskDecode the backtrace with the Exception Decoder; increase stack
    Reboot loop with "Task watchdog got triggered"Task never yields, starving IDLEAdd vTaskDelay(1) or block on a queue
    analogRead() returns 0 while Wi-Fi is onADC2 pins are shared with the Wi-Fi driverUse ADC1 pins (GPIO32-39)
    Board will not boot with a sensor attachedStrapping pins (GPIO0, 2, 12, 15) pulled to the wrong levelMove the device or add a series resistor
    "Brownout detector was triggered"Supply sags during Wi-Fi TX bursts (up to ~240 mA)Better USB cable or a 100-470 µF bulk capacitor

    Key fact: interrupt handlers must be marked IRAM_ATTR so they live in internal RAM. While flash is being written (for example by NVS or OTA), the flash cache is disabled, and an ISR that executes from flash will crash the chip. Never call Serial.print or allocate memory inside an ISR; set a flag or use xQueueSendFromISR instead.

    Seeing the scheduler in action

    The animation below shows two tasks sharing one core. A high-priority sensor task wakes every period, preempts the low-priority background task, finishes quickly and blocks again. That pattern, short bursts followed by blocking, is the entire secret to a responsive ESP32 design.

    Preemption timeline on one core (animated) Sensor (prio 2) loopTask (prio 1) tick tick tick Cyan line = CPU time. Orange runs first and preempts green. CourseTron · coursetron.com
    A moving time cursor sweeps across the timeline. Every period the high-priority task takes the core, then blocks and hands it back.

    Practical settings worth knowing

    • Partition scheme: the default on a 4 MB module gives about 1.2 MB to the app. If a Wi-Fi plus BLE sketch no longer fits, switch to "Huge APP", or to a scheme that keeps two OTA slots if you need over-the-air updates.
    • Core debug level: set Tools → Core Debug Level to "Verbose" while diagnosing Wi-Fi issues, and back to "None" for production to save flash and CPU.
    • Dual-core use: pin heavy computation (DSP, filtering) to core 1 only if it cooperates; leave core 0 alone, as the radio is timing-critical.
    • Deep sleep: only variables marked RTC_DATA_ATTR survive a wake-up; everything else restarts as a fresh boot.

    Once your prototype works, turning it into a reliable product involves PCB layout, antenna keep-out and certification. Teams that need that jump often engage Avecas design services for hardware and firmware co-design. To move on from the Arduino abstraction to native ESP-IDF, see our ESP-IDF versus Arduino core comparison.

    Want to build real firmware, not just copy sketches? Explore the embedded systems tracks on our CourseTron courses page and learn RTOS design, peripherals and debugging with hands-on labs.

    FAQ

    Which Board should I select for a generic ESP32 DevKit?

    "ESP32 Dev Module" works for almost every 30- or 38-pin DevKit clone. Choose a specific variant (S3, C3) only if the chip marking on your module says so, because the wrong target produces an image the ROM bootloader rejects.

    Does the Arduino core on ESP32 use FreeRTOS?

    Yes, always. Your setup() and loop() run inside a FreeRTOS task called loopTask on core 1, and you can create additional tasks with xTaskCreatePinnedToCore.

    Why does my upload hang at "Connecting........"?

    The chip is not entering download mode. Hold the BOOT button (GPIO0 low) while the dots appear, check the USB cable carries data, and confirm no peripheral is loading GPIO0 or EN.

    Can I use analogRead with Wi-Fi enabled?

    Only on ADC1 channels (GPIO32 to GPIO39). ADC2 is claimed by the Wi-Fi driver and reads fail or return stale values while the radio is active.

    Advertisement