ADS1115 ADC with Raspberry Pi Pico & MicroPython Code

This tutorial explains how to interface Raspberry Pi Pico with the ADS1115 external ADC module and use MicroPython code to read analog signals through the module. Whether you’re working with temperature sensors, light sensors, or other analog devices, this guide will provide the essential steps to get your project up and running.

If you want to use Arduino code instead, check out my article:

Why is External ADC Required with Pico?

The pinout of Raspberry Pi Pico features four user-accessible 12-bit SAR-type ADCs.

  • RP2040 ADC does not have an on-chip voltage reference. It relies on the onboard SMPS’s 3.3 Volts supply, which isn’t great.
  • 12-bits of ADC resolution may not be enough for applications requiring high precision and accuracy.

The ADS1115 external ADC uses its own internal low-drift voltage reference which helps in noise rejection. This 16-bit ADC chip divides the input signal range into 65,536 ( 2^16) discrete steps.

Also read: How to Use ADC in Raspberry Pi Pico – MicroPython Example

Overview of ADS1115 ADC

The ADS1115 is a 16-bit Analog-to-Digital (ADC) converter IC manufactured by Texas Instruments. The IC can easily interface with microcontrollers/microprocessors by using the I2C protocol to send and receive data and commands. The IC supports four single-ended and two differential inputs. The differential inputs help to minimize noise in analog readings. The ADC supports a wide range of supply voltage and consumes very little current.

Features:

  • Range of ADC inputs: ±256 mV to ±6.144 V
  • Supply voltage range: 2.0 V to 5.5 V
  • Current consumption: 150 μA at Continuous-Conversion Mode
  • Programmable Data Rate: 8 samples per second (SPS) to 860 samples per second
  • Inbuilt programmable gain amplifier (PGA) and a digital comparator
  • 4 multiplexed inputs
  • Operates in either single-shot mode or continuous conversion mode

The ADS1115 IC is commonly available as breakout board modules. Some examples are shown in the image below:

ADS1115 ADC modules from different manufacturers

Block Diagram

Block Diagram of ADS1115 IC. Image credits: Texas instruments

The functional block diagram of ADS1115 illustrates how different parts of the IC work together to provide 16-bit digital data. ADS1115 consists of:

  • A delta-sigma ADC
  • A programmable gain amplifier (PGA)
  • An internal voltage reference
  • An internal oscillator for synchronization
  • An I2C interface for communication
  • An inbuilt comparator which can be used for over-voltage or under-voltage detection.

The inputs of ADS1115 are multiplexed through an input multiplexer (MUX) before being fed to the PGA. The device can measure four single-ended inputs or two differential inputs. While measuring single-ended inputs, the negative input of the ADC is internally connected to GND by a switch within the multiplexer.

The pairs of differential inputs are:

  • AIN0 and AIN1
  • AIN0 and AIN3
  • AIN1 and AIN3
  • AIN2 and AIN3

Pinout of ADS1115 Module

The diagram below shows the pinout of a generic ADS1115 breakout board module:

Pinout description:

PINDESCRIPTION
VDDPositive of power supply (2.0 Volts to 5.5 Volts)
GNDGround of power supply
SCLSerial Clock (I2C)
SDASerial Data (I2C)
ADDRConfigurable Address (I2C)
ALRTComparator output or conversion ready
A0Analog input 0
A1Analog input 1
A2Analog input 2
A3Analog input 3

For more information on ADS1115, you can refer to its datasheet.

The maximum voltage to the analog input pins should be limited to VDD ± 0.3 Volts

Components and Setup

Components used in this tutorial:

  • A Raspberry Pi Pico development board
  • ADS1115 ADC module
  • A potentiometer
  • Breadboard and connecting wires

The Raspberry Pi Pico must be flashed with a MicroPython UF2 file to program it in MicroPython. You can read our getting started guide for Raspberry Pi Pico where we show all steps required to start programming RP2040 in MicroPython. Any Micropython IDE such as Thonny or uPyCraft can be used.

Wiring Raspberry Pi Pico with ADS1115 ADC

The schematic below shows Raspberry Pi Pico connected to an ADS1115 ADC breakout board module and a 10 kiloohms potentiometer. You can also use other potentiometers with resistance in the kiloohms range.

Wiring Explained:

  • Connect the 3.3 Volt output pin of Raspberry Pi Pico to the VDD pin of ADS1115
  • Connect any Ground pin in Raspberry Pi Pico to the Ground pin of ADS1115. The third pin from any corner of Raspberry Pi Pico is a Ground pin
  • Connect Raspberry Pi Pico’s GPIO 16 to the SDA pin of ADS1115
  • Connect Raspberry Pi Pico’s GPIO 17 to the SCL pin of ADS1115
  • Connect 3.3 Volt and Ground to the outermost terminals of the potentiometer
  • Connect the middle pin of the potentiometer to the A0 pin of ADS1115

