Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

ADS1115 on STM32F103: Configuration and I²C Readout with Example Code

Tech 2

ADS1115 at a glance

  • 16‑bit SAR ADC, low power
  • Four analog inputs: AIN0–AIN3 (single‑ended) or differential pairs
  • Supply: 2.0–5.5 V on VDD, GND reference
  • Digital interface: I²C (SCL, SDA)
  • Address select: ADDR pin (GND/ VDD/ SDA/ SCL)
  • Optional comparator/alert output: ALERT/RDY

Typical wiring to STM32F103 (3.3 V system):

  • VDD → 3V3
  • GND → GND
  • ADDR → GND (7‑bit I²C address 0x48)
  • SDA/SCL → MCU SDA/SCL (with pull‑ups)
  • ALERT/RDY → leave unconnected unless comparator/ready is used

Note on I²C address: ADS1115 uses a 7‑bit address. With ADDR=GND it is 0x48. Some code bases show the 8‑bit "wire format" address (0x90 for write, 0x91 for read). Both represent the same device.


Brief I²C recap (start/stop/ACK/NACK)

  • Start (S): while SCL is high, SDA transitions high→low
  • Stop (P): while SCL is high, SDA transitions low→high
  • ACK: receiver pulls SDA low during the 9th SCL pulce
  • NACK: receiver leaves SDA high during the 9th SCL pulse

The following software I²C snippets demonstrate the signaling. Replace the pin accesors and delays with your board’s implementation.

// soft_i2c.h — minimal bit‑banged I²C interface (GPIO + delays required)
#include <stdint.h>
#include <stdbool.h>

// Platform hooks to implement
void i2c_gpio_sda_out(void);
void i2c_gpio_sda_in(void);
void i2c_gpio_set_sda(int level);
void i2c_gpio_set_scl(int level);
int  i2c_gpio_get_sda(void);
void i2c_delay_us(unsigned int us);

void i2c_init(void);
void i2c_start(void);
void i2c_stop(void);
bool i2c_write_byte(uint8_t byte);
uint8_t i2c_read_byte(bool ack);
// soft_i2c.c — simple implementation
#include "soft_i2c.h"

void i2c_init(void) {
    i2c_gpio_sda_out();
    i2c_gpio_set_sda(1);
    i2c_gpio_set_scl(1);
}

static void i2c_clock_tick(void) {
    i2c_gpio_set_scl(1);
    i2c_delay_us(3);
    i2c_gpio_set_scl(0);
}

void i2c_start(void) {
    i2c_gpio_sda_out();
    i2c_gpio_set_sda(1);
    i2c_gpio_set_scl(1);
    i2c_delay_us(3);
    i2c_gpio_set_sda(0);   // START: SDA high→low while SCL high
    i2c_delay_us(3);
    i2c_gpio_set_scl(0);
}

void i2c_stop(void) {
    i2c_gpio_sda_out();
    i2c_gpio_set_sda(0);
    i2c_gpio_set_scl(1);
    i2c_delay_us(3);
    i2c_gpio_set_sda(1);   // STOP: SDA low→high while SCL high
    i2c_delay_us(3);
}

bool i2c_write_byte(uint8_t byte) {
    i2c_gpio_sda_out();
    for (int i = 7; i >= 0; --i) {
        i2c_gpio_set_sda((byte >> i) & 0x01);
        i2c_clock_tick();
    }
    // Read ACK
    i2c_gpio_sda_in();
    i2c_gpio_set_scl(1);
    i2c_delay_us(3);
    bool ack = (i2c_gpio_get_sda() == 0);
    i2c_gpio_set_scl(0);
    i2c_gpio_sda_out();
    return ack;
}

uint8_t i2c_read_byte(bool ack) {
    uint8_t data = 0;
    i2c_gpio_sda_in();
    for (int i = 7; i >= 0; --i) {
        i2c_gpio_set_scl(1);
        i2c_delay_us(3);
        data |= (i2c_gpio_get_sda() & 1) << i;
        i2c_gpio_set_scl(0);
        i2c_delay_us(3);
    }
    // Send ACK/NACK
    i2c_gpio_sda_out();
    i2c_gpio_set_sda(ack ? 0 : 1);
    i2c_clock_tick();
    i2c_gpio_set_sda(1);
    return data;
}

