If you are reading a load cell, a precision thermistor, or a low-voltage current shunt, the stock 10-bit internal ADC on an Arduino Uno will bottleneck your project. The ATmega328P’s internal analog-to-digital converter yields only 1,024 discrete steps. On a 5V reference, that means a best-case resolution of 4.88mV per step, heavily compromised by internal switching noise. To achieve true millivolt or microvolt precision, you must interface your Arduino with ADC hardware built for the job—specifically, an external Sigma-Delta converter like the Texas Instruments ADS1115.

The ADS1115 provides 16-bit resolution (65,536 steps) and includes a Programmable Gain Amplifier (PGA) that lets you scale your input window down to ±256mV. Below is the complete bench guide to wiring, programming, and debugging the ADS1115 on the I2C bus, targeting the Arduino Uno R3 (and compatible Nano/Pro Mini ATmega328P boards).

Hardware Spec Sheet and Voltage Resolution Matrix

Before writing a single line of code, you must configure the ADS1115’s PGA. The PGA dictates your Full Scale Range (FSR) and the exact millivolt weight of the Least Significant Bit (LSB). A common bench mistake is leaving the PGA at the default ±6.144V setting while measuring a 0-1V sensor, effectively throwing away 80% of your 16-bit resolution.

Consult this matrix to select the correct gain multiplier in your firmware based on your sensor's maximum voltage output. Note that the ADS1115 is powered by 5V, but its analog inputs are referenced to its internal 2.048V bandgap, allowing it to safely measure voltages slightly above VDD when set to the 2/3x gain.

Table 1: ADS1115 PGA Gain Settings vs. Voltage Resolution
PGA Gain Setting Full Scale Range (FSR) LSB Size (Resolution) Max Safe Input Voltage Best Use Case
2/3x (Default) ±6.144 V 0.1875 mV VDD + 0.3V 0-5V Arduino logic, basic battery monitoring
1x ±4.096 V 0.125 mV VDD + 0.3V Standard industrial 0-3.3V analog sensors
2x ±2.048 V 0.0625 mV 4.096 V Thermocouple amplifiers, 0-2V pressure transducers
4x ±1.024 V 0.03125 mV 2.048 V MQ gas sensors, high-res potentiometers
8x ±0.512 V 0.015625 mV 1.024 V Current shunts (e.g., 50A/50mV shunt amplification)
16x ±0.256 V 0.0078125 mV 0.512 V Direct thermocouple reads, Wheatstone bridges

Required Parts List

  • Microcontroller: Arduino Uno R3 (ATmega328P) or Arduino Nano v3.
  • ADC Module: Adafruit ADS1115 16-Bit ADC Breakout (Product ID 1085) or equivalent generic module with 4.7kΩ pull-ups pre-populated.
  • Test Load: 10kΩ precision linear potentiometer (Bourns 3590 series or similar) for baseline calibration.
  • Passives: 0.1µF ceramic decoupling capacitor (place across VDD and GND at the ADC breakout if using a generic board lacking one).

Pin Mapping Table

Table 2: I2C Pin Mapping (Arduino Uno R3 to ADS1115)
ADS1115 Pin Arduino Uno R3 Pin Function / Notes
VDD 5V Power. Do not exceed 5.5V.
GND GND Common ground. Must share ground with sensor.
SCL A5 (I2C Clock) Requires 4.7kΩ pull-up to 5V.
SDA A4 (I2C Data) Requires 4.7kΩ pull-up to 5V.
ADDR GND Sets I2C address to 0x48. (See debugging section for alternatives).
A0 - A3 Sensor Output Analog inputs. Single-ended or differential.

