Skip to content

ESP32 Deep Dive โ€” GPIO, ADC, PWM, I2C, WiFi, BLE, FreeRTOS, and OTA

DodaTech Updated 2026-06-20 5 min read

In this tutorial, you'll learn about ESP32 Deep Dive. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The ESP32 is more than a WiFi-enabled Arduino โ€” it's a dual-core microcontroller with a rich peripheral set, real-time operating system (FreeRTOS), Bluetooth Classic and BLE, cryptographic accelerators, and multiple low-power modes. This deep dive covers every subsystem you'll use in production.

In this tutorial, you'll move beyond blinking LEDs to using GPIO interrupts, ADC with precision calibration, PWM for motor control, I2C/SPI bus communication, BLE GATT services, FreeRTOS task scheduling, OTA firmware updates, and sensor fusion. This is the hardware knowledge you need to build reliable, professional IoT products.

A predictive maintenance system using the ESP32 reads vibration from an I2C accelerometer, processes it with an FFT running on the second core via FreeRTOS, publishes alerts over MQTT, and sleeps between readings at 10ยตA โ€” running on a battery for a year.

ESP32 Block Diagram

graph TD
    subgraph "ESP32 SoC"
        CPU0[Core 0
Protocol Stack] --> MEM[520KB SRAM] CPU1[Core 1
Application] --> MEM MEM --> Flash[External Flash
Up to 16MB] MEM --> PSRAM[External PSRAM
Up to 8MB] end subgraph "Peripherals" GPIO[GPIO x 34] --> CPU0 ADC[ADC 12-bit x 2] --> CPU1 I2C[I2C x 2] --> CPU1 SPI[SPI x 4] --> CPU1 end subgraph "Wireless" WiFi[802.11 b/g/n] --> CPU0 BLE[Bluetooth 4.2/BLE] --> CPU0 end style CPU0 fill:#e74c3c,color:#fff style CPU1 fill:#e67e22,color:#fff

GPIO and Interrupts

ESP32 GPIO pins are multiplexed โ€” most peripherals share pins 0โ€“39. Configure them carefully to avoid conflicts.

// GPIO interrupt on button press
const int BUTTON_PIN = 0;  // BOOT button on dev board
volatile bool button_pressed = false;

void IRAM_ATTR isr_handler() {
    button_pressed = true;
}

void setup() {
    Serial.begin(115200);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    attachInterrupt(BUTTON_PIN, isr_handler, FALLING);
    Serial.println("Press the button");
}

void loop() {
    if (button_pressed) {
        Serial.println("Button pressed โ€” interrupt fired!");
        button_pressed = false;
    }
}

ADC โ€” Analog-to-Digital Conversion

ESP32's ADC has 12-bit resolution (0โ€“4095) with two SAR ADC units. The input range is 0โ€“3.3V, but attenuation settings extend it.

#define ADC_PIN 34
#define ADC_ATTEN ADC_11db  // 0โ€“3.6V range

void setup() {
    Serial.begin(115200);
    analogReadResolution(12);
    analogSetAttenuation(ADC_ATTEN);
}

void loop() {
    int raw = analogRead(ADC_PIN);
    float voltage = raw * (3.6f / 4095.0f);
    Serial.printf("Raw: %d, Voltage: %.2fV\n", raw, voltage);
    delay(1000);
}

Expected output:

Raw: 2048, Voltage: 1.80V
Raw: 2050, Voltage: 1.80V

PWM with LEDC

ESP32's LEDC peripheral generates up to 16 independent PWM channels at variable frequency and duty cycle.

const int LED_PIN = 2;
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 5000;
const int PWM_RES = 8;

void setup() {
    ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES);
    ledcAttachPin(LED_PIN, PWM_CHANNEL);
}

void loop() {
    for (int duty = 0; duty <= 255; duty++) {
        ledcWrite(PWM_CHANNEL, duty);
        delay(5);
    }
    for (int duty = 255; duty >= 0; duty--) {
        ledcWrite(PWM_CHANNEL, duty);
        delay(5);
    }
}

I2C Sensor Integration (BME280)

#include <Wire.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 bme;

void setup() {
    Serial.begin(115200);
    Wire.begin(21, 22);  // SDA, SCL
    if (!bme.begin(0x76)) {
        Serial.println("BME280 not found!");
        return;
    }
}

