How to Use the ADS1115 ADC with Arduino and ESP32

Analog input sounds simple until it isn’t. You wire up a sensor, read a value, and notice the number jumping around by 50 counts even when nothing is touching the circuit. Or you’re building an ESP32 project with Wi-Fi, and your analog readings turn to garbage the moment the radio fires up. At that point, the built-in ADC has failed you — and you need an external one.

The ADS1115 is the part most experienced makers reach for when that happens. It’s a 16-bit, 4-channel ADC that communicates over I2C, costs very little, and solves essentially every problem the built-in ADC creates. This guide will take you from zero to confident readings on both Arduino and ESP32, with real code you can run today.

Why the ESP32’s Built-In ADC Is a Problem

The ESP32 has two ADC peripherals: ADC1 (GPIO32–39) and ADC2 (GPIO0, 2, 4, 12–15, 25–27). On paper, both offer 12-bit resolution — 4096 steps across the input voltage range. In practice, there are two serious problems.

First, ADC2 is shared with the Wi-Fi radio. When Wi-Fi is active — which is most of the time if you’re doing anything networked — ADC2 channels are completely unavailable. Attempts to read them will either fail outright or return meaningless values. This eliminates half your analog inputs the moment your project goes online.

Second, the ESP32’s ADC has a non-linear response curve. The relationship between actual input voltage and reported count is not straight. It bows in the middle, meaning readings in the 0.1–0.3 V and 2.8–3.3 V ranges are particularly inaccurate. Espressif provides calibration routines that partially correct this, but they’re complex to implement correctly and still don’t fix the fundamental noise floor.

The Arduino Uno’s 10-bit ADC doesn’t have the linearity problems, but 10 bits gives you only 1024 steps — about 4.9 mV resolution at 5 V. Usable for rough readings, but inadequate for precision sensor work: weighing, precision voltage monitoring, or any application where you need to distinguish small changes.

Both situations have the same solution: hand the job to a dedicated ADC designed to do nothing else.

What the ADS1115 Is — and What 16-Bit Resolution Actually Means

The ADS1115 is a precision, low-power, 16-bit analog-to-digital converter made by Texas Instruments. It has four single-ended input channels (or two differential pairs), communicates over I2C, runs on 2–5.5 V, and includes a programmable gain amplifier (PGA) and a comparator. The whole thing fits on a small breakout module with 0.1-inch pitch headers — directly breadboard compatible.

Sixteen bits means 216 = 65,536 steps. But the ADS1115 is signed — it distinguishes positive and negative voltages for differential measurements — so in single-ended mode you have 32,767 positive steps. At the default ±2.048 V gain setting, each step represents:

2.048 V / 32767 = 0.0000625 V = 62.5 µV per step

Compare that to the Arduino Uno at 4.88 mV per step, or the ESP32 at around 0.8 mV per step (with its accuracy problems). The ADS1115 at default gain is roughly 78 times more granular than the Arduino’s built-in ADC. With the highest gain setting (±0.256 V), each step drops to just 7.8 µV — a level of precision usually reserved for laboratory instruments.

In practical terms: you can meaningfully measure a 0.1 mV change in voltage. Battery cells, bridge sensors, thermocouples with amplifiers, 4–20 mA current loops — all of these become tractable.

I2C Address Configuration: The ADDR Pin Trick

The ADS1115 communicates over I2C, which means it shares the bus with every other I2C device on your project. I2C devices need unique addresses, and by default most ADS1115 modules sit at address 0x48. If you need more than four analog channels, or want to run multiple ADS1115 modules on one bus, you need different addresses — and that’s where the ADDR pin comes in.

The ADDR pin lets you select from four distinct I2C addresses by connecting it to different points in your circuit:

ADDR Pin Connected ToI2C Address
GND0x48 (default)
VDD0x49
SDA0x4A
SCL0x4B

Connecting ADDR to SDA or SCL is unusual — most I2C devices don’t allow this — but it’s a legitimate part of the ADS1115 specification. It gives you four unique addresses from a single address pin, which is an elegant hardware design. With four modules in a project, you get 16 analog input channels over two wires.

On most breakout boards, ADDR is pulled to GND by default, so 0x48 is what you’ll see without any modification. You don’t need to configure this in software unless you’ve changed the hardware connection.

Gain and PGA Settings Explained

The PGA (Programmable Gain Amplifier) is what makes the ADS1115 flexible across wildly different signal levels. It sits between the input and the ADC, amplifying the signal so the full resolution of the converter is applied to a smaller voltage range. Higher gain means more resolution over a narrower input window.

