If you have ever watched your ESP32 analog input readings bounce wildly between 0 and 4095, or silently flatline the moment you turn on WiFi, you have run into the quirks of the ESP32's internal ADC. The ESP32 features two 12-bit Successive Approximation Register (SAR) ADCs mapped to specific GPIO pins, capable of reading 0-3.3V. However, due to silicon-level non-linearities and peripheral sharing, getting reliable, repeatable voltage measurements requires specific pin selection and calibration.
This guide targets the ESP32-WROOM-32 DevKit V1 (30-pin variant). We will cover the hardware traps, provide a fully calibrated, compilable Arduino Core sketch, and break down exactly how to debug the most common ADC failures on the bench.
The ADC1 vs ADC2 Trap (And How to Avoid It)
The most common mistake makers and junior engineers make with the ESP32 analog input is ignoring the peripheral routing. The chip has two distinct ADC units:
- ADC1: 8 channels, mapped to GPIO 32-39. These are dedicated to the ADC and work perfectly alongside WiFi and Bluetooth.
- ADC2: 10 channels, mapped to GPIO 0, 2, 4, 12-15, 25-27. ADC2 is shared with the WiFi driver.
analogRead() will silently return 0 or 4095. In ESP-IDF, it will throw the exact error string: E (123) adc: adc2_get_raw(...) failed. Always use ADC1 pins for analog sensors in IoT projects.
Parts List and Pin Mapping
For this build, we are reading a precision potentiometer and an NTC thermistor via a voltage divider. We intentionally use one ADC1 pin and one ADC2 pin to demonstrate the WiFi conflict and the debugging process.
Bill of Materials
| Component | Exact Variant / Spec | Estimated Cost |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (30-pin, Type-C) | $6.50 |
| Potentiometer | Bourns 3386P 10kΩ Linear (B10K) | $1.20 |
| Thermistor | 10kΩ NTC 3950 (Glass encapsulated) | $0.80 |
| Divider Resistor | 10kΩ 1/4W 1% Tolerance (Metal Film) | $0.10 |
| Decoupling Caps | 100nF (0.1µF) Ceramic (x2) | $0.05 |
ESP32 Analog Input Pin Mapping
| Sensor | ESP32 GPIO | ADC Unit | ADC Channel | Safe with WiFi? |
|---|---|---|---|---|
| Potentiometer (Wiper) | GPIO 34 | ADC1 | Channel 6 | Yes |
| Thermistor (Divider) | GPIO 25 | ADC2 | Channel 8 | No (Fails if WiFi is ON) |
Step-by-Step Wiring and Voltage Divider Math
The ESP32 ADC pins are strictly limited to 3.3V. Feeding 5V into GPIO 34 will permanently damage the silicon. For the thermistor, we use a voltage divider to step down the 3.3V rail into a measurable range.
- Prep the Power Rail: Connect the ESP32 3.3V pin to the positive breadboard rail, and GND to the negative rail. Do not use the 5V (VIN) rail for analog references; the ESP32 ADC references the internal 3.3V LDO.
- Wire the Potentiometer: Connect Pin 1 to 3.3V, Pin 3 to GND. Connect the wiper (Pin 2) to GPIO 34. Place a 100nF ceramic capacitor between GPIO 34 and GND to filter high-frequency noise.
- Wire the Thermistor Divider: Connect the 10kΩ fixed resistor between 3.3V and GPIO 25. Connect the NTC thermistor between GPIO 25 and GND. Place a second 100nF capacitor between GPIO 25 and GND.
- Verify Voltages: Before powering the ESP32, use a multimeter to verify the breadboard 3.3V rail reads between 3.25V and 3.35V. Turn the pot to its extremes and verify the wiper voltage never exceeds 3.3V.
Complete Compilable Code (Arduino Core v2.x / v3.x)
This code targets the Arduino ESP32 Core v2.0.4 or newer (including v3.x). It utilizes the built-in analogReadMilliVolts() function, which automatically applies the chip's factory eFuse calibration data to correct for silicon-level ADC non-linearities.
/*
* ESP32 Analog Input Calibration & Debugging Sketch
* Target Board: ESP32-WROOM-32 DevKit V1 (30-pin)
* Core Version: Arduino ESP32 Core v2.0.4+ or v3.x
*/
#include <WiFi.h>
// --- PIN DEFINITIONS ---
#define PIN_POT_ADC1 34 // ADC1_CH6 (Safe with WiFi)
#define PIN_THERM_ADC2 25 // ADC2_CH8 (Fails with WiFi)
// --- CONSTANTS ---
const float VCC_MV = 3300.0; // Nominal 3.3V in millivolts
const float R_FIXED = 10000.0; // 10k ohm fixed resistor
// WiFi Credentials (Used to demonstrate ADC2 failure)
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n--- ESP32 Analog Input Calibration Demo ---");
// Set ADC resolution to 12-bit (0-4095)
analogReadResolution(12);
// Set attenuation to 11dB for full 0-3.3V range reading
analogSetPinAttenuation(PIN_POT_ADC1, ADC_11db);
analogSetPinAttenuation(PIN_THERM_ADC2, ADC_11db);
// Connect to WiFi to trigger the ADC2 conflict for debugging demo
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi Connected. ADC2 reads will now fail.");
} else {
Serial.println("\nWiFi Failed to connect. ADC2 reads may succeed.");
}
}
void loop() {
// --- READ ADC1 (Potentiometer) ---
int rawPot = analogRead(PIN_POT_ADC1);
int mvPot = analogReadMilliVolts(PIN_POT_ADC1);
// Error handling for out-of-bounds or disconnected pins
if (rawPot < 0 || rawPot > 4095) {
Serial.println("[ERROR] ADC1 Pin 34 returned invalid raw data.");
} else {
float potVoltage = mvPot / 1000.0;
Serial.printf("ADC1 (GPIO 34) | Raw: %4d | Calibrated: %4d mV (%.2f V)\n", rawPot, mvPot, potVoltage);
}
// --- READ ADC2 (Thermistor) ---
int rawTherm = analogRead(PIN_THERM_ADC2);
// Debugging the ADC2 + WiFi conflict
if (WiFi.status() == WL_CONNECTED && (rawTherm == 0 || rawTherm == 4095)) {
Serial.println("[DEBUG] ADC2 (GPIO 25) failed. WiFi is active, blocking ADC2 hardware.");
Serial.println(" ESP-IDF equivalent error: E (123) adc: adc2_get_raw(...) failed");
} else {
int mvTherm = analogReadMilliVolts(PIN_THERM_ADC2);
// Calculate thermistor resistance using voltage divider math: R_ntc = R_fixed * (V_ntc / (Vcc - V_ntc))
float vTherm = mvTherm / 1000.0;
float rTherm = R_FIXED * (vTherm / ((VCC_MV / 1000.0) - vTherm));
Serial.printf("ADC2 (GPIO 25) | Raw: %4d | Calibrated: %4d mV | R_ntc: %.0f ohms\n", rawTherm, mvTherm, rTherm);
}
Serial.println("---------------------------------------------------");
delay(1000);
}
Debugging: The First Three Things to Check When It Fails
When your ESP32 analog input returns garbage data, flatlines at 4095, or throws the ESP-IDF error string E (xxx) adc: adc2_get_raw(...) failed, do not rewrite your code immediately. Hardware and peripheral routing are almost always the culprits. Check these three things in order:
- Verify the ADC Unit vs. WiFi State: If you are using an ADC2 pin (GPIO 0, 2, 4, 12-15, 25-27) and WiFi or Bluetooth is initialized, the read will fail. Fix: Move your sensor to an ADC1 pin (GPIO 32-39) or disable WiFi before taking the reading.
- Check for Strapping Pin Conflicts: GPIO 0, 2, 4, 5, 12, and 15 are strapping pins used during boot. If you have external pull-ups, pull-downs, or sensors wired to these pins, the ESP32 may boot into flash mode or alter the pin's internal state, ruining the ADC baseline. Fix: Avoid using strapping pins for analog inputs entirely.
- Measure the Actual VCC Rail: The
analogReadMilliVolts()function assumes a perfect 3.3V reference. If your USB cable is thin and the board's LDO is sagging to 3.1V under WiFi load, your math will be skewed. Fix: Probe the 3.3V pin with a multimeter while the sketch is running. If it reads below 3.2V, power the board via the 5V pin with a dedicated bench supply or upgrade your USB cable.
For deeper silicon-level quirks, refer to the official Espressif ADC API Documentation, which details the non-linear voltage curves at the 0-100mV and 3.1-3.3V extremes.
Extending and Simplifying the Build
How to Simplify: If you do not need raw 12-bit ADC values and just want reliable voltage data, strip out analogRead() entirely. Rely solely on analogReadMilliVolts(). This single function handles the 11dB attenuation mapping and applies the factory eFuse calibration curve, saving you from writing complex lookup tables in your sketch.
How to Extend: The internal ESP32 ADC is notoriously noisy and non-linear at the edges of its range. If your project requires true precision (e.g., reading a 4-20mA industrial pressure transducer or a precision load cell), abandon the internal ADC. Extend your build by wiring an Adafruit ADS1115 (16-bit I2C ADC with built-in Programmable Gain Amplifier) to the ESP32's I2C pins (GPIO 21/22). This offloads the analog conversion to a dedicated chip, completely bypassing the ESP32's WiFi/ADC2 conflicts and internal noise floor.
Frequently Asked Questions
Why is my ESP32 analog input reading jumping around by 50-100 points?
The ESP32's internal SAR ADC is highly susceptible to electromagnetic interference (EMI) and lacks robust internal decoupling. A jump of 50-100 points (roughly 40-80mV) is normal for an unshielded, unfiltered pin. To fix this, solder a 100nF ceramic capacitor directly between the ADC GPIO pin and GND as close to the board as possible. In software, implement a simple rolling average filter (e.g., average the last 16 reads) or discard the top and bottom 10% of a 20-sample batch before calculating the mean.
Can I use ESP32 analog input while WiFi is connected?
Yes, but only if you use ADC1 pins (GPIO 32, 33, 34, 35, 36, 39). The WiFi radio driver takes exclusive control of the ADC2 hardware peripheral to monitor internal RF temperature and voltage. If you attempt to read an ADC2 pin while WiFi.begin() has been called, the read will fail. Always design your PCB or breadboard layout to route analog sensors to ADC1 pins if the device is IoT-connected.
How do I read exactly 0 to 3.3V on the ESP32 ADC without distortion?
You cannot read a perfectly linear 0 to 3.3V using the internal ADC. Even with 11dB attenuation enabled, the ESP32 ADC exhibits severe non-linearity below 100mV and above 3.1V. The practical, linear sweet spot for the ESP32 analog input is 0.15V to 2.5V. If your sensor outputs 0-3.3V, use an op-amp voltage divider or a resistor divider to scale the maximum output down to 2.5V before it hits the GPIO pin, ensuring you stay in the linear region of the silicon.
Why does analogRead() return 4095 even when the pin is grounded?
If a pin reads a hard 4095 when grounded, you are likely reading a pin that is being driven high by an internal pull-up, or you are reading a pin that does not have an ADC channel mapped to it (e.g., GPIO 13 on some specific ESP32-S3 variants, or standard digital-only pins on the original WROOM). Double-check the ESP32 GPIO pinout reference to ensure your chosen pin actually supports analog input, and verify you haven't accidentally enabled pinMode(pin, INPUT_PULLUP) in your setup routine.