void loop() {
    float temp = bme.readTemperature();
    float humid = bme.readHumidity();
    float press = bme.readPressure() / 100.0F;
    Serial.printf("%.1fยฐC, %.1f%%, %.1fhPa\n", temp, humid, press);
    delay(5000);
}

Expected output:

24.7ยฐC, 55.2%, 1013.2hPa
24.8ยฐC, 55.0%, 1013.1hPa

FreeRTOS โ€” Dual-Core Tasking

FreeRTOS runs on both cores. Assign WiFi/BLE tasks to core 0 and application logic to core 1.

TaskHandle_t sensorTask;
TaskHandle_t wifiTask;

void sensorLoop(void *param) {
    while (1) {
        Serial.printf("Core %d: Reading sensor\n", xPortGetCoreID());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void wifiLoop(void *param) {
    while (1) {
        Serial.printf("Core %d: WiFi task\n", xPortGetCoreID());
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void setup() {
    Serial.begin(115200);
    xTaskCreatePinnedToCore(sensorLoop, "Sensor", 4096, NULL, 1, &sensorTask, 1);
    xTaskCreatePinnedToCore(wifiLoop, "WiFi", 8192, NULL, 1, &wifiTask, 0);
}

void loop() { vTaskDelete(NULL); }

Expected output:

Core 1: Reading sensor
Core 1: Reading sensor
Core 0: WiFi task
Core 1: Reading sensor

Deep Sleep Modes

Mode Current Wake sources RTC memory
Modem sleep ~3mA Timer, GPIO Preserved
Light sleep ~0.8mA Timer, GPIO, touch Preserved
Deep sleep ~10ยตA Timer, GPIO, touch, ULP Preserved
Hibernation ~2.5ยตA GPIO only Lost

Common Mistakes

  1. GPIO conflict with flash: GPIO 6โ€“11 are used for internal SPI flash. Do not use them for external circuits.
  2. ADC2 usage during WiFi: ADC2 works only when WiFi is disabled. Use ADC1 (pins 32โ€“39) for analog reads during WiFi operation.
  3. No watchdog in production: If WiFi or MQTT hangs, the device freezes. Enable the task watchdog timer: esp_task_wdt_init(10, true).
  4. PSRAM without proper config: Using external PSRAM requires specific flash voltage and timing settings in menuconfig.
  5. Stack overflow in FreeRTOS tasks: Default task stack (2048 bytes) is often insufficient. Monitor with uxTaskGetStackHighWaterMark().

Practice Questions

  1. Why should you avoid GPIO 6โ€“11 on ESP32? These pins are connected to the internal SPI flash. Using them causes boot failures and flash corruption.

  2. What's the difference between ADC1 and ADC2 on ESP32? ADC1 has 8 channels (pins 32โ€“39) and works independently. ADC2 has 10 channels but is disabled when WiFi is active.

  3. How does the ULP coprocessor work in deep sleep? The Ultra-Low-Power co-processor runs simple programs from RTC memory while the main CPU sleeps, reading sensors or GPIO at <1mA.

  4. What is the LEDC peripheral used for? LEDC generates PWM signals for LED dimming, servo control, and motor speed control with 16 independent channels.

  5. How do FreeRTOS task priorities work on ESP32? Priorities range from 0 (idle) to configMAX_PRIORITIES. WiFi/BLE tasks run at priority 23. Application tasks typically use 1โ€“5.

Challenge

Build a dual-core sensor hub: Core 0 runs WiFi + MQTT to publish data, Core 1 reads three sensors (temperature, humidity, pressure) at 10Hz and stores readings in a FreeRTOS queue. The device enters deep sleep for 5 minutes between measurement bursts.

FAQ

**Can ESP32 run TensorFlow Lite?**

Yes, TensorFlow Lite Micro runs on ESP32 with quantized models under 500KB. Use the ESP32-S3 for better performance with its vector extensions.

**What's the maximum WiFi throughput?**

ESP32 achieves ~50 Mbps UDP and ~30 Mbps TCP. Real-world throughput depends on antenna, distance, and interference.

**Does deep sleep preserve WiFi credentials?**

Yes, RTC memory is preserved during deep sleep. Store WiFi credentials there. WiFi must reconnect on wake.

**What's the difference between ESP32 and ESP32-S3?**

ESP32-S3 adds vector instructions (for ML), more GPIO, USB OTG, and improved cryptographic acceleration.

**Can I use both BLE and WiFi simultaneously?**

Yes, the ESP32 supports dual-mode Bluetooth + WiFi, but they share the radio. Throughput drops when both are active.

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro