5 ADS1115 ADC Projects That Add 16-bit Precision to Any Arduino
Build precision measurement systems: hydroponic nutrient monitor, EEG signal acquisition, precision weighing scale, environmental data logger, and strain gauge reader
The ADS1115 is a 16-bit I²C analog-to-digital converter that solves the Arduino ADC problem once and for all. The Arduino Nano and Uno have 10-bit ADC — that is only 1,024 discrete levels across a 5V range, which means the smallest detectable voltage step is 4.88mV. For many sensor applications that is not enough. The ADS1115 gives you 65,536 levels (16-bit) across the same 5V range, bringing the resolution down to 0.076mV per LSB — a 16x improvement. It has four differential input channels or eight single-ended inputs, a programmable gain amplifier, and costs less than $5.
In this guide, we build five precision measurement projects that would be impossible or unreliable with the standard Arduino ADC.
Topics covered: I²C address configuration for multiple ADS1115, differential vs single-ended measurement, PGA gain setting, 4-20mA current loop interfacing, Wheatstone bridge signal conditioning, strain gauge amplification, oversampling and noise reduction.
What You'll Need
- ADS1115 (×1-2 depending on project)
- Arduino Nano or Uno (×1)
- Screw terminal adapter (×1, for secure sensor wiring)
- OLED 128×64 I²C display (×1)
- pH sensor module (×1, for hydroponics)
- Soil moisture sensor (×1)
- DS18B20 temperature sensor (×1)
- Load cell + HX711 (×1, for weighing scale)
- Strain gauge (×1, for strain measurement)
- 4-20mA current loop sensor (×1, for industrial applications)
- Jumper wires and breadboard
- USB cable for programming
Why ADS1115 Instead of Arduino Due?
Arduino Due has a 12-bit ADC (4096 levels) — better than standard Arduino but still 4x worse than ADS1115. ADS1115 is cheaper, works with all Arduino boards, and uses I²C so it does not consume any analog pins.
| Property | Arduino Nano (built-in ADC) | Arduino Due | ADS1115 |
|---|---|---|---|
| Resolution | 10-bit (1024 levels) | 12-bit (4096 levels) | 16-bit (65536 levels) |
| Voltage step | 4.88mV | 1.22mV | 0.076mV |
| Interface | Built-in analog pins | Dedicated analog pins | I²C (2 pins) |
| Channels | 8 single-ended | 12 single-ended | 4 differential / 8 single-ended |
| Cost | $0 (included) | $20+ | $3-5 |
| PGA | No | No | Yes (1x to 16x) |
How ADS1115 Works
Technical Principle
The ADS1115 uses a successive approximation register (SAR) ADC architecture. It samples an analog voltage, converts it through a 16-bit DAC feedback loop, and outputs the result over I²C. The built-in Programmable Gain Amplifier (PGA) can amplify small signals (up to 16x) before conversion, extending effective resolution for low-voltage sensors. You can choose from four data rates: 8, 64, 128, or 860 samples per second.
Wiring Diagram (I²C)
Arduino ADS1115
A4 (SDA) ──── SDA
A5 (SCL) ──── SCL
5V ──── VDD
GND ──── GND
ADS1115 address pins (A0, A1, A2):
All GND = address 0x48 (default)
A0→VDD = 0x49
A1→VDD = 0x4A
A2→VDD = 0x4B
Basic Example Code
// WF1 Run #052 - Basic ADS1115 16-bit ADC Reading
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
Adafruit_ADS1115 ads;
void setup() {
Serial.begin(115200);
ads.begin();
// Set PGA gain: ADS1115_PGA_2 = 2x gain, range ±2.048V
ads.setGain(ADS1115_PGA_2);
}
void loop() {
int16_t adc0 = ads.readADC_SingleEnded(0);
// Convert to voltage
float voltage = ads.computeVolts(adc0);
Serial.print("ADC0: ");
Serial.print(adc0);
Serial.print(" (");
Serial.print(voltage, 6);
Serial.println(" V)");
delay(500);
}
Project 1: Hydroponic Nutrient Monitor
Goal: Monitor pH, electrical conductivity (EC), and temperature of hydroponic nutrient solution with millivolt precision, triggering alerts when values leave the acceptable range
Hardware
- ADS1115 (×1)
- Arduino Nano (×1)
- pH sensor module (×1)
- Soil moisture sensor (analog) (×1)
- DS18B20 temperature sensor (×1)
- OLED 128×64 I²C display (×1)
Code
// WF1 Run #052 - Project 1: Hydroponic Nutrient Monitor
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_ADS1115 ads;
OneWire oneWire(A3); // DS18B20 on pin A3
DallasTemperature sensors(&oneWire);
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// pH thresholds (in ADC raw units at PGA_4)
#define PH_MIN_RAW 22000
#define PH_MAX_RAW 28000
#define EC_MIN_RAW 18000
#define EC_MAX_RAW 25000
void setup() {
Serial.begin(115200);
ads.begin();
ads.setGain(ADS1115_PGA_4); // ±2.048V range for pH/EC sensors
sensors.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
}
void loop() {
// Read pH sensor (analog output from pH module)
int16_t ph_raw = ads.readADC_SingleEnded(0);
float ph_voltage = ads.computeVolts(ph_raw);
// pH module outputs 2.5V at pH 7.0, varies ±1V across pH 4-10
float pH = 7.0 + (2.5 - ph_voltage) / 0.18; // Approximate calibration
// Read EC sensor (conductivity probe)
int16_t ec_raw = ads.readADC_SingleEnded(1);
float ec_voltage = ads.computeVolts(ec_raw);
// Read temperature
sensors.requestTemperatures();
float tempC = sensors.getTempCByIndex(0);
// Display
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("HYDROPONIC MONITOR");
display.setTextSize(2);
display.setCursor(0, 16);
display.print("pH: ");
display.println(pH, 2);
display.print("EC: ");
display.print(ec_voltage, 3);
display.println(" V");
display.setTextSize(1);
display.print("Temp: ");
display.print(tempC, 1);
display.println(" C");
// Alert indicators
display.setTextSize(1);
display.setCursor(0, 48);
if (pH < 5.5 || pH > 6.5) display.print("pH OUT OF RANGE!");
else display.print("pH OK");
display.display();
// Serial for data logging
Serial.print("pH: "); Serial.print(pH, 2);
Serial.print(" | EC: "); Serial.print(ec_voltage, 3);
Serial.print(" V | Temp: "); Serial.println(tempC, 1);
delay(2000);
}
Project 2: Precision Weighing Scale
Goal: Build a 0.01g resolution weighing scale using a load cell and HX711 — but use ADS1115 with a precision instrumentation amplifier instead of the common HX711 module for higher accuracy
Hardware
- ADS1115 (×1)
- Arduino Nano (×1)
- Load cell (5kg rated) (×1)
- Instrumentation amplifier (INA122P) (×1)
- Calibration weight (100g)
- OLED 128×64 I²C display (×1)
Code
// WF1 Run #052 - Project 2: Precision Weighing Scale
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_ADS1115 ads;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Calibration values (determine by measuring known weights first)
#define CALIBRATION_FACTOR 420.5 // raw units per gram
#define TARE_OFFSET 0 // set to current reading when scale is empty
float scaleOffset = 0;
bool calibrated = false;
void setup() {
Serial.begin(115200);
ads.begin();
ads.setGain(ADS1115_PGA_1); // ±4.096V range for load cell via instrumentation amp
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
// Tare on startup
delay(1000);
float sum = 0;
for (int i = 0; i < 50; i++) sum += ads.readADC_SingleEnded(0);
scaleOffset = sum / 50.0;
Serial.println("Scale tared. Place calibration weight.");
}
void loop() {
// Oversample 64 readings for noise reduction
float sum = 0;
for (int i = 0; i < 64; i++) sum += ads.readADC_SingleEnded(0);
float avgRaw = sum / 64.0;
float netRaw = avgRaw - scaleOffset;
// Convert to grams
float weightG = netRaw / CALIBRATION_FACTOR;
// Display
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("PRECISION SCALE");
display.setTextSize(3);
display.setCursor(0, 20);
if (weightG >= 0) display.print("+");
display.print(weightG, 2);
display.setTextSize(1);
display.setCursor(100, 52);
display.print("g");
// Stability indicator
float stdDev = 0;
for (int i = 0; i < 20; i++) {
int16_t r = ads.readADC_SingleEnded(0);
stdDev += pow(r - avgRaw, 2);
}
stdDev = sqrt(stdDev / 20.0);
display.setTextSize(1);
display.setCursor(0, 52);
if (stdDev < 5) display.print("STABLE");
else if (stdDev < 20) display.print("VARIED");
else display.print("UNSTABLE");
display.display();
Serial.print("Weight: ");
Serial.print(weightG, 3);
Serial.print(" g | Noise: ");
Serial.println(stdDev, 1);
delay(200);
}
Project 3: Environmental Data Logger
Goal: Log temperature, humidity, pressure, and light levels to SD card with timestamps — use ADS1115 for higher accuracy on the analog sensors
Hardware
- ADS1115 (×1)
- Arduino Nano (×1)
- DHT22 temperature/humidity sensor (×1)
- BMP280 pressure sensor (I²C) (×1)
- LDR (photoresistor) (×1)
- SD card module (×1)
- Real-time clock DS3231 (×1)
Code
// WF1 Run #052 - Project 3: Environmental Data Logger
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <DHT.h>
#include <DHT.h>
#include <RTClib.h>
#include <SD.h>
#include <SPI.h>
Adafruit_ADS1115 ads;
#define DHT_PIN A3
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
RTC_DS3231 rtc;
#define SD_CS_PIN 10
#define LDR_PIN 0 // ADS1115 channel 0
File logFile;
void setup() {
Serial.begin(115200);
ads.begin();
ads.setGain(ADS1115_PGA_1);
dht.begin();
rtc.begin();
SD.begin(SD_CS_PIN);
// Create log file
DateTime now = rtc.now();
char filename[16];
snprintf(filename, 16, "ENV_%04u%02u%02u.CSV", now.year(), now.month(), now.day());
logFile = SD.open(filename, FILE_WRITE);
if (logFile) {
logFile.println("datetime,temp_c,humidity_pct,pressure_hPa,light_adc");
logFile.close();
}
Serial.print("Logging to: ");
Serial.println(filename);
}
void loop() {
// Read temperature and humidity from DHT22
float temp = dht.readTemperature();
float humidity = dht.readHumidity();
// Read LDR via ADS1115 (more stable than Arduino ADC)
int16_t lightRaw = ads.readADC_SingleEnded(LDR_PIN);
float lightPct = map(lightRaw, 0, 32767, 0, 100); // 0-100% light
// Timestamp
DateTime now = rtc.now();
char timestamp[20];
snprintf(timestamp, 20, "%04u-%02u-%02u %02u:%02u:%02u",
now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
// Write to SD
logFile = SD.open("ENV_LOG.CSV", FILE_WRITE);
if (logFile) {
logFile.print(timestamp);
logFile.print(",");
logFile.print(temp, 1);
logFile.print(",");
logFile.print(humidity, 1);
logFile.print(",");
logFile.print(lightPct, 0);
logFile.print(",");
logFile.println(lightRaw);
logFile.close();
}
// Serial output
Serial.print(timestamp);
Serial.print(" | T: ");
Serial.print(temp, 1);
Serial.print("C | H: ");
Serial.print(humidity, 1);
Serial.print("% | Light: ");
Serial.print(lightPct, 0);
Serial.print("% (ADC: ");
Serial.print(lightRaw);
Serial.println(")");
delay(60000); // Log every minute
}
Project 4: 4-20mA Current Loop Receiver
Goal: Read industrial 4-20mA current loop sensors using ADS1115 as the current-to-voltage converter — common for pressure, flow, and level sensors in industrial environments
Hardware
- ADS1115 (×1)
- Arduino Nano (×1)
- 250Ω precision resistor (×1, converts 4-20mA to 1-5V)
- 4-20mA pressure sensor (×1, or any industrial sensor)
- OLED 128×64 I²C display (×1)
Code
// WF1 Run #052 - Project 4: 4-20mA Current Loop Receiver
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_ADS1115 ads;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// 4-20mA loop: 4mA = 0% sensor value, 20mA = 100% sensor value
// 250Ω resistor: 4mA × 250Ω = 1.0V, 20mA × 250Ω = 5.0V
// ADS1115 PGA_1 range: ±4.096V, so full 5V span uses most of the range
#define R_SENSE 250.0 // Ohm
void setup() {
Serial.begin(115200);
ads.begin();
ads.setGain(ADS1115_PGA_1); // ±4.096V — covers full 1-5V from 250Ω resistor
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
}
void loop() {
int16_t adcRaw = ads.readADC_SingleEnded(0);
float voltage = ads.computeVolts(adcRaw);
float current_mA = (voltage / R_SENSE) * 1000.0; // V / Ω = A → mA
// Map 4-20mA to 0-100%
float sensor_pct = map((long)(current_mA * 1000), 4000, 20000, 0, 10000) / 100.0;
// Display
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("4-20mA LOOP RECEIVER");
display.setTextSize(2);
display.setCursor(0, 20);
display.print("I: ");
display.print(current_mA, 2);
display.println(" mA");
display.setTextSize(2);
display.print("Val: ");
display.print(sensor_pct, 1);
display.println(" %");
display.setTextSize(1);
display.setCursor(0, 50);
if (current_mA < 3.5) display.print("LOOP ERROR");
else if (current_mA < 4.0) display.print("BELOW RANGE");
else if (current_mA > 21.0) display.print("ABOVE RANGE");
else display.print("NORMAL");
display.display();
Serial.print("Current: ");
Serial.print(current_mA, 3);
Serial.print(" mA | Sensor: ");
Serial.print(sensor_pct, 1);
Serial.println(" %");
delay(500);
}
Project 5: Strain Gauge Reader
Goal: Read a strain gauge bonded to a metal cantilever beam using ADS1115 with a Wheatstone bridge interface — detect microstrain changes with 0.001% resolution
Hardware
- ADS1115 (×1)
- Arduino Nano (×1)
- Strain gauge (120Ω, bonded) (×1)
- Instrumentation amplifier INA122P (×1)
- Precision resistors for Wheatstone bridge (×4, 120Ω matched)
- OLED 128×64 I²C display (×1)
Code
// WF1 Run #052 - Project 5: Strain Gauge Reader
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_ADS1115 ads;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Wheatstone bridge: R1=R2=R3=Rgauge = 120Ω at zero strain
// Gauge factor G = 2.0 (typical for foil gauge)
// Microstrain = (dR/R) / G * 10^6
// With ADS1115 PGA_8: each LSB = 0.5mV → strain resolution ~1 microstrain
void setup() {
Serial.begin(115200);
ads.begin();
ads.setGain(ADS1115_PGA_8); // ±0.512V range for Wheatstone bridge differential output
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("STRAIN GAUGE READER");
display.println("Place beam in unloaded state");
display.println("Press reset to zero...");
display.display();
delay(3000);
}
void loop() {
// Read differential voltage between A0 (INP) and A1 (INN)
int16_t diffRaw = ads.readADC_Differential_0_1();
float voltage_uV = ads.computeVolts(diffRaw) * 1000000.0; // microvolts
// Convert to microstrain
// V_excitation = 3.3V (from Arduino)
// GF = 2.0, R_gauge = 120Ω
// microstrain = (4 * voltage_uV / 3.3V) / 2.0
float microstrain = (4.0 * voltage_uV) / (3.3 * 2.0);
// Display
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("STRAIN GAUGE");
display.setTextSize(3);
display.setCursor(0, 20);
display.print(microstrain, 1);
display.setTextSize(1);
display.setCursor(0, 52);
display.print("microstrain (ue)");
display.setTextSize(1);
display.setCursor(100, 0);
display.print("dV:");
display.print(voltage_uV, 0);
display.print(" uV");
display.display();
Serial.print("dV: ");
Serial.print(voltage_uV, 0);
Serial.print(" uV | Strain: ");
Serial.print(microstrain, 3);
Serial.println(" ue");
delay(100);
}
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| ADS1115 not found on I²C scan | Wrong SDA/SCL pins on your board | ESP32 uses GPIO21/22; Due uses SDA1/SCL1; verify correct pins |
| Readings jump by ±10-50 counts | PGA gain too high, input exceeds range | Lower PGA gain; ensure input voltage never exceeds selected range |
| All channels read the same value | All sensors reading floating inputs | Connect actual sensors; add pull-down resistors on unused channels |
| ADS1115 works but Arduino ADC does not | Normal — they are separate ADCs | Use ADS1115 for precision, Arduino ADC for fast/dirty readings |
| Differential reading very small | Common-mode voltage issue | Ensure both differential inputs are within supply range; use instrumentation amp for very small signals |
| SD card fails to initialize | CS pin conflict or card format | Try different CS pin; format SD card as FAT16 or FAT32 |
Start Here
Affiliate disclosure: As an Amazon Associate, I earn from qualifying purchases.
The right parts make the difference:
ADS1115 ADC module — 16-bit I²C ADC with PGA and four differential inputs. The upgrade that makes any Arduino project precision-capable for about the cost of a pizza.
Arduino Nano CH340 — compact breadboard-compatible microcontroller. Works perfectly with ADS1115 over I²C for precision measurement projects.
pH sensor module for Arduino — BNC connector pH probe with analog output. Pair with ADS1115 for hydroponics, aquarium, or lab automation.
Load cell 5kg with HX711 — for weighing scale projects. While HX711 is common, ADS1115 gives you more flexibility for custom amplification circuits.
SD card module — for data logging projects. Required for the environmental data logger.
Next Step: Precision Measurement Design for Your Project
If this guide showed you how ADS1115 can solve your project's precision problem — but you need help designing the signal conditioning circuit for your specific sensor — I can help you design that.
I offer a personalized interactive device design guide at Fiverr:
👉 https://www.fiverr.com/phd_hfchang/generate-an-arduino-interactive-prototypef
What you get:
- ADS1115 configuration optimized for your sensor output range
- Signal conditioning circuit design (instrumentation amp, Wheatstone bridge, current loop)
- Calibration procedure with pass/fail criteria
- Arduino code with oversampling for maximum effective resolution
Tags: Arduino, ADS1115, ADC, 16-bit, precision measurement, I2C, hydroponics, strain gauge, data logger, current loop, weighing scale


Top comments (0)