ADS1115 registers and configuration feilds

  • Pointer register (write one byte before data access):

    • 0x00: Conversion register (read 16‑bit)
    • 0x01: Config register (write/read 16‑bit)
    • 0x02: Lo_thresh (comparator)
    • 0x03: Hi_thresh (comparator)
  • Config register bit map (MSB→LSB):

    • [15] OS (1 too start single conversion; reads 0 while converting, 1 when ready)
    • [14:12] MUX (input selection)
    • [11:9] PGA (full‑scale range)
    • [8] MODE (0=continuous, 1=single‑shot)
    • [7:5] DR (data rate)
    • [4] COMP_MODE
    • [3] COMP_POL
    • [2] COMP_LAT
    • [1:0] COMP_QUE (0–2 enable queue, 3 disables comparator)

Convenient bit masks (high/low byte accurate to the datasheet):

// ads1115.h — register addresses and field masks
#pragma once
#include <stdint.h>

#define ADS1115_PTR_CONVERT   0x00
#define ADS1115_PTR_CONFIG    0x01
#define ADS1115_PTR_LO_THR    0x02
#define ADS1115_PTR_HI_THR    0x03

// 7‑bit I²C base address when ADDR=GND
#define ADS1115_ADDR_7BIT     0x48

// Config register fields
#define ADS1115_OS_SINGLE     (1u << 15)

// MUX (single‑ended and differential)
#define ADS1115_MUX_AIN0_AIN1 (0u << 12)
#define ADS1115_MUX_AIN0_AIN3 (1u << 12)
#define ADS1115_MUX_AIN1_AIN3 (2u << 12)
#define ADS1115_MUX_AIN2_AIN3 (3u << 12)
#define ADS1115_MUX_AIN0_GND  (4u << 12)
#define ADS1115_MUX_AIN1_GND  (5u << 12)
#define ADS1115_MUX_AIN2_GND  (6u << 12)
#define ADS1115_MUX_AIN3_GND  (7u << 12)

// PGA (full‑scale range)
#define ADS1115_PGA_6V144     (0u << 9)
#define ADS1115_PGA_4V096     (1u << 9)
#define ADS1115_PGA_2V048     (2u << 9)
#define ADS1115_PGA_1V024     (3u << 9)
#define ADS1115_PGA_0V512     (4u << 9)
#define ADS1115_PGA_0V256     (5u << 9) // 5,6,7 map to the same 0.256 V range

#define ADS1115_MODE_CONT     (0u << 8)
#define ADS1115_MODE_SINGLE   (1u << 8)

// Data rate
#define ADS1115_DR_8SPS       (0u << 5)
#define ADS1115_DR_16SPS      (1u << 5)
#define ADS1115_DR_32SPS      (2u << 5)
#define ADS1115_DR_64SPS      (3u << 5)
#define ADS1115_DR_128SPS     (4u << 5)
#define ADS1115_DR_250SPS     (5u << 5)
#define ADS1115_DR_475SPS     (6u << 5)
#define ADS1115_DR_860SPS     (7u << 5)

// Comparator disable
#define ADS1115_COMP_WINDOW   (1u << 4) // comparator mode
#define ADS1115_COMP_ACTIVE_H (1u << 3) // polarity
#define ADS1115_COMP_LATCH    (1u << 2) // latch
#define ADS1115_COMP_QUE(x)   ((x) & 0x3) // 0–3; 3 disables comparator
#define ADS1115_COMP_DISABLE  (0x3)

// LSB sizes for voltage conversion
static inline float ads1115_lsb_volts(uint16_t pga_field) {
    switch (pga_field) {
        case ADS1115_PGA_6V144: return 6.144f / 32768.0f; // ~0.1875 mV
        case ADS1115_PGA_4V096: return 4.096f / 32768.0f;
        case ADS1115_PGA_2V048: return 2.048f / 32768.0f;
        case ADS1115_PGA_1V024: return 1.024f / 32768.0f;
        case ADS1115_PGA_0V512: return 0.512f / 32768.0f;
        default:                return 0.256f / 32768.0f; // 0.256 V
    }
}