Step-by-Step Wiring and I2C Bus Configuration

  1. Power and Decoupling: Connect VDD to 5V and GND to GND. If your breakout board does not have a decoupling capacitor, solder a 0.1µF ceramic capacitor directly across the VDD and GND header pins. The ADS1115 draws transient current during Sigma-Delta conversion; missing decoupling causes baseline jitter.
  2. Set the I2C Address: Tie the ADDR pin to GND. This configures the chip to respond to I2C hex address 0x48. If you plan to daisy-chain multiple ADCs later, you can tie ADDR to VDD (0x49), SDA (0x4A), or SCL (0x4B).
  3. Wire the I2C Bus: Connect SDA to A4 and SCL to A5. Bench Tip: The internal ATmega328P pull-ups are weak (~20kΩ). If your I2C wires exceed 6 inches, the bus capacitance will round off your clock edges, causing timeouts. Ensure your breakout has 4.7kΩ physical pull-up resistors, or add them externally to the 5V rail.
  4. Connect the Sensor: Wire your analog sensor output to the A0 pin on the ADS1115. Ensure the sensor shares the exact same GND plane as the Arduino and the ADC.

Complete C++ Firmware with I2C Error Handling

The following code targets the Arduino Uno R3. It utilizes the Adafruit_ADS1X15 library but improves upon standard examples by adding an explicit I2C bus ping check. This prevents the microcontroller from hanging indefinitely if the I2C lines are shorted or disconnected during boot.

Note: Install the "Adafruit ADS1X15" library via the Arduino Library Manager before compiling.

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

// Pin & Address Definitions
#define ADS_ADDRESS 0x48       // ADDR pin tied to GND
#define I2C_SDA_PIN A4         // Hardware I2C SDA on Uno
#define I2C_SCL_PIN A5         // Hardware I2C SCL on Uno

// Instantiate the ADC object
Adafruit_ADS1115 ads;

// Variable to hold the raw 16-bit integer
int16_t adc_raw;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(10); // Wait for serial port on Leonardo/Micro, harmless on Uno
  }
  
  Serial.println("Initializing I2C Bus...");
  Wire.begin();
  
  // 1. Pre-flight I2C Bus Check (Prevents Hard Lockups)
  Wire.beginTransmission(ADS_ADDRESS);
  byte i2c_error = Wire.endTransmission();
  
  if (i2c_error != 0) {
    Serial.print("FATAL: I2C device not found at 0x");
    Serial.println(ADS_ADDRESS, HEX);
    Serial.println("Check SDA/SCL wiring, pull-ups, and ADDR pin state.");
    while (1) {
      delay(1000); // Halt execution safely
    }
  }

  // 2. Initialize the ADS1115
  if (!ads.begin(ADS_ADDRESS)) {
    Serial.println("Failed to initialize ADS1115. Check I2C wiring!");
    while (1) {
      delay(1000);
    }
  }

  // 3. Configure ADC Parameters
  // Set Gain to 1x (+/- 4.096V) for standard 0-3.3V sensors
  ads.setGain(GAIN_ONE);
  
  // Set Data Rate to 128 SPS (Samples Per Second)
  // Lower SPS = higher noise rejection for DC signals
  ads.setDataRate(RATE_ADS1115_128SPS);
  
  Serial.println("ADS1115 Initialized Successfully. Reading A0...");
}

void loop() {
  // Read single-ended channel 0
  adc_raw = ads.readADC_SingleEnded(0);
  
  // Check for I2C read timeout/error (returns 0 or erratic on bus failure)
  if (adc_raw < 0) {
    Serial.println("ERROR: ADC read returned negative value. I2C bus fault.");
  } else {
    // Calculate voltage based on GAIN_ONE (0.125mV per bit)
    float voltage = adc_raw * 0.125 / 1000.0; 
    
    Serial.print("Raw: ");
    Serial.print(adc_raw);
    Serial.print(" | Voltage: ");
    Serial.print(voltage, 4);
    Serial.println(" V");
  }
  
  // Delay to match the 128 SPS sample rate (~7.8ms per sample)
  delay(10); 
}

Troubleshooting: I2C Timeouts and Noisy Baselines

When integrating external ADCs, the physical layer is usually where builds fail. If your serial monitor outputs FATAL: I2C device not found at 0x48 or the standard I2C Scanner sketch returns No I2C devices found, do not immediately assume the chip is dead. Follow this ranked diagnostic path.

