ESP32 ADC Complete Beginner Guide: analogRead, Attenuation Ranges and Accuracy Traps

Prerequisite: the GPIO Complete Guide. GPIO only reads HIGH or LOW — but potentiometers, LDRs, soil sensors and battery voltages are continuous. That’s where the ADC comes in.


1. What is an ADC?

The ADC (Analog-to-Digital Converter) translates a continuous voltage into a number:

ESP32 ADC: 12-bit resolution
  0V    → reading 0
  3.3V  → reading 4095 (2¹² − 1)
  1.65V → reading ~2048

ADC1 vs ADC2: the invisible landmine

BankPins (classic ESP32)Wi-Fi interaction
ADC1GPIO 32, 33, 34, 35, 36, 39✅ always available
ADC2GPIO 0, 2, 4, 12, 13, 14, 15, 25, 26, 27❌ dead once Wi-Fi starts

⚠️ Iron rule: wire all analog sensors to ADC1 (GPIO 32–39). A sketch that works standalone and reads zeros after WiFi.begin() is this exact bug. Verify any pin in the Pinout Explorer.

Newer chips (S3/C3) remap the banks (S3’s ADC1 = GPIO1–10), but the ADC2/Wi-Fi conflict persists — check the datasheet.


2. analogRead in five minutes

1. Reading a potentiometer (the classic)

#define POT_PIN 32   // must be an ADC1 pin!

void setup() {
  Serial.begin(115200);
}

void loop() {
  int raw = analogRead(POT_PIN);            // 0 ~ 4095
  float voltage = raw * 3.3 / 4095.0;
  Serial.printf("raw: %d, voltage: %.2fV\n", raw, voltage);
  delay(200);
}

Turning the pot should sweep the reading smoothly from 0 to 4095.

2. Attenuation: why the default range is 3.3V

The ESP32 ADC’s native range is only ~1.1V; a front-end attenuator extends it. Arduino defaults to 11dB:

AttenuationUsable range (approx)Use case
0 dB (ADC_0db)0 ~ 1.1VSmall signals, best accuracy
2.5 dB (ADC_2_5db)0 ~ 1.5V—
6 dB (ADC_6db)0 ~ 2.2V—
11 dB (default ADC_11db)0 ~ 3.3VGeneral sensors
// arduino-esp32 3.x
analogSetPinAttenuation(POT_PIN, ADC_11db);

Warning: at 11dB the response is markedly non-linear near 0V and 3.3V; the middle is decent. For high accuracy, shrink the range and divide externally.


3. The accuracy truth: ESP32 ADCs aren’t precise

Honest talk — the classic ESP32 ADC has a mediocre accuracy reputation:

  1. Non-linearity: equal voltage steps don’t produce equal reading steps across the range;
  2. Noise: 10 consecutive reads of the same voltage can swing ±20–50 counts.

Three field-proven countermeasures

#1: Multi-sample averaging (simplest, works)

int readAdcStable(int pin, int samples = 16) {
  long sum = 0;
  for (int i = 0; i < samples; i++) {
    sum += analogRead(pin);
    delayMicroseconds(100);
  }
  return sum / samples;   // averaging 16 reads tames the noise
}

#2: Calibration (factory eFuse reference)

Each chip ships with a two-point calibration (~1100mV) that esp_adc_cal applies automatically:

#include "esp_adc_cal.h"

float readVoltageCalibrated(int pin) {
  esp_adc_cal_characteristics_t chars;
  esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &chars);
  uint32_t mv = esp_adc_cal_raw_to_voltage(analogRead(pin), &chars);
  return mv / 1000.0;
}

#3: Lower the bar — for soil moisture or brightness where only the trend matters, map() straight to a percentage:

int moisture = map(readAdcStable(32), 4095, 1500, 0, 100);  // dry→wet to 0~100%
moisture = constrain(moisture, 0, 100);

4. Project: battery voltage monitoring (every battery project needs it)

The ADC tops out at 3.3V; measuring an 18650 (3.0–4.2V) requires a divider:

Battery(+) ──[100kΩ]──●──[100kΩ]── GND
                      │
                    GPIO33 (an ADC1 pin)
#define BAT_PIN 33
#define BAT_DIVIDER 0.5

void setup() { Serial.begin(115200); }

void loop() {
  float pinVolt = readAdcStable(BAT_PIN) * 3.3 / 4095.0;
  float batVolt = pinVolt / BAT_DIVIDER;
  Serial.printf("battery: %.2fV\n", batVolt);
  delay(1000);
}

💡 Advanced: power the divider only during sampling (a MOSFET switch) and cut it fully otherwise — saves tens of µA in deep-sleep projects (see the upcoming deep-sleep article).


5. Pitfall cheat sheet (bookmark this)

ScenarioVerdict
Choosing pinsADC1 only (classic ESP32 = GPIO 32–39)
Reads all zero after enabling Wi-FiYou’re on ADC2 — move to ADC1
Stuck at 4095Wire broken/floating, or above range
Readings jump ±50Normal noise — average 16 samples
Inaccurate at range extremesNon-linear zone — calibrate or avoid
Measuring 5V signalsDivide first (e.g. 10k+20k → 1/3) or the pin burns
Battery monitoring100k+100k divider into ADC1, ×2 in software
Sensor drifts with temperatureADC reads voltage; drift compensation is a sensor-layer job

6. Further reading


License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com