Low‑level register access helpers (software I²C)

// ads1115_ll.c — basic read/write over bit‑banged I²C
#include "soft_i2c.h"
#include "ads1115.h"

static inline uint8_t addr8(uint8_t addr7, int read) {
    return (uint8_t)((addr7 << 1) | (read ? 1 : 0));
}

static bool ads1115_write16(uint8_t addr7, uint8_t reg, uint16_t val) {
    i2c_start();
    if (!i2c_write_byte(addr8(addr7, 0))) { i2c_stop(); return false; }
    if (!i2c_write_byte(reg))             { i2c_stop(); return false; }
    // MSB first
    if (!i2c_write_byte((uint8_t)(val >> 8))) { i2c_stop(); return false; }
    if (!i2c_write_byte((uint8_t)(val & 0xFF))) { i2c_stop(); return false; }
    i2c_stop();
    return true;
}

static bool ads1115_read16(uint8_t addr7, uint8_t reg, uint16_t *out) {
    // Write pointer register
    i2c_start();
    if (!i2c_write_byte(addr8(addr7, 0))) { i2c_stop(); return false; }
    if (!i2c_write_byte(reg))             { i2c_stop(); return false; }
    // Repeated START, switch to read
    i2c_start();
    if (!i2c_write_byte(addr8(addr7, 1))) { i2c_stop(); return false; }
    uint8_t msb = i2c_read_byte(true);
    uint8_t lsb = i2c_read_byte(false); // NACK last byte
    i2c_stop();
    *out = (uint16_t)((msb << 8) | lsb);
    return true;
}

ADS1115 configuration and single‑shot conversion

The configuration sequence:

  1. Build the 16‑bit config word (MUX, PGA, mode, data rate, comparator disabled).
  2. Write the config register.
  3. Wait until conversion completes (OS bit reads back as 1) or wait long enough based on DR.
  4. Read the 16‑bit conversion register.
// ads1115.c — single‑shot read API
typedef struct {
    uint8_t i2c_addr7;   // e.g., 0x48 for ADDR=GND
    uint16_t pga_field;  // one of ADS1115_PGA_* (kept for scaling)
} ads1115_t;

static bool ads1115_read_config(uint8_t addr7, uint16_t *cfg) {
    return ads1115_read16(addr7, ADS1115_PTR_CONFIG, cfg);
}

static bool ads1115_write_config(uint8_t addr7, uint16_t cfg) {
    return ads1115_write16(addr7, ADS1115_PTR_CONFIG, cfg);
}

bool ads1115_init(ads1115_t *dev, uint8_t addr7) {
    dev->i2c_addr7 = addr7;
    dev->pga_field = ADS1115_PGA_6V144; // default range
    // Optional: program comparator thresholds if used; here we disable comparator
    return true;
}

// Trigger a single conversion on a selected channel and read raw result
bool ads1115_read_single_raw(ads1115_t *dev, uint16_t mux_field,
                             uint16_t dr_field, int16_t *raw) {
    // Build config word
    uint16_t cfg = 0;
    cfg |= ADS1115_OS_SINGLE;
    cfg |= mux_field;            // input selection
    cfg |= dev->pga_field;       // full scale range
    cfg |= ADS1115_MODE_SINGLE;  // single-shot mode
    cfg |= dr_field;             // data rate
    cfg |= ADS1115_COMP_QUE(ADS1115_COMP_DISABLE);

    if (!ads1115_write_config(dev->i2c_addr7, cfg)) return false;

    // Poll OS bit until ready (or use a conservative delay)
    for (int i = 0; i < 50; ++i) {
        uint16_t r;
        if (!ads1115_read_config(dev->i2c_addr7, &r)) return false;
        if (r & ADS1115_OS_SINGLE) break; // ready
        // ~1 ms max at 860 SPS; adjust if DR is lower
        i2c_delay_us(500);
    }

    uint16_t data;
    if (!ads1115_read16(dev->i2c_addr7, ADS1115_PTR_CONVERT, &data)) return false;
    *raw = (int16_t)data; // two's complement
    return true;
}

