ESP32 Deep Dive โ GPIO, ADC, PWM, I2C, WiFi, BLE, FreeRTOS, and OTA
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
- GPIO conflict with flash: GPIO 6โ11 are used for internal SPI flash. Do not use them for external circuits.
- ADC2 usage during WiFi: ADC2 works only when WiFi is disabled. Use ADC1 (pins 32โ39) for analog reads during WiFi operation.
- No watchdog in production: If WiFi or MQTT hangs, the device freezes. Enable the task watchdog timer:
esp_task_wdt_init(10, true). - PSRAM without proper config: Using external PSRAM requires specific flash voltage and timing settings in menuconfig.
- Stack overflow in FreeRTOS tasks: Default task stack (2048 bytes) is often insufficient. Monitor with
uxTaskGetStackHighWaterMark().
Practice Questions
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.
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.
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.
What is the LEDC peripheral used for? LEDC generates PWM signals for LED dimming, servo control, and motor speed control with 16 independent channels.
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
Cross-References
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro