ESP32 Deep Dive — GPIO, ADC, PWM, I2C, WiFi, BLE, FreeRTOS, and OTA
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<br/>Protocol Stack] --> MEM[520KB SRAM]
CPU1[Core 1<br/>Application] --> MEM
MEM --> Flash[External Flash<br/>Up to 16MB]
MEM --> PSRAM[External PSRAM<br/>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.80VPWM 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.1hPaFreeRTOS — 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 sensorDeep 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