To measure water quality with a TDS (Total Dissolved Solids) sensor and an Arduino, use the DFRobot Gravity Analog TDS Sensor (SEN0244) wired to a 5V analog pin, apply a 30-sample median filter in your code to eliminate ADC ripple, and calculate PPM using the factory cubic polynomial. This setup yields ±10% accuracy for hydroponics, aquariums, and water filtration monitoring without the $150+ price tag of lab-grade I2C probes.

The Quick Decision: Which TDS Sensor Module to Buy?

Not all TDS modules are created equal. The cheap 3-pin generic boards often suffer from poor waterproofing and uncalibrated op-amps, while digital I2C sensors are overkill for most hobbyists. Use this decision path to select your hardware:

Condition / Requirement Recommended Module Approx. Cost (2026)
Budget is under $15; acceptable error is ±15%; quick proof-of-concept. Generic 3-Pin Analog TDS V1.0 (Keyestudio/unbranded) $10 - $14
Budget $20-$30; need reliable waterproof epoxy seal, stable analog out, and plug-and-play wiring. DFRobot Gravity: Analog TDS Sensor (SEN0244) $22 - $26
Need lab-grade accuracy (±1%), digital I2C/UART output, and continuous submersion in harsh environments. Atlas Scientific EZO-TDS Circuit $150 - $180
Default Recommendation: For 90% of DIY hydroponics, aquaponics, and RO (Reverse Osmosis) water monitoring builds, buy the DFRobot SEN0244. It operates on 3.3V to 5.5V, includes a high-quality epoxy-sealed probe, and has extensive community documentation for polynomial calibration.

Hardware Spec Sheet & Pin Mapping

This guide targets the Arduino Nano V3 (ATmega328P, 5V/16MHz variant). The Nano is ideal for compact, enclosed water-monitoring builds. If you are using an Arduino Uno R3, the pin mapping and code remain identical. Note: If you are adapting this for a 3.3V board like the ESP32 or Arduino Nano 33 IoT, you must power the sensor with 3.3V and adjust the VREF constant in the code, or use a voltage divider on the analog out pin.

Module Specifications (SEN0244)

  • Operating Voltage: 3.3V ~ 5.5V DC
  • Output Signal: 0 ~ 3.3V (when powered at 3.3V) or 0 ~ 5V (when powered at 5V)
  • Measurement Range: 0 ~ 1000 ppm (parts per million)
  • Accuracy: ±10% F.S. (Full Scale) at 25°C
  • Response Time: < 1 second

Pin Mapping Table

TDS Sensor Pin Arduino Nano V3 Pin Wire Color (Standard) Notes
VCC (+) 5V Red Do not use the 3V3 pin on a 5V Nano.
GND (-) GND Black Ensure a solid common ground to prevent ADC noise.
AOUT (Signal) A0 Blue Analog input. Do not use a digital pin.

Step-by-Step Wiring Procedure

Safety & Hardware Warning: Never submerge the sensor probe past the epoxy line. The top of the probe where the wire enters is not waterproof. Submerging the joint will cause water ingress, shorting the internal resistors and permanently destroying the module.
  1. Prep the Wires: Cut three 6-inch lengths of 22 AWG stranded silicone wire. Strip 1/4 inch of insulation from each end and tin them with a soldering iron (set to 350°C / 660°F) using rosin-core flux.
  2. Connect the Sensor: Solder or use the included Gravity 4-pin JST connector to attach Red to VCC, Black to GND, and Blue to AOUT. (If your module has a 4-pin connector, the 4th pin is usually a digital output that triggers at a fixed threshold; leave it unconnected for analog PPM reading).
  3. Wire to the Nano: Plug the Red wire into the 5V pin on the Arduino Nano V3, Black into GND, and Blue into A0.
  4. Probe Placement: Secure the TDS probe in your water reservoir using a 3D-printed mount or a suction cup clip. Ensure the probe tip is at least 1 inch away from water pump intakes or aeration stones, as trapped air bubbles on the probe electrodes will cause massive reading spikes.
  5. Verify Connections: Use a multimeter set to DC Voltage. Probe the VCC and GND pins at the sensor header. You should read between 4.8V and 5.1V. If it reads lower, your USB cable or power supply is experiencing excessive voltage drop.