PGA SettingFull-Scale RangeResolution (per step)Use Case
GAIN_TWOTHIRDS±6.144 V187.5 µVReading 0–5 V signals on a 3.3 V board
GAIN_ONE±4.096 V125 µVGeneral 3.3 V and 5 V systems
GAIN_TWO±2.048 V62.5 µVDefault — sensors outputting 0–2 V
GAIN_FOUR±1.024 V31.25 µVPrecision sensors with small output range
GAIN_EIGHT±0.512 V15.625 µVAmplified bridge sensors
GAIN_SIXTEEN±0.256 V7.8125 µVThermocouples, strain gauges

Critical warning: the PGA setting defines the range the ADC expects, not the range it protects against. If your signal exceeds the selected PGA range, the reading will clip (saturate at maximum) but the chip won’t be damaged — as long as the input voltage stays within the absolute maximum of VDD + 0.3 V. Never connect a 5 V signal to an ADS1115 powered at 3.3 V without appropriate input protection. The input voltage must never exceed VDD.

A good default for most projects: start with GAIN_ONE (±4.096 V) when you’re not sure what voltage range your signal covers. Once you’ve characterised the signal, tighten the gain to use the full resolution.

Wiring the ADS1115

Pins on the ADS1115 Module

  • VDD: Power supply — 3.3 V or 5 V
  • GND: Ground
  • SCL: I2C clock
  • SDA: I2C data
  • ADDR: Address select (leave unconnected or tie to GND for 0x48)
  • ALRT/RDY: Alert or data-ready output (optional — leave unconnected for basic use)
  • A0–A3: Four analog input channels

Wiring to Arduino Uno

The Arduino Uno’s I2C bus is on pins A4 (SDA) and A5 (SCL). Use a breadboard to keep connections clean and easy to modify during testing.

ADS1115 PinArduino Uno Pin
VDD5V
GNDGND
SDAA4
SCLA5
ADDRGND (or leave unconnected)
A0Your analog signal

Wiring to ESP32

The ESP32 defaults to GPIO21 (SDA) and GPIO22 (SCL) for I2C, but the bus can be remapped to any GPIO pair in software. Power the ADS1115 from the 3.3 V pin — do not use 5 V on an ESP32 board unless you have a dedicated 5 V rail, as the I/O is not 5 V tolerant.

ADS1115 PinESP32 Pin
VDD3.3V
GNDGND
SDAGPIO21
SCLGPIO22
ADDRGND (or leave unconnected)
A0Your analog signal

Note on pull-up resistors: I2C requires pull-up resistors on SDA and SCL. Most ADS1115 breakout modules include these on-board (typically 10 kΩ). If you’re using a bare chip or a minimal module, add 4.7 kΩ resistors from SDA and SCL to VDD.

Arduino Code

Installing the Library

The easiest way to use the ADS1115 on Arduino is with Adafruit’s ADS1X15 library. Install it through the Arduino IDE Library Manager:

  1. Open the Arduino IDE
  2. Go to Sketch → Include Library → Manage Libraries
  3. Search for ADS1X15
  4. Install the Adafruit ADS1X15 library (also install the Adafruit BusIO dependency if prompted)

Basic Single-Ended Reading — Arduino

This sketch reads all four channels in sequence and prints the raw ADC count and calculated voltage to the Serial Monitor.

#include <Wire.h>
#include <Adafruit_ADS1X15.h>

Adafruit_ADS1115 ads;

void setup() {
  Serial.begin(9600);
  Serial.println("ADS1115 single-ended reading demo");

  // Default address is 0x48 (ADDR to GND)
  if (!ads.begin()) {
    Serial.println("Failed to initialise ADS1115. Check wiring.");
    while (1);
  }

  // Set gain — GAIN_ONE = ±4.096V range, 125µV per step
  ads.setGain(GAIN_ONE);
}

void loop() {
  int16_t raw0 = ads.readADC_SingleEnded(0); // Channel A0
  int16_t raw1 = ads.readADC_SingleEnded(1); // Channel A1
  int16_t raw2 = ads.readADC_SingleEnded(2); // Channel A2
  int16_t raw3 = ads.readADC_SingleEnded(3); // Channel A3

  // computeVolts() converts raw count to voltage using the current gain setting
  float v0 = ads.computeVolts(raw0);
  float v1 = ads.computeVolts(raw1);
  float v2 = ads.computeVolts(raw2);
  float v3 = ads.computeVolts(raw3);

  Serial.println("---");
  Serial.print("A0: "); Serial.print(raw0); Serial.print(" raw | "); Serial.print(v0, 4); Serial.println(" V");
  Serial.print("A1: "); Serial.print(raw1); Serial.print(" raw | "); Serial.print(v1, 4); Serial.println(" V");
  Serial.print("A2: "); Serial.print(raw2); Serial.print(" raw | "); Serial.print(v2, 4); Serial.println(" V");
  Serial.print("A3: "); Serial.print(raw3); Serial.print(" raw | "); Serial.print(v3, 4); Serial.println(" V");

  delay(1000);
}