I2C Address of ADS1115

I2C devices have unique addresses. A microcontroller can use the same I2C bus to communicate with devices with different addresses. The ADS1115 can be configured to have a different I2C address by changing the connections to the ADDR pin. The default I2C address of ADS1115 is 0x48 in hexadecimal.

The following table illustrates how the I2C address of ADS1115 changes depending on the ADDR pin connection:

I2C Address (in hexadecimal)ADDR Pin Connection
0x49VDD
0x4ASDA
0x48GND
0x4BSCL

Thus, we can connect four ADS1115 modules to the same I2C bus by setting a different address for each module.

Also read: Raspberry Pi Pico I2C Communication Tutorial (MicroPython Code)

Scan I2C Address

Before interfacing any I2C module with a microcontroller, it is recommended to check the device’s I2C address. Let us see how a simple MicroPython script will display the I2C address of devices connected to the I2C bus.

First, ensure that you have wired the components according to the schematic above and that MicroPython firmware runs on Raspberry Pi Pico.

Upload the following script to Raspberry Pi Pico. I used Thonny IDE to upload and run code.

# Import the machine module for hardware access
import machine
import time

# Initialize the SDA (Serial Data Line) pin for I2C communication
sdaPIN = machine.Pin(16)

# Initialize the SCL (Serial Clock Line) pin for I2C communication
sclPIN = machine.Pin(17)

#Initialize the I2C interface with the specified pins and frequency, sda=sdaPIN, scl=sclPIN, freq=400000)
i2c=machine.I2C(0,sda=sdaPIN, scl=sclPIN, freq=400000)

time.sleep(0.2)

# Scan for devices connected to the I2C bus
devices = i2c.scan()

# Check if any devices are found
if len(devices) == 0:
    # If no devices are found, print a message
    print("No I2C devices found!")
else:
    # If devices are found, print the number of devices found
    print('I2C devices found:', len(devices))
    # Iterate through each device found and print its hexadecimal address
    for device in devices:
        print("Hexadecimal address:", hex(device))Code language: PHP (php)

Upon running the code, the I2C address of the ADS1115 ADC should be shown in the shell of Thonny IDE as shown in the screenshot below.

The default I2C address of ADS1115 is shown as 0x48 in hexadecimal. We do not need to connect wires to the ADDR pin for this default address. This address will be used later in our main code.

Install MicroPython Library for ADS1115

To send commands and read data from the ADS1115 IC, we shall use a MicroPython library that will provide easy-to-use functions.

Copy and paste the following code to your IDE. The code can also be found on this GitHub page.

# The MIT License (MIT)
#
# Copyright (c) 2016 Radomir Dopieralski (@deshipu),
#               2017 Robert Hammelrath (@robert-hh)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import utime as time

_REGISTER_MASK = const(0x03)
_REGISTER_CONVERT = const(0x00)
_REGISTER_CONFIG = const(0x01)
_REGISTER_LOWTHRESH = const(0x02)
_REGISTER_HITHRESH = const(0x03)

_OS_MASK = const(0x8000)
_OS_SINGLE = const(0x8000)  # Write: Set to start a single-conversion
_OS_BUSY = const(0x0000)  # Read: Bit=0 when conversion is in progress
_OS_NOTBUSY = const(0x8000)  # Read: Bit=1 when no conversion is in progress

_MUX_MASK = const(0x7000)
_MUX_DIFF_0_1 = const(0x0000)  # Differential P  =  AIN0, N  =  AIN1 (default)
_MUX_DIFF_0_3 = const(0x1000)  # Differential P  =  AIN0, N  =  AIN3
_MUX_DIFF_1_3 = const(0x2000)  # Differential P  =  AIN1, N  =  AIN3
_MUX_DIFF_2_3 = const(0x3000)  # Differential P  =  AIN2, N  =  AIN3
_MUX_SINGLE_0 = const(0x4000)  # Single-ended AIN0
_MUX_SINGLE_1 = const(0x5000)  # Single-ended AIN1
_MUX_SINGLE_2 = const(0x6000)  # Single-ended AIN2
_MUX_SINGLE_3 = const(0x7000)  # Single-ended AIN3