Complete Arduino Code with Median Filtering

Raw analog reads from water sensors are notoriously noisy due to EMI from water pumps, USB power ripple, and the high impedance of the probe circuit. This code implements a 30-sample median filter to discard outlier spikes before calculating the PPM using the manufacturer's cubic polynomial. It also includes error handling for ADC saturation.

Target Board: Arduino Nano V3 (ATmega328P, 5V/16MHz). Ensure "Arduino Nano" and "ATmega328P (Old Bootloader)" are selected in the Arduino IDE Tools menu if using a clone board.

/*
 * TDS Sensor Arduino Code with Median Filtering
 * Target: Arduino Nano V3 (5V/16MHz) or Uno R3
 * Sensor: DFRobot Gravity Analog TDS (SEN0244) or equivalent
 * Reference: https://wiki.dfrobot.com/Gravity_Analog_TDS_Sensor_Meter_For_Arduino_SKU_SEN0244
 */

#define TDS_PIN A0
#define VREF 5.0          // System voltage (5.0V for Nano/Uno)
#define ADC_RES 1024.0    // 10-bit ADC resolution
#define NUM_SAMPLES 30    // Number of samples for median filter
#define TEMP_C 25.0       // Fixed temperature for compensation (update if using a temp sensor)

int samples[NUM_SAMPLES];

// Function to sort the array for median extraction (Insertion Sort)
void sortArray(int arr[], int n) {
  for (int i = 1; i < n; ++i) {
    int key = arr[i];
    int j = i - 1;
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j = j - 1;
    }
    arr[j + 1] = key;
  }
}

int getMedianValue() {
  for (int i = 0; i < NUM_SAMPLES; i++) {
    samples[i] = analogRead(TDS_PIN);
    delay(2); // Small delay to allow ADC capacitor to settle
  }
  sortArray(samples, NUM_SAMPLES);
  return samples[NUM_SAMPLES / 2]; // Return the middle value
}

void setup() {
  Serial.begin(115200);
  pinMode(TDS_PIN, INPUT);
  analogReference(DEFAULT); // Ensure 5V reference on Nano/Uno
  Serial.println("TDS Sensor Initialized. Warming up for 3 seconds...");
  delay(3000); // Allow op-amp to stabilize
}

void loop() {
  int analogValue = getMedianValue();
  
  // Error Handling: Check for ADC saturation or open circuit
  if (analogValue >= 1020) {
    Serial.println("Error: ADC saturated (1023). Check for short circuit or 3.3V/5V logic mismatch.");
    delay(2000);
    return;
  }
  
  if (analogValue <= 2) {
    Serial.println("Error: ADC reads 0. Probe may be disconnected or not submerged.");
    delay(2000);
    return;
  }

  // Calculate Voltage
  float averageVoltage = analogValue * VREF / ADC_RES;
  
  // Temperature Compensation Formula
  // TDS increases by ~2% per degree Celsius above 25C
  float compensationCoefficient = 1.0 + 0.02 * (TEMP_C - 25.0);
  float compensationVoltage = averageVoltage / compensationCoefficient;
  
  // Cubic Polynomial for PPM calculation (Factory calibrated for SEN0244)
  float tdsValue = (133.42 * pow(compensationVoltage, 3) 
                  - 255.86 * pow(compensationVoltage, 2) 
                  + 857.39 * compensationVoltage) * 0.5;
  
  // Sanity check for negative or impossible values
  if (tdsValue < 0) tdsValue = 0;
  if (tdsValue > 1200) tdsValue = 1200; // Cap at sensor max range

  Serial.print("Analog: ");
  Serial.print(analogValue);
  Serial.print(" | Voltage: ");
  Serial.print(averageVoltage, 2);
  Serial.print("V | TDS: ");
  Serial.print(tdsValue, 1);
  Serial.println(" ppm");

  delay(1000); // Read once per second
}