The First Three Things to Check When It Fails

  1. The ADDR Pin State: The ADS1115 does not default to 0x48 if the ADDR pin is left floating; it will drift and fail to acknowledge. Ensure ADDR is firmly soldered or jumpered to GND, VDD, SDA, or SCL.
  2. I2C Pull-Up Resistors: Measure the voltage on the SDA and SCL lines with a multimeter. They should read a steady 5V (or 3.3V if using a 3.3V board) when idle. If they read 0V or float around 1.5V, you are missing pull-up resistors or the lines are shorted to ground.
  3. VDD vs VCC Logic Levels: If you are using a 3.3V Arduino (like a Due or an ESP32, though this code targets the 5V Uno), ensure the ADS1115 VDD is powered by 3.3V. Powering the ADS1115 with 5V while driving SDA/SCL with 3.3V logic often results in the ATmega/ESP recognizing the clock, but failing to read the data line due to threshold mismatches.

Ranked Causes for Noisy or Drifting Readings

If the code compiles and runs, but your voltage readings jump by ±10mV when they should be stable:

  • Cause 1: Ground Loops. The sensor ground and the ADS1115 ground must meet at a single star-ground point. If return currents from a motor or relay share the same thin ground wire as your sensor, the ADC will digitize that voltage drop.
  • Cause 2: Sample Rate Too High. The ADS1115 can sample up to 860 SPS. At high speeds, internal digital noise bleeds into the analog front end. Drop the ads.setDataRate() to RATE_ADS1115_32SPS or 16SPS for highly stable DC measurements.
  • Cause 3: Source Impedance. While the ADS1115 has a high input impedance, driving it from a high-impedance voltage divider (e.g., two 1MΩ resistors) without a bypass capacitor at the A0 pin will cause the internal sampling capacitor to pull the line down during conversion. Add a 100nF capacitor between A0 and GND.

Scaling the Build: Simplify or Extend

Not every project requires 16-bit precision, and some require more than single-ended reads. Here is how to adapt this architecture based on your actual bench requirements.

How to Simplify the Build (Drop the External Hardware)

If you realize your application only needs 11 or 12 bits of resolution and you want to eliminate the I2C wiring, you can use the Arduino's internal 10-bit ADC combined with oversampling. By taking 16 rapid readings and averaging them, you mathematically gain 2 bits of resolution (effectively achieving 12-bit precision). This requires adding a small amount of intentional noise (dithering) to the signal, but for basic battery voltage monitoring or crude light sensing, it saves the cost and complexity of the ADS1115 module entirely.

How to Extend the Build (Differential and Industrial Reads)

To extend this build for harsh, noisy environments, switch from readADC_SingleEnded(0) to readADC_Differential_0_1(). In differential mode, the ADS1115 measures the voltage difference between A0 and A1, completely ignoring any common-mode noise (like 60Hz mains hum) that is induced equally onto both wires. This is the mandatory approach when reading current shunts or remote Wheatstone bridges over long twisted-pair cables.

Furthermore, if you need to log data remotely, swap the Arduino Uno R3 for an ESP32 DevKit V1. The ESP32 has its own notoriously noisy internal ADCs, making the addition of an I2C ADS1115 practically mandatory for precision sensor work on the Espressif platform. The exact C++ code provided above will compile and run on the ESP32 via the Arduino IDE with zero modifications, provided you update the Wire pins to the ESP32's default I2C bus (GPIO 21 for SDA, GPIO 22 for SCL).

Bench Safety Note: Never feed a voltage higher than VDD + 0.3V into the analog inputs of the ADS1115. While the PGA can mathematically handle a ±6.144V FSR, the physical silicon input multiplexer will break down and permanently short if the absolute pin voltage exceeds the supply rail by more than 0.3V. Always use a resistor voltage divider or op-amp clamp for high-voltage transducer inputs.

For deeper technical specifications on the Sigma-Delta architecture and internal register mapping, refer to the Texas Instruments ADS1115 Datasheet.