A raw analog reading from a Total Dissolved Solids (TDS) probe is practically useless on its own. TDS sensors do not actually count particles; they measure electrical conductivity (EC) and use a polynomial curve to estimate the concentration of dissolved ions. Because water conductivity changes by roughly 2% per degree Celsius, a tds sensor for arduino projects must include temperature compensation to provide meaningful data. If you skip the temp sensor, your readings will drift wildly as your water warms up or cools down.
This guide walks through wiring the DFRobot Gravity Analog TDS Sensor (SEN0244) to an Arduino Uno R3, integrating a DS18B20 waterproof temperature probe for real-time compensation, and debugging the most common analog read failures.
Understanding TDS Readings and Water Quality Benchmarks
Before wiring the sensor, you need to know what the numbers actually mean. TDS is measured in parts per million (ppm) or milligrams per liter (mg/L). It is critical to understand that TDS sensors cannot detect biological contaminants, PFAS, lead, or non-ionic organic chemicals. A reading of 0 ppm does not mean water is safe to drink; it only means it lacks conductive mineral salts.
The table below outlines expected TDS and EC ranges for common water sources, based on EPA secondary drinking water guidelines and standard hydroponic baselines.
| Water Source | Expected TDS (ppm) | Expected EC (µS/cm) | Primary Conductive Ions |
|---|---|---|---|
| Distilled / RO Water | 0 - 50 | 0 - 100 | Trace minerals, dissolved CO2 |
| Optimal Hydroponic Feed | 500 - 1200 | 1000 - 2400 | Nitrate, Potassium, Calcium, Magnesium |
| Municipal Tap Water (US Avg) | 150 - 400 | 300 - 800 | Chloride, Sulfate, Sodium, Calcium |
| EPA Secondary Max Limit | 500 | ~1000 | Aesthetic limit (taste/scale), not health |
| Brackish / Estuary Water | 1000 - 5000 | 2000 - 10000 | Sodium, Chloride (Saline intrusion) |
Hardware Selection and Pin Mapping
For this build, we are targeting the Arduino Uno R3 (DIP-28 ATmega328P). The Uno's stable 5V logic and dedicated voltage regulator make it superior to 3.3V boards (like the ESP32 or Arduino Nano 33 IoT) for raw analog sensor work, unless you add an external ADC. The DFRobot SEN0244 is calibrated for a 5V analog reference.
Parts List
- Microcontroller: Arduino Uno R3 (or genuine clone with ATmega328P)
- TDS Sensor: DFRobot Gravity: Analog TDS Sensor / Meter (SKU: SEN0244)
- Temp Sensor: DS18B20 Waterproof Digital Temperature Sensor (with 1m stainless probe)
- Resistor: 4.7kΩ (for DS18B20 I2C/OneWire pull-up)
- Capacitor: 100nF (0.1µF) ceramic (for analog pin decoupling)
Pin Mapping Table
| Component | Component Pin | Arduino Uno R3 Pin | Notes |
|---|---|---|---|
| TDS Sensor Board | VCC | 5V | Do not use 3.3V out |
| TDS Sensor Board | GND | GND | Shared ground plane |
| TDS Sensor Board | A (Analog) | A1 | Add 100nF cap to GND here |
| DS18B20 Probe | Red (VCC) | 5V | Parasitic power not recommended |
| DS18B20 Probe | Black (GND) | GND | Shared ground plane |
| DS18B20 Probe | Yellow (Data) | D2 | 4.7kΩ pull-up to 5V required |
Wiring Steps and Analog Reference Gotchas
- Decouple the Analog Pin: Solder or plug a 100nF ceramic capacitor between the Arduino A1 pin and GND. The ATmega328P ADC is notoriously noisy; this capacitor acts as a low-pass filter to stabilize the TDS voltage reading.
- Wire the DS18B20 Pull-up: Connect the yellow data wire to D2. Connect one leg of the 4.7kΩ resistor to D2, and the other leg to 5V. Without this pull-up, the OneWire bus will float and return
-127.00errors. - Connect the TDS Probe: Plug the waterproof probe into the 3.5mm jack on the DFRobot analog board. Ensure the connection is tight; a loose BNC or 3.5mm jack introduces massive contact resistance, skewing the EC calculation.
- Submerge and Stabilize: Place both the TDS probe and the DS18B20 probe into your test solution. Wait at least 60 seconds. The stainless steel TDS probe acts as a heat sink and takes time to reach thermal equilibrium with the water.
Complete Compilable Code with Temperature Compensation
This code targets the Arduino Uno R3. It uses the OneWire and DallasTemperature libraries for the DS18B20, and implements the exact DFRobot polynomial for the SEN0244 sensor. It includes error handling to prevent nan propagation if the temperature sensor fails.
#include <OneWire.h>
#include <DallasTemperature.h>
// --- PIN DEFINITIONS ---
#define ONE_WIRE_BUS 2 // DS18B20 Data pin
#define TDS_SENSOR_PIN A1 // TDS Analog pin
// --- CONSTANTS ---
#define V_REF 5.0 // Arduino Uno analog reference voltage
#define ADC_RESOLUTION 1024.0
#define SCOUNT 30 // Number of samples for analog averaging
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
float analogBuffer[SCOUNT];
int analogBufferIndex = 0;
void setup() {
Serial.begin(115200);
sensors.begin();
pinMode(TDS_SENSOR_PIN, INPUT);
Serial.println("TDS Sensor Initialized. Warming up...");
delay(2000);
}
void loop() {
// 1. Read Temperature
sensors.requestTemperatures();
float temperature = sensors.getTempCByIndex(0);
// Error Handling: DS18B20 returns -127.0 on failure
if (temperature == -127.00) {
Serial.println("ERROR: DS18B20 disconnected or missing pull-up. Defaulting to 25C.");
temperature = 25.0;
}
// 2. Read and Average TDS Analog Voltage
analogBuffer[analogBufferIndex] = analogRead(TDS_SENSOR_PIN);
analogBufferIndex++;
if (analogBufferIndex == SCOUNT) {
analogBufferIndex = 0;
}
float averageAnalog = getMedianNum(analogBuffer, SCOUNT);
float averageVoltage = averageAnalog * (V_REF / ADC_RESOLUTION);
// 3. Temperature Compensation
// Conductivity increases ~2% per degree C above 25C
float compensationCoefficient = 1.0 + 0.02 * (temperature - 25.0);
float compensationVoltage = averageVoltage / compensationCoefficient;
// 4. Calculate TDS (ppm) using DFRobot SEN0244 Polynomial
float tdsValue = (133.42 * pow(compensationVoltage, 3)
- 255.86 * pow(compensationVoltage, 2)
+ 857.39 * compensationVoltage) * 0.5;
// Prevent negative TDS readings from noise at very low voltages
if (tdsValue < 0) tdsValue = 0;
// 5. Output Data
Serial.print("Temp: ");
Serial.print(temperature, 1);
Serial.print(" C | Voltage: ");
Serial.print(compensationVoltage, 3);
Serial.print(" V | TDS: ");
Serial.print(tdsValue, 0);
Serial.println(" ppm");
delay(800); // Non-blocking in production, use millis() for multitasking
}
// Helper: Median filter to drop analog noise spikes
float getMedianNum(float bArray[], int iFilterLen) {
float bTab[iFilterLen];
for (byte i = 0; i < iFilterLen; i++) bTab[i] = bArray[i];
for (byte i = 0; i < iFilterLen - 1; i++) {
for (byte j = i + 1; j < iFilterLen; j++) {
if (bTab[i] > bTab[j]) {
float temp = bTab[i];
bTab[i] = bTab[j];
bTab[j] = temp;
}
}
}
if ((iFilterLen & 1) > 0) return bTab[(iFilterLen - 1) / 2];
else return (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2.0;
}
Debugging: First Three Things to Check When It Fails
Analog sensors are unforgiving. If your serial monitor outputs garbage, follow this exact decision path.
1. Error String: TDS: nan ppm or ERROR: DS18B20 disconnected
- Cause: The DS18B20 is returning
-127.00, causing the compensation coefficient to calculate incorrectly, or the math library is dividing by zero due to a floating bus. - Fix: Check the 4.7kΩ pull-up resistor on D2. A common mistake is wiring the resistor between D2 and GND instead of D2 and 5V. Measure D2 with a multimeter; it should read ~5V when idle.
2. Error String: TDS: 0.00 ppm (when submerged in tap water)
- Cause: The analog pin is reading 0V. This happens if the TDS probe is not fully seated in the 3.5mm jack, or if you are powering the sensor board with 3.3V while the code expects 5V.
- Fix: Measure the voltage at the TDS board's 'A' pin with a multimeter while submerged. It should read between 0.5V and 2.5V for standard tap water. If it reads 0V, replace the probe cable.
3. Symptom: Wild Fluctuations (e.g., jumping from 150 to 800 ppm in seconds)
- Cause: ATmega328P ADC noise, usually caused by USB power ripple from a cheap laptop charger, or missing the decoupling capacitor.
- Fix: Install the 100nF capacitor between A1 and GND. If fluctuations persist, power the Arduino via the barrel jack with a regulated 9V wall supply instead of USB, bypassing the PC's noisy 5V rail. Reference the DFRobot SEN0244 Wiki for hardware-specific noise mitigation.
Extending and Simplifying the Build
How to Simplify (The 'Quick and Dirty' Method)
If you are building a temporary hydroponic monitor and know your water temperature stays exactly at room temperature (approx 22-25°C), you can drop the DS18B20 entirely. Delete the OneWire libraries, hardcode float temperature = 25.0;, and wire the TDS sensor directly to A1. This reduces wiring complexity and eliminates the pull-up resistor requirement, though you sacrifice accuracy if the water temp shifts.
How to Extend (IoT and Automation)
To turn this into a remote monitoring node, swap the Arduino Uno R3 for an ESP32 DevKit V1. However, because the ESP32 is a 3.3V device, you must use an ADS1115 16-bit external ADC over I2C to read the TDS sensor accurately. The ESP32's internal 12-bit ADC is non-linear and poorly suited for precision analog reads. Once on the ESP32, use the PubSubClient library to publish the compensated TDS and temperature values to an MQTT broker (like Mosquitto) for ingestion into Home Assistant or Grafana.