_PGA_MASK = const(0x0E00)
_PGA_6_144V = const(0x0000)  # +/-6.144V range  =  Gain 2/3
_PGA_4_096V = const(0x0200)  # +/-4.096V range  =  Gain 1
_PGA_2_048V = const(0x0400)  # +/-2.048V range  =  Gain 2 (default)
_PGA_1_024V = const(0x0600)  # +/-1.024V range  =  Gain 4
_PGA_0_512V = const(0x0800)  # +/-0.512V range  =  Gain 8
_PGA_0_256V = const(0x0A00)  # +/-0.256V range  =  Gain 16

_MODE_MASK = const(0x0100)
_MODE_CONTIN = const(0x0000)  # Continuous conversion mode
_MODE_SINGLE = const(0x0100)  # Power-down single-shot mode (default)

_DR_MASK = const(0x00E0)     # Values ADS1015/ADS1115
_DR_128SPS = const(0x0000)   # 128 /8 samples per second
_DR_250SPS = const(0x0020)   # 250 /16 samples per second
_DR_490SPS = const(0x0040)   # 490 /32 samples per second
_DR_920SPS = const(0x0060)   # 920 /64 samples per second
_DR_1600SPS = const(0x0080)  # 1600/128 samples per second (default)
_DR_2400SPS = const(0x00A0)  # 2400/250 samples per second
_DR_3300SPS = const(0x00C0)  # 3300/475 samples per second
_DR_860SPS = const(0x00E0)  # -   /860 samples per Second

_CMODE_MASK = const(0x0010)
_CMODE_TRAD = const(0x0000)  # Traditional comparator with hysteresis (default)
_CMODE_WINDOW = const(0x0010)  # Window comparator

_CPOL_MASK = const(0x0008)
_CPOL_ACTVLOW = const(0x0000)  # ALERT/RDY pin is low when active (default)
_CPOL_ACTVHI = const(0x0008)  # ALERT/RDY pin is high when active

_CLAT_MASK = const(0x0004)  # Determines if ALERT/RDY pin latches once asserted
_CLAT_NONLAT = const(0x0000)  # Non-latching comparator (default)
_CLAT_LATCH = const(0x0004)  # Latching comparator

_CQUE_MASK = const(0x0003)
_CQUE_1CONV = const(0x0000)  # Assert ALERT/RDY after one conversions
_CQUE_2CONV = const(0x0001)  # Assert ALERT/RDY after two conversions
_CQUE_4CONV = const(0x0002)  # Assert ALERT/RDY after four conversions
# Disable the comparator and put ALERT/RDY in high state (default)
_CQUE_NONE = const(0x0003)

_GAINS = (
    _PGA_6_144V,  # 2/3x
    _PGA_4_096V,  # 1x
    _PGA_2_048V,  # 2x
    _PGA_1_024V,  # 4x
    _PGA_0_512V,  # 8x
    _PGA_0_256V   # 16x
)

_GAINS_V = (
    6.144,  # 2/3x
    4.096,  # 1x
    2.048,  # 2x
    1.024,  # 4x
    0.512,  # 8x
    0.256  # 16x
)

_CHANNELS = {
    (0, None): _MUX_SINGLE_0,
    (1, None): _MUX_SINGLE_1,
    (2, None): _MUX_SINGLE_2,
    (3, None): _MUX_SINGLE_3,
    (0, 1): _MUX_DIFF_0_1,
    (0, 3): _MUX_DIFF_0_3,
    (1, 3): _MUX_DIFF_1_3,
    (2, 3): _MUX_DIFF_2_3,
}

_RATES = (
    _DR_128SPS,   # 128/8 samples per second
    _DR_250SPS,   # 250/16 samples per second
    _DR_490SPS,   # 490/32 samples per second
    _DR_920SPS,   # 920/64 samples per second
    _DR_1600SPS,  # 1600/128 samples per second (default)
    _DR_2400SPS,  # 2400/250 samples per second
    _DR_3300SPS,  # 3300/475 samples per second
    _DR_860SPS    # - /860 samples per Second
)