Open the Serial Monitor at 9600 baud. You should see four voltage readings updating every second. If you see “Failed to initialise,” double-check your SDA/SCL connections and confirm the ADS1115 address with an I2C scanner sketch.

ESP32 Code

The same Adafruit library works on ESP32 without modification — the Arduino framework abstracts the I2C hardware. The only difference is that you may want to explicitly specify which GPIO pins to use for I2C, in case your board layout differs from the defaults.

#include <Wire.h>
#include <Adafruit_ADS1X15.h>

Adafruit_ADS1115 ads;

// ESP32 default I2C pins — change if your board differs
#define I2C_SDA 21
#define I2C_SCL 22

void setup() {
  Serial.begin(115200);
  Serial.println("ADS1115 on ESP32");

  // Explicitly initialise I2C with defined pins
  Wire.begin(I2C_SDA, I2C_SCL);

  if (!ads.begin(0x48, &Wire)) {
    Serial.println("ADS1115 not found. Check wiring and address.");
    while (1) { delay(10); }
  }

  ads.setGain(GAIN_TWO); // ±2.048V range — 62.5µV per step
  Serial.println("ADS1115 ready.");
}

void loop() {
  int16_t raw = ads.readADC_SingleEnded(0);
  float voltage = ads.computeVolts(raw);

  Serial.print("A0 raw: ");
  Serial.print(raw);
  Serial.print("  |  Voltage: ");
  Serial.print(voltage, 5);
  Serial.println(" V");

  delay(500);
}

On the ESP32, set your Serial Monitor to 115200 baud. This code reads only channel A0 at 500 ms intervals — expand to all four channels following the Arduino pattern above. Because this uses I2C (not the built-in ADC), Wi-Fi can be fully active without affecting readings.

Using Wi-Fi Alongside the ADS1115 on ESP32

The key advantage of the ADS1115 on ESP32 is that it’s completely unaffected by Wi-Fi activity. You can initialise Wi-Fi normally and read the ADS1115 in your main loop without any special handling:

#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_ADS1X15.h>

const char* ssid     = "your-network";
const char* password = "your-password";

Adafruit_ADS1115 ads;

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  ads.begin(0x48, &Wire);
  ads.setGain(GAIN_ONE);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi connected");
}

void loop() {
  // Reads are rock-solid even with Wi-Fi active
  int16_t raw = ads.readADC_SingleEnded(0);
  float voltage = ads.computeVolts(raw);
  Serial.print("Voltage: "); Serial.println(voltage, 4);
  delay(1000);
}

Differential vs Single-Ended Mode

So far we’ve used single-ended mode — each channel measured relative to GND. That’s correct for most sensors and potentiometers. But the ADS1115 also supports differential mode, and understanding when to use it matters.

Single-Ended Mode

Each channel (A0–A3) is read relative to the chip’s GND pin. The result is always a positive voltage between 0 V and VDD. Use this for: potentiometers, phototransistors, voltage dividers, most analog sensors, anything with a single output wire and a shared ground.

Differential Mode

Two channels are read against each other. The ADS1115 supports two differential pairs: A0 vs A1, and A2 vs A3. The result is the voltage difference between the pair — which can be positive or negative. Use this for: Wheatstone bridge sensors (load cells, pressure sensors, strain gauges), current sensing across a shunt resistor, any application where common-mode noise rejection is needed.

Differential mode is powerful because it rejects noise that appears equally on both input lines — which is exactly what you get with long cable runs in electrically noisy environments. If both wires pick up the same 50 Hz interference, the differential reading cancels it out.

// Differential reading: A0 positive, A1 negative
int16_t diffResult = ads.readADC_Differential_0_1();
float diffVoltage = ads.computeVolts(diffResult);

// Result can be negative if A1 > A0
Serial.print("Differential A0-A1: ");
Serial.print(diffVoltage, 5);
Serial.println(" V");

// Second differential pair: A2 vs A3
int16_t diffResult2 = ads.readADC_Differential_2_3();
float diffVoltage2 = ads.computeVolts(diffResult2);

For a standard load cell, the differential reading removes the need for a dedicated amplifier like the HX711 in many lower-resolution applications — the ADS1115’s 16-bit differential mode with GAIN_SIXTEEN is sufficient for light precision weighing.

Real Project Examples

1. LiPo Battery Voltage Monitor