// Convert raw code to volts based on the configured PGA
double ads1115_raw_to_volts(const ads1115_t *dev, int16_t code) {
    const float lsb = ads1115_lsb_volts(dev->pga_field);
    return (double)code * (double)lsb;
}

Usage example for single‑ended AIN3 vs GND at 860 SPS with ±6.144 V range:

// example.c
#include "soft_i2c.h"
#include "ads1115.h"

int main(void) {
    i2c_init();

    ads1115_t adc;
    ads1115_init(&adc, ADS1115_ADDR_7BIT);       // 0x48 when ADDR=GND
    adc.pga_field = ADS1115_PGA_6V144;           // ±6.144 V, ~0.1875 mV/LSB

    while (1) {
        int16_t raw = 0;
        if (ads1115_read_single_raw(&adc, ADS1115_MUX_AIN3_GND, ADS1115_DR_860SPS, &raw)) {
            double v = ads1115_raw_to_volts(&adc, raw);
            // Use v (volts)
        }
        // Small delay
        i2c_delay_us(10000);
    }
}

Pointer operations (explicit)

If you perfer to split pointer and data phases manually:

  • To select the conversion register for a subsequent read, write the pointer 0x00, then re‑start and read two bytes.
  • To write the configuration, write pointer 0x01 followed by MSB then LSB of the config word.
// Set pointer to conversion register only
static bool ads1115_select_conversion(uint8_t addr7) {
    i2c_start();
    if (!i2c_write_byte((addr7 << 1) | 0)) { i2c_stop(); return false; }
    if (!i2c_write_byte(ADS1115_PTR_CONVERT)) { i2c_stop(); return false; }
    i2c_stop();
    return true;
}

Trimmed‑mean filter (drop min and max)

Collect multiple samples, discard the largest and smallest, then average the rest.

// filter.c
#include <float.h>
#include <stddef.h>

static double trimmed_mean_read(ads1115_t *dev, uint16_t mux_field,
                                uint16_t dr_field, int samples) {
    if (samples < 3) samples = 3;
    double sum = 0.0;
    double vmin = DBL_MAX, vmax = -DBL_MAX;

    for (int i = 0; i < samples; ++i) {
        int16_t raw;
        if (!ads1115_read_single_raw(dev, mux_field, dr_field, &raw)) {
            return 0.0; // handle I²C error appropriately
        }
        double v = ads1115_raw_to_volts(dev, raw);
        sum += v;
        if (v < vmin) vmin = v;
        if (v > vmax) vmax = v;
    }
    sum -= vmin;
    sum -= vmax;
    return sum / (double)(samples - 2);
}

Example wrapper to read AIN3 with a 10‑sample trimmed mean:

double ads1115_read_voltage_filtered(ads1115_t *dev) {
    return trimmed_mean_read(dev, ADS1115_MUX_AIN3_GND, ADS1115_DR_860SPS, 10);
}

Notes on comparator thresholds (optional)

The Lo_thresh and Hi_thresh registers (0x02, 0x03) are only needed if using the comparator/ALERT. For simple polling reads, you can leave the comparator disabled via COMP_QUE=3 in the config word.


Changing input channels

Select the desired input using MUX bits when building the config:

  • AIN0 vs GND: ADS1115_MUX_AIN0_GND
  • AIN1 vs GND: ADS1115_MUX_AIN1_GND
  • AIN2 vs GND: ADS1115_MUX_AIN2_GND
  • AIN3 vs GND: ADS1115_MUX_AIN3_GND

For differential measurements, use the corresponding AINp/AINn fields (e.g., ADS1115_MUX_AIN0_AIN1).


Voltage conversion reminder

The ADS1115 returns a signed 16‑bit code (two’s complement). To convert to volts, multiply by the LSB size determined by the selected PGA. For ±6.144 V full‑scale, LSB ≈ 0.1875 mV. For other ranges, use the provided ads1115_lsb_volts helper.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.