class ADS1115:
    def __init__(self, i2c, address=0x48, gain=1):
        self.i2c = i2c
        self.address = address
        self.gain = gain
        self.temp2 = bytearray(2)

    def _write_register(self, register, value):
        self.temp2[0] = value >> 8
        self.temp2[1] = value & 0xff
        self.i2c.writeto_mem(self.address, register, self.temp2)

    def _read_register(self, register):
        self.i2c.readfrom_mem_into(self.address, register, self.temp2)
        return (self.temp2[0] << 8) | self.temp2[1]

    def raw_to_v(self, raw):
        v_p_b = _GAINS_V[self.gain] / 32768
        return raw * v_p_b

    def set_conv(self, rate=4, channel1=0, channel2=None):
        """Set mode for read_rev"""
        self.mode = (_CQUE_NONE | _CLAT_NONLAT |
                     _CPOL_ACTVLOW | _CMODE_TRAD | _RATES[rate] |
                     _MODE_SINGLE | _OS_SINGLE | _GAINS[self.gain] |
                     _CHANNELS[(channel1, channel2)])

    def read(self, rate=4, channel1=0, channel2=None):
        """Read voltage between a channel and GND.
           Time depends on conversion rate."""
        self._write_register(_REGISTER_CONFIG, (_CQUE_NONE | _CLAT_NONLAT |
                             _CPOL_ACTVLOW | _CMODE_TRAD | _RATES[rate] |
                             _MODE_SINGLE | _OS_SINGLE | _GAINS[self.gain] |
                             _CHANNELS[(channel1, channel2)]))
        while not self._read_register(_REGISTER_CONFIG) & _OS_NOTBUSY:
            time.sleep_ms(1)
        res = self._read_register(_REGISTER_CONVERT)
        return res if res < 32768 else res - 65536

    def read_rev(self):
        """Read voltage between a channel and GND. and then start
           the next conversion."""
        res = self._read_register(_REGISTER_CONVERT)
        self._write_register(_REGISTER_CONFIG, self.mode)
        return res if res < 32768 else res - 65536

    def alert_start(self, rate=4, channel1=0, channel2=None,
                    threshold_high=0x4000, threshold_low=0, latched=False) :
        """Start continuous measurement, set ALERT pin on threshold."""
        self._write_register(_REGISTER_LOWTHRESH, threshold_low)
        self._write_register(_REGISTER_HITHRESH, threshold_high)
        self._write_register(_REGISTER_CONFIG, _CQUE_1CONV |
                             _CLAT_LATCH if latched else _CLAT_NONLAT |
                             _CPOL_ACTVLOW | _CMODE_TRAD | _RATES[rate] |
                             _MODE_CONTIN | _GAINS[self.gain] |
                             _CHANNELS[(channel1, channel2)])

    def conversion_start(self, rate=4, channel1=0, channel2=None):
        """Start continuous measurement, trigger on ALERT/RDY pin."""
        self._write_register(_REGISTER_LOWTHRESH, 0)
        self._write_register(_REGISTER_HITHRESH, 0x8000)
        self._write_register(_REGISTER_CONFIG, _CQUE_1CONV | _CLAT_NONLAT |
                             _CPOL_ACTVLOW | _CMODE_TRAD | _RATES[rate] |
                             _MODE_CONTIN | _GAINS[self.gain] |
                             _CHANNELS[(channel1, channel2)])

    def alert_read(self):
        """Get the last reading from the continuous measurement."""
        res = self._read_register(_REGISTER_CONVERT)
        return res if res < 32768 else res - 65536


class ADS1113(ADS1115):
    def __init__(self, i2c, address=0x48):
        super().__init__(i2c, address, 1)

    def raw_to_v(self, raw):
        return super().raw_to_v(raw)

    def read(self, rate=4):
        return super().read(rate, 0, 1)

    def alert_start(self, rate=4, threshold_high=0x4000, threshold_low=0, latched=False):
        return super().alert_start(rate, 0, 1, threshold_high, threshold_low, latched)

    def alert_read(self):
        return super().alert_read()


class ADS1114(ADS1115):
    def __init__(self, i2c, address=0x48, gain=1):
        super().__init__(i2c, address, gain)

    def raw_to_v(self, raw):
        return super().raw_to_v(raw)

    def read(self, rate=4):
        return super().read(rate, 0, 1)

    def alert_start(self, rate=4, threshold_high=0x4000, threshold_low=0, latched=False):
        return super().alert_start(rate, 0, 1, threshold_high,
            threshold_low, latched)

    def alert_read(self):
        return super().alert_read()


class ADS1015(ADS1115):
    def __init__(self, i2c, address=0x48, gain=1):
        super().__init__(i2c, address, gain)

    def raw_to_v(self, raw):
        return super().raw_to_v(raw << 4)

    def read(self, rate=4, channel1=0, channel2=None):
        return super().read(rate, channel1, channel2) >> 4

    def alert_start(self, rate=4, channel1=0, channel2=None, threshold_high=0x400,
        threshold_low=0, latched=False):
        return super().alert_start(rate, channel1, channel2, threshold_high << 4,
            threshold_low << 4, latched)

    def alert_read(self):
        return super().alert_read() >> 4

Save the library code as ADS1x15.py to your Raspberry Pi Pico.

MicroPython Code

With the library uploaded to Pico and wiring done according to the schematic, you can now upload code to read ADC values from ADS1115. The following MicroPython code will display the analog voltage input to the A0 input of ADS1115.

