A Precise Voltmeter on the ADS1115: 16 Bits Instead of the ESP32's Wobbly ADC
The ESP32's built-in ADC has a dead zone near zero, a 3.1 V ceiling and non-linearity. An external 16-bit ADS1115 on I2C measures in 0.125 mV steps. We compare both on one potentiometer and measure a 12 V battery through a divider.
About the project
analogRead() on the ESP32 is handy but not accurate: up to ~0.1 V it reads zero, above ~3.1 V it reads full scale, and in between it can be tens of millivolts off. That is fine for a battery indicator or a potentiometer, not for a measuring instrument. The ADS1115 is a 16-bit ADC with a programmable gain amplifier and its own voltage reference: 4 inputs, I2C, and errors in fractions of a percent. We compare it with the built-in ADC and build a voltmeter for a 12 V battery.
What you need
- ESP32 DevKit
- ADS1115 module (the purple board)
- 10 kΩ potentiometer
- 100 kΩ and 22 kΩ resistors for the divider
- The Adafruit ADS1X15 library
Wiring
| ADS1115 | Connection |
|---|---|
| VDD / GND | 3V3 / GND |
| SDA / SCL | GPIO21 / GPIO22 |
| ADDR | GND → address 0x48 |
| A0 | the potentiometer wiper (also on GPIO34 for comparison) |
| A1 | the midpoint of a 100 kΩ / 22 kΩ divider from "+12 V" |
Potentiometer: outer pins to 3V3 and GND. Divider: "+12 V" → 100 kΩ → A1 → 22 kΩ → GND, with the battery ground tied to the ESP32 GND.
Setting the gain
setGain() sets the measuring range:
| GAIN | Range | Step (16-bit) |
|---|---|---|
GAIN_TWOTHIRDS |
±6.144 V | 0.1875 mV |
GAIN_ONE |
±4.096 V | 0.125 mV |
GAIN_TWO (default) |
±2.048 V | 0.0625 mV |
GAIN_FOUR … GAIN_SIXTEEN |
±1.024 … ±0.256 V | down to 0.0078 mV |
Important: a ±6.144 V range does not mean you may feed 6 V into an input. No input may go outside GND − 0.3 V … VDD + 0.3 V. On a 3.3 V supply that is 3.6 V at most. Anything higher must go through a divider. The simulator warns when you exceed it.
Sizing the divider
V(in) = V(bat) × R2 / (R1 + R2) = 14.4 × 22 / 122 ≈ 2.6 V even for a battery on charge (14.4 V), with room to spare in the ±4.096 V range. The code multiplies back by (100 + 22) / 22. A 122 kΩ divider draws only ~0.1 mA from the battery.
Code
// Voltmeter: ADS1115 (16-bit) vs the ESP32 ADC + a battery voltage
#include <Adafruit_ADS1X15.h>
Adafruit_ADS1115 ads;
const float DIVIDER = (100.0 + 22.0) / 22.0; // 100 kΩ / 22 kΩ divider on A1
void setup() {
Serial.begin(115200);
ads.setGain(GAIN_ONE); // ±4.096 V, 0.125 mV steps
if (!ads.begin(0x48)) {
Serial.println("ADS1115 not found");
while (true) delay(1000);
}
}
void loop() {
float pot = ads.computeVolts(ads.readADC_SingleEnded(0));
float naive = analogRead(34) * 3.3 / 4095.0; // the "naive" conversion
float mv = analogReadMilliVolts(34) / 1000.0; // with the factory calibration
float battery = ads.computeVolts(ads.readADC_SingleEnded(1)) * DIVIDER;
Serial.printf("A0: ADS1115 %.4f V | analogRead %.3f V | analogReadMilliVolts %.3f V\n", pot, naive, mv);
Serial.printf("Battery: %.3f V\n", battery);
delay(1000);
}
What you will see
Turn the potentiometer to one end. Near zero the ESP32 reads 0 V while the ADS1115 still sees 0.05 V, and near 3.3 V the built-in ADC "hits the ceiling" at 3.1 V. analogReadMilliVolts() is clearly better than the naive formula, but ±10–20 mV of noise remains.
Good to know
- Speed. The default is 128 samples per second (~8 ms each).
setDataRate(RATE_ADS1115_860SPS)is faster but noisier. - Differential mode,
readADC_Differential_0_1(), measures the difference between A0 and A1, for example across a current shunt. - The ADS1015 is the 12-bit sibling with the same pinout; in code it is the
Adafruit_ADS1015class. Mix up the classes and your readings are off by a factor of 16. - Four ADS1115s on one bus: ADDR to GND, VDD, SDA or SCL gives addresses 0x48–0x4B.