A single-cell LiPo runs from 3.0 V (depleted) to 4.2 V (full). With GAIN_TWOTHIRDS (±6.144 V full scale), the ADS1115 can read this directly. Connect the battery positive to A0, battery negative to GND. The 187.5 µV resolution means you can detect 0.01% state-of-charge changes — far beyond what any built-in ADC can resolve.

ads.setGain(GAIN_TWOTHIRDS);
float batteryVoltage = ads.computeVolts(ads.readADC_SingleEnded(0));
int percent = map(batteryVoltage * 100, 300, 420, 0, 100);
percent = constrain(percent, 0, 100);
Serial.print("Battery: "); Serial.print(batteryVoltage, 3);
Serial.print(" V ("); Serial.print(percent); Serial.println("%)");

For multi-cell packs above 6.144 V, use a resistor voltage divider to scale down to the input range, and multiply the result by the divider ratio in your code.

2. Precision Potentiometer Control

A 10 kΩ potentiometer wired from VDD to GND with the wiper to A0 gives a voltage from 0 V to VDD. With a 12-bit Arduino ADC, a full sweep of the pot gives you 1024 steps. With the ADS1115 at GAIN_ONE, the same sweep gives you approximately 32,767 steps — 32× finer control. This is meaningful for applications like motorised focus control, audio level setting, or any position-sensitive input where fine resolution matters.

ads.setGain(GAIN_ONE); // ±4.096 V — covers 0-3.3V or 0-5V pot output
int16_t raw = ads.readADC_SingleEnded(0);
// Map to a 0-1000 range for fine-grained control
int controlValue = map(raw, 0, 32767, 0, 1000);
Serial.println(controlValue);

3. Analog Sensor Array Over I2C (ESP32 + Wi-Fi)

Suppose you’re building an IoT environmental monitor with four analog sensors — light, soil moisture, a 4–20 mA temperature transmitter (with a 250 Ω shunt resistor), and a CO₂ sensor with a 0–3 V output. Normally, the ESP32’s ADC problems would force you into compromises. With a single ADS1115 on the I2C bus, all four sensors are read cleanly and the readings are posted to your server over Wi-Fi without interference between the two. The I2C bus runs independently of the radio hardware — they simply don’t share any silicon.

4. Load Cell Reading Without the HX711

A typical 50 kg load cell produces a differential output of around 2 mV/V excitation. With 3.3 V excitation, that’s a full-scale output of 6.6 mV — tiny. Set GAIN_SIXTEEN (±0.256 V full scale, 7.8 µV per step) and use differential mode on A0/A1. You’ll get around 850 usable steps across the 6.6 mV range — not quite HX711 territory for a kitchen scale, but entirely adequate for coarse load detection, overload protection, or relative weight comparison in a machine or dispenser.

Quick Troubleshooting Reference

  • ads.begin() returns false: Wrong I2C address, bad wiring, or missing pull-up resistors. Run an I2C scanner sketch first to confirm the device is visible on the bus.
  • Readings stuck at 32767 or -32768: Input voltage is outside the PGA range. Lower the gain setting (e.g., switch from GAIN_FOUR to GAIN_ONE) or check that your signal voltage matches what you expect.
  • Noisy, unstable readings: Add a 100 nF ceramic decoupling capacitor between VDD and GND close to the ADS1115 module. Also check that your signal source has a low impedance output — the ADS1115’s input impedance drops significantly at high sample rates.
  • Readings don’t match expected voltage: Confirm your gain setting. A common mistake is reading 3.3 V on GAIN_TWO (±2.048 V range), which will clip at the maximum count. Switch to GAIN_ONE or GAIN_TWOTHIRDS for signals approaching or exceeding 2 V.
  • ESP32: readings fine on bench, drift when Wi-Fi connects: Confirm you are using the ADS1115 over I2C, not the ESP32’s built-in ADC. If you accidentally left analogRead() calls in your code, those will behave exactly this way.

Closing

The ADS1115 is one of those components that quietly improves everything it touches. You wire it in once, install a library, and suddenly your analog measurements are an order of magnitude more accurate, your ESP32 reads sensors cleanly with Wi-Fi active, and you have four well-behaved channels instead of a collection of compromises. It belongs in every maker’s parts drawer.

Pick up an ADS1115 module and pair it with your ESP32 development board — they’re a natural combination for anything networked and precision-sensitive. If you’re starting from scratch or want a clean prototyping setup, grab a solderless breadboard to keep the wiring organised while you dial in your circuit before committing to a PCB.

The code examples above are complete and ready to run — copy, wire up, and read. Once you’ve got stable voltage readings on your Serial Monitor, you’ll understand why experienced makers reach for an external ADC without a second thought.

Leave a Reply

Your email address will not be published. Required fields are marked *