Troubleshooting: First Three Things to Check When It Fails

When your serial monitor outputs garbage data, do not immediately recalibrate. Hardware and power issues account for 95% of TDS sensor failures. Follow this ranked checklist:

1. Symptom: Serial monitor prints Error: ADC saturated (1023) or maxed out PPM

  • Cause A (Most Likely): You are powering a 5V sensor but reading it with a 3.3V microcontroller (like an ESP32 or Raspberry Pi Pico). The 5V analog output exceeds the 3.3V ADC maximum, pegging it at 1023/4095.
  • Fix: Power the sensor with 3.3V instead of 5V (the SEN0244 supports 3.3V input), and change #define VREF 5.0 to #define VREF 3.3 in the code.
  • Cause B: The analog signal wire is shorted to the 5V rail on the breadboard.
  • Fix: Disconnect the sensor and measure continuity between the AOUT wire and VCC with a multimeter.

2. Symptom: Serial monitor prints TDS: 0.00 ppm continuously

  • Cause A (Most Likely): The probe is not submerged in a conductive liquid. Pure distilled water has near-zero TDS and will read close to 0. Air reads 0.
  • Fix: Test the probe in a glass of tap water (typically 100-300 ppm) or a mild saltwater solution to verify it responds.
  • Cause B: Broken ground connection. Without a common ground, the Arduino ADC cannot read the voltage differential.
  • Fix: Verify continuity from the sensor GND pin to the Arduino GND pin.

3. Symptom: Wild fluctuations (e.g., jumping from 120 ppm to 450 ppm between reads)

  • Cause A (Most Likely): Electromagnetic interference (EMI) from a nearby AC water pump, or USB power ripple from a cheap switching power supply.
  • Fix: Ensure the median filter code (provided above) is active. If fluctuations persist, power the Arduino via a linear voltage regulator or a high-quality USB-C PD power bank rather than a wall-wart. Keep sensor wires away from AC pump lines.
  • Cause B: Air bubbles clinging to the probe electrodes.
  • Fix: Gently tap the probe against the side of the container to dislodge bubbles.

Extending and Simplifying the Build

Depending on your final deployment environment, you may need to strip this build down to its bare essentials or scale it up for IoT logging.

How to Simplify (For Quick Bench Testing)

If you are just testing a water sample on your desk and don't care about high-frequency noise, remove the getMedianValue() function and the sorting array. Replace it with a single analogRead(TDS_PIN). This frees up SRAM and reduces code complexity, though you will see ±5% jitter in the serial output. Additionally, remove the temperature compensation math if you are strictly measuring room-temperature water (20°C - 25°C), as the error introduced by ignoring temp compensation in this narrow band is less than 2%.

How to Extend (For Production IoT Systems)

To make this build robust for a permanent hydroponics installation:

  1. Add Real-Time Temperature Compensation: TDS readings drift by roughly 2% per degree Celsius. Wire a DS18B20 waterproof temperature probe to digital pin D2. Use the OneWire and DallasTemperature libraries to read the actual water temperature, and feed that live value into the TEMP_C variable in the compensation formula.
  2. Upgrade to ESP32 for MQTT: Swap the Nano V3 for an ESP32-WROOM-32 DevKit v1. Use the PubSubClient library to publish the TDS PPM and temperature to an MQTT broker (like Mosquitto) every 60 seconds. Remember to power the TDS sensor from the ESP32's 3V3 pin and update the VREF constant to 3.3.
  3. Implement Auto-Cleaning Logic: If you are using a motorized ball valve to flush the system when TDS gets too high, add a relay module. Trigger the relay in the loop() when tdsValue > 800, but add a software debounce delay to prevent the pump from short-cycling if the sensor reads a temporary air bubble spike.

For deeper regulatory context on what TDS levels mean for water safety and agriculture, refer to the EPA's National Aquatic Resource Surveys on Total Dissolved Solids. For official Arduino ADC behavior and reference voltage configurations, consult the Arduino analogRead() documentation.