from machine import I2C, Pin
import time
from ads1x15 import ADS1115
i2c=I2C(0, sda=Pin(16), scl=Pin(17))
adc = ADS1115(i2c, address=72, gain=0)

while True:
    value = adc.read(4, 0)
    print("ADC Value:", value)
    voltage = adc.raw_to_v(value)
    print("Voltage", voltage)
    time.sleep(0.5)Code language: JavaScript (javascript)

Save the code to Raspberry Pi Pico and run it. If you save it as main.py, the script will run automatically on power-up.

When you run the code, the following kind of output will be displayed on the shell of the IDE:

Code Explanation

First, we import the necessary MicroPython modules. The machine module provides access to the hardware pins, time module is used for delays, and the ads1x15 module contains the driver for the ADS1115 ADC.

from machine import I2C, Pin
import time
from ads1x15 import ADS1115Code language: JavaScript (javascript)

Next, we initialize the I2C interface. The I2C bus is configured using pins 16 (SDA) and 17 (SCL) on the microcontroller. These pins are used for communication with the ADS1115.

i2c=I2C(0, sda=Pin(16), scl=Pin(17))

An ADS1115 object is initialized on the I2C bus. The I2C address is set to 72. The I2C address we got using the address scanner script (explained above) is 48 in hexadecimal which converts to 72 in decimal system.

adc = ADS1115(i2c, address=72, gain=0)

The gain parameter configures the full range of the ADC as follows:

GainFull Scale Range (Voltage)
0±6.144 V
1± 4.096 V
2±2.048
3±1.024
4±0.512
5±0.256

Main Loop

Inside the main loop, the code continuously reads the ADC values from ADS1115 and prints the value and corresponding voltage.

The line below reads the raw ADC value at 128 samples from the first ADC channel.

value = adc.read(4, 0)

For example, in the line of code: value = adc.read([rate, [channel1[, channel2]]]), channel1 is the single input channel. If channel2 is supplied, the difference between channel1 and channel2 is taken. The channels range from 0 to 3 corresponding to the three analog inputs in ADS1115. The rate is the conversion rate which can be one of the following:

Value in codeRate (in samples per second)
08
116
232
364
4128
5250
6475
7860

As we used the value ‘4’ in our code, the rate of conversion is 128 samples per second. As we increase the conversion rate, the noise in results also increases.

Next, we print the raw ADC values

print("ADC Value:", value)Code language: PHP (php)

The raw ADC result is converted to a voltage based on the gain we set earlier. A float value of the voltage is returned.

voltage = adc.raw_to_v(value)

Finally, we print the voltage read by the ADC and introduce a delay of 500 ms before the next reading.

print("Voltage", voltage)
time.sleep(0.5)Code language: CSS (css)

Demonstration

The steps to upload MicroPython code are explained here using Thonny IDE.

1. With all connections done according to the schematic, connect Pico to your computer using a USB cable. Open Thonny IDE and set the interpreter to use MicroPython on Raspberry Pi Pico.

Thonny IDE select interpreter as MicroPython Raspberry Pi Pico

2. Go to File>New in Thonny IDE to create a new project. 

3. Paste the MicroPython code into the new project, whether its the library code or the main script.

4. Click on File>Save as and select the save location as Raspberry Pi Pico.

Thonny Save to

5. Name the main code file as main.py or any other name with a “.py” extension.

main.py

6. Run the code by clicking the Run icon or by pressing the F5 key.

run-button-Thonny-1

When the code runs, you must see the ADC values in the output terminal change as you rotate the potentiometer.

The image below shows the breadboard layout of the circuit described earlier.

You can verify if the voltage output shown in the shell of the MicroPython IDE is correct by measuring with a multimeter. Set a multimeter to measure DC voltage and take the voltage reading between the center and ground positions of the potentiometer. The voltage in the multimeter should be the same as the voltage shown in the output when you run your main MicroPython script.

Final Thoughts

ADS1113 and ADS1114 are alternatives to the ADS1115 but lack some features. The ADS1113 and ADS1114 do not have an input multiplexer and can measure either one differential signal or one single-ended signal. They will be suitable for simple and cost-sensitive applications.

You can connect multiple ADS1115 modules to Raspberry Pi Pico by changing the I2C address as described in a section earlier. Each of these modules can measure up to four analog signals. These features enhance the usability of the ADS1115 in IoT, industrial monitoring, and hobbyist projects by reducing the need for additional ADCs or complex multiplexing circuits.

You can read the following articles that discuss using the internal ADC in Raspberry Pi Pico:


Posted

in

by

Comments

Leave a Reply

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