Overview
TDS — total dissolved solids — is the quickest single number for "what's in this water": minerals, salts, and everything else conductive, expressed in ppm. This Gravity-style analog TDS meter measures it the way handheld TDS pens do, but as a module your microcontroller can log: the probe applies an alternating excitation to the water (alternating so the electrodes don't polarize and corrode), the board conditions the resulting conductivity signal, and out comes a clean 0-2.3V analog voltage your ADC converts to ppm with a standard cubic formula.
It runs from 3.3-5.5V, so it wires identically to ESP32, Pi (via ADS1115), Pico, and Arduino, and the waterproof probe on its XH connector drops into an aquarium, hydroponics reservoir, RO filter output, or kettle of suspect tap water. Typical readings: RO/distilled 0-50 ppm, tap water 100-400 ppm, hydroponic nutrient solutions 500-1500 ppm — the module's 0-1000+ ppm sweet spot covers all of it.
Two honest caveats to design around: TDS-by-conductivity moves ~2% per °C, so temperature compensation matters for comparisons across days (the code includes it — pair with a DS18B20 for automatic correction), and the probe should not live permanently energized in the tank — dip or duty-cycle it for longest electrode life. With those habits it's an accurate, dependable water-quality workhorse.
At a Glance
Specifications
| Parameter | Value |
| Type | Gravity-style analog TDS meter (DFRobot-compatible) |
| Supply Voltage | 3.3 - 5.5V DC |
| Output Voltage | 0 - 2.3V analog (safe for 3.3V ADCs at any supply) |
| Measurement Range | 0 - 1000 ppm calibrated; usable beyond with reduced accuracy |
| Accuracy | ±10% F.S. uncalibrated; better with single-point calibration |
| Working Current | 3 - 6 mA |
| Excitation | AC square-wave drive prevents electrode polarization |
| Probe | 2-electrode waterproof probe, ~80 cm lead, XH2.54 plug (probe tip submerged, plug dry) |
| Interface | 3-pin: A (analog out) · + (VCC) · - (GND) |
| Conversion | Standard cubic formula on compensated voltage (in the code below) |
| Temperature Effect | ~2% / °C — compensate with a water-temperature reading |
Pinout Diagram
The probe plugs into the 2-pin XH socket on the left; the 3-pin connector on the right is your interface — silkscreened A (analog), + (VCC), - (GND). Keep the board and both connectors dry; only the probe tip goes in the water.
Wiring Guide
Arduino Wiring
| Module Pin | Arduino Pin |
|---|---|
| + (VCC) | 5V |
| - (GND) | GND |
| A (analog) | A1 |
ESP32 Wiring
| Module Pin | ESP32 Pin | Details |
|---|---|---|
| + | 3V3 | Runs happily at 3.3V |
| - | GND | |
| A | GPIO 34 | ADC1, input-only; output never exceeds 2.3V |
Raspberry Pi Wiring (via ADS1115)
| Connection | Details |
|---|---|
| Module + / - → 3.3V / GND | Pins 1 / 6 |
| Module A → ADS1115 A0 | Analog channel |
| ADS1115 VDD/GND/SDA/SCL | 3.3V / GND / Pin 3 / Pin 5 |
Raspberry Pi Pico Wiring
| Module Pin | Pico Pin | Details |
|---|---|---|
| + | 3V3(OUT) (pin 36) | |
| - | GND (pin 38) | |
| A | GP26 (pin 31) | ADC0 |
Code Examples
All four examples median-filter the ADC, apply temperature compensation (edit waterTemp, or feed a DS18B20 reading), and run the standard cubic voltage-to-ppm conversion.
Arduino
// Analog TDS Meter - Arduino Example
// A->A1, +->5V, -->GND
const int tdsPin = A1;
const float VREF = 5.0;
float waterTemp = 25.0; // replace with DS18B20 reading if available
float readVolts() {
long total = 0;
for (int i = 0; i < 30; i++) { total += analogRead(tdsPin); delay(3); }
return total / 30.0 * VREF / 1023.0;
}
void setup() {
Serial.begin(115200);
Serial.println("TDS meter ready - dip the probe");
}
void loop() {
float volts = readVolts();
float compensation = 1.0 + 0.02 * (waterTemp - 25.0);
float v = volts / compensation;
float tds = (133.42 * v * v * v
- 255.86 * v * v
+ 857.39 * v) * 0.5;
Serial.print("V: ");
Serial.print(volts, 3);
Serial.print(" | TDS: ");
Serial.print(tds, 0);
Serial.println(" ppm");
delay(1000);
}
ESP32 (MicroPython)
# Analog TDS Meter - ESP32 MicroPython Example
# A->GPIO 34, +->3V3, -->GND
from machine import ADC, Pin
import time
adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)
WATER_TEMP = 25.0
def read_volts(n=30):
total = 0
for _ in range(n):
total += adc.read_uv()
time.sleep_ms(3)
return total / n / 1_000_000
def tds_ppm(volts, temp_c):
v = volts / (1.0 + 0.02 * (temp_c - 25.0))
return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5
print("TDS meter ready - dip the probe")
while True:
volts = read_volts()
print("V: {:.3f} | TDS: {:.0f} ppm".format(volts, tds_ppm(volts, WATER_TEMP)))
time.sleep(1)
Raspberry Pi (Python + ADS1115)
#!/usr/bin/env python3
# Analog TDS Meter - Raspberry Pi + ADS1115 Example
# A->ADS1115 A0, +->3.3V, -->GND
# Install: pip3 install adafruit-circuitpython-ads1x15
import time
import board, busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
ads.gain = 1
chan = AnalogIn(ads, ADS.P0)
WATER_TEMP = 25.0
def tds_ppm(volts, temp_c):
v = volts / (1.0 + 0.02 * (temp_c - 25.0))
return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5
print("TDS meter ready - dip the probe")
try:
while True:
volts = sum(chan.voltage for _ in range(15)) / 15
print(f"V: {volts:.3f} | TDS: {tds_ppm(volts, WATER_TEMP):.0f} ppm")
time.sleep(1)
except KeyboardInterrupt:
print("Stopped by user")
Raspberry Pi Pico (MicroPython)
# Analog TDS Meter - Pico MicroPython Example
# A->GP26 (ADC0), +->3V3(OUT), -->GND
from machine import ADC
import time
adc = ADC(26)
WATER_TEMP = 25.0
def read_volts(n=30):
total = 0
for _ in range(n):
total += adc.read_u16()
time.sleep_ms(3)
return total / n / 65535 * 3.3
def tds_ppm(volts, temp_c):
v = volts / (1.0 + 0.02 * (temp_c - 25.0))
return (133.42 * v**3 - 255.86 * v**2 + 857.39 * v) * 0.5
def quality(ppm):
if ppm < 50: return "RO / very pure"
if ppm < 300: return "good drinking water"
if ppm < 600: return "hard / mineral-rich"
return "very high TDS"
print("TDS meter ready - dip the probe")
while True:
volts = read_volts()
ppm = tds_ppm(volts, WATER_TEMP)
print("TDS: {:.0f} ppm ({})".format(ppm, quality(ppm)))
time.sleep(1)