Can't get Keyestudio TDS sensor calibrated with Photon

Just got a KS0429 keyestudio TDS Meter V1.0 that I’ve been trying to setup and calibrate for days following the sample code in the sensor’s wiki without much luck.

The problem I have is that the TDS readings are off. I am using a 1000ppm TDS calibration solution (NaCl at 25C/77F, 90ml).

I wired the black cable from the sensor to the photon’s ground pin, the red cable to the photon’s VIN pin and the yellow cable to the photon’s A1 pin.

When introducing the sensor in the calibration solution I get the following readings:

analogRead(TdsSensorPin) = 1343
temperature (measured manually with an independent thermometer) = 22.2
tds = 668.3 <- [ this should be 1000 I believe ]

The constants I set in the sample firmware are:

#define TdsSensorPin A1
#define VREF 4.8 // analog reference voltage(Volt) of the ADC
#define SCOUNT 30 // sum of sample point
#define ADCRANGE 4096.0
#define KVALUE 0.5

At this point I’ve run out of ideas and I’m hoping someone might be able to shed a light on what part of my code needs to be corrected and why.

Here is the firmware I am using:

#define TdsSensorPin A1
#define VREF 4.8 // analog reference voltage(Volt) of the ADC
#define SCOUNT 30 // sum of sample point
#define ADCRANGE 4096.0 // 1024.0
#define KVALUE 0.5 // 2.12

int analogBuffer[SCOUNT]; // store the analog value in the array, read from ADC
int analogBufferTemp[SCOUNT];
int analogBufferIndex = 0, copyIndex = 0;
float averageVoltage = 0, tdsValue = 0;

float temperature = 22.2;
float median_raw = 0;

char vitalsvalue[255];
unsigned long VITALS_INTERVAL = 60000;
unsigned long vitalsStart = 0; // the time the vitals check started
bool vitalsSuccess;

void setup() {
  Serial.begin(9600);
  Particle.variable("vitalsvalue", vitalsvalue, STRING);
  pinMode(TdsSensorPin, INPUT);
}

void loop() {
  //****************************************************
    
  static unsigned long analogSampleTimepoint = millis();
   if(millis()-analogSampleTimepoint > 40U)     //every 40 milliseconds,read the analog value from the ADC
   {
     analogSampleTimepoint = millis();
     analogBuffer[analogBufferIndex] = analogRead(TdsSensorPin);    //read the analog value and store into the buffer
     analogBufferIndex++;
     if(analogBufferIndex == SCOUNT) 
         analogBufferIndex = 0;
   }   
   static unsigned long printTimepoint = millis();
   if(millis()-printTimepoint > 800U)
   {
      printTimepoint = millis();
      for(copyIndex=0;copyIndex<SCOUNT;copyIndex++)
        analogBufferTemp[copyIndex]= analogBuffer[copyIndex];
      
      median_raw = getMedianNum(analogBufferTemp,SCOUNT);
      averageVoltage = median_raw * (float)VREF / ADCRANGE; // read the analog value more stable by the median filtering algorithm, and convert to voltage value
      float compensationCoefficient=1.0+0.02*(temperature-25.0);    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0));
      float compensationVoltage=averageVoltage/compensationCoefficient;  //temperature compensation
      tdsValue=(133.42*compensationVoltage*compensationVoltage*compensationVoltage - 255.86*compensationVoltage*compensationVoltage + 857.39*compensationVoltage)*KVALUE; //convert voltage value to tds value
      
      Serial.print("raw:");
      Serial.print(median_raw);
      Serial.print("   voltage:");
      Serial.print(averageVoltage);
      Serial.print("V   ");
      Serial.print("   compensationCoefficient:");
      Serial.print(compensationCoefficient);
      Serial.print("   compensationVoltage:");
      Serial.print(compensationVoltage);
      Serial.print("   TDS Value:");
      Serial.print(tdsValue);
      Serial.println("ppm");
      
      publishVitals();
   }
  
  //****************************************************/
  
}

int publishVitals() {
  char jsonData[255];
  // assuming lightvalue to be char* or similar
  snprintf(jsonData, sizeof(jsonData), "{ \"status\": \"active\", \"raw\": \"%f\", \"voltage\": \"%f\", \"tds\": \"%f\", \"temperature\": \"%f\" }", median_raw, averageVoltage, tdsValue, temperature);
  strcpy(vitalsvalue, jsonData);
  
  if ( (millis() - vitalsStart) >= VITALS_INTERVAL ) {      // Completed
    
    // This prints the value to the USB debugging serial port (for optional debugging purposes)
    Serial.printlnf("data: %s", vitalsvalue);
      
    vitalsStart += VITALS_INTERVAL; // this prevents drift in the intervals
    
    vitalsSuccess = Particle.publish("vitals", vitalsvalue, PRIVATE);
      
    Serial.printlnf("vitals published to the cloud: %s", (vitalsSuccess ? "success" : "failed"));
    
    return 1;
  } else {
    return 0;   
  }
}


int getMedianNum(int bArray[], int iFilterLen) {
  int bTab[iFilterLen];
  for (byte i = 0; i<iFilterLen; i++)
    bTab[i] = bArray[i];
  int i, j, bTemp;
  for (j = 0; j < iFilterLen - 1; j++) 
  {
    for (i = 0; i < iFilterLen - j - 1; i++) 
    {
      if (bTab[i] > bTab[i + 1]) 
      {
        bTemp = bTab[i];
        bTab[i] = bTab[i + 1];
        bTab[i + 1] = bTemp;
      }
    }
  }
  if ((iFilterLen & 1) > 0)
    bTemp = bTab[(iFilterLen - 1) / 2];
  else
    bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
  return bTemp;
}

Any help is greatly appreciated!

Welcome to the Particle Community.

What range of voltage are you presenting to the analog pin A1?

You have a define VREF as 4.8 where the Photon only measures between 0-3V3 and 3V3 should not be exceeded! If you are using a voltage divider resistor bridge then maybe you need to rethink your constants.

When using a pin for analogRead you do not need to set pinMode(); Note: you do not need to set the pinMode() with analogRead(). The pinMode() is automatically set to AN_INPUT any time analogRead() is called for a particular analog pin.

How do you prime the median filter?

Armor, thank you for your help and happy to be a part of the community!

I must apologize as I am new to electronics and I am having a hard time understanding your last question.

Regarding the voltage coming into pin A1, this is one of the things I don’t know for sure. Initially, I was using pin “3v3” and a constant of VREF 3.3 but was getting an incorrect ppm reading. But reading the documentation for the TDS sensor it saw that it said “Output Voltage: 0 ~ 2.3V”, and changed VREF 2.3 but kept getting reading that were really off. Finally I decided to use pin “VIN” instead of pin “3V3” to power the sensor because it outputs 4.8v and I thought that perhaps this sensor is typically made for arduinos (5v). I repeated the test using VREF 4.8 but keep getting inaccorate results.

Following your suggestions I modified the code back to VREF 3.3 and run a new test using the 1000ppm calibration solution. Here are my results:

analogRead(TdsSensorPin) = 1354
temperature (measured manually with an independent thermometer) = 23.3
tds = 379.87ppm <- [ this should be 1000 I believe ]
#define TdsSensorPin A1
#define VREF 3.3 // analog reference voltage(Volt) of the ADC
#define SCOUNT 30 // sum of sample point
#define ADCRANGE 4096.0 // 1024.0
#define KVALUE 0.5 // 2.12

int analogBuffer[SCOUNT]; // store the analog value in the array, read from ADC
int analogBufferTemp[SCOUNT];
int analogBufferIndex = 0, copyIndex = 0;
float averageVoltage = 0, tdsValue = 0;

float temperature = 22.2;
float median_raw = 0;

char vitalsvalue[255];
unsigned long VITALS_INTERVAL = 60000;
unsigned long vitalsStart = 0; // the time the vitals check started
bool vitalsSuccess;

void setup() {
  Serial.begin(9600);
  Particle.variable("vitalsvalue", vitalsvalue, STRING);
}

void loop() {
  //****************************************************
    
  static unsigned long analogSampleTimepoint = millis();
   if(millis()-analogSampleTimepoint > 40U)     //every 40 milliseconds,read the analog value from the ADC
   {
     analogSampleTimepoint = millis();
     analogBuffer[analogBufferIndex] = analogRead(TdsSensorPin);    //read the analog value and store into the buffer
     analogBufferIndex++;
     if(analogBufferIndex == SCOUNT) 
         analogBufferIndex = 0;
   }   
   static unsigned long printTimepoint = millis();
   if(millis()-printTimepoint > 800U)
   {
      printTimepoint = millis();
      for(copyIndex=0;copyIndex<SCOUNT;copyIndex++)
        analogBufferTemp[copyIndex]= analogBuffer[copyIndex];
      
      median_raw = getMedianNum(analogBufferTemp,SCOUNT);
      averageVoltage = median_raw * (float)VREF / ADCRANGE; // read the analog value more stable by the median filtering algorithm, and convert to voltage value
      float compensationCoefficient = (1.0 - (0.02*(temperature-25.0)));    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0));
      float compensationVoltage=averageVoltage/compensationCoefficient;  //temperature compensation
      tdsValue=(133.42*compensationVoltage*compensationVoltage*compensationVoltage - 255.86*compensationVoltage*compensationVoltage + 857.39*compensationVoltage)*KVALUE; //convert voltage value to tds value
      
      Serial.print("raw:");
      Serial.print(median_raw);
      Serial.print("   voltage:");
      Serial.print(averageVoltage);
      Serial.print("V   ");
      Serial.print("   compensationCoefficient:");
      Serial.print(compensationCoefficient);
      Serial.print("   compensationVoltage:");
      Serial.print(compensationVoltage);
      Serial.print("   TDS Value:");
      Serial.print(tdsValue);
      Serial.println("ppm");
      
      publishVitals();
   }
  
  //****************************************************/
  
}

int publishVitals() {
  char jsonData[255];
  // assuming lightvalue to be char* or similar
  snprintf(jsonData, sizeof(jsonData), "{ \"status\": \"active\", \"raw\": \"%f\", \"voltage\": \"%f\", \"tds\": \"%f\", \"temperature\": \"%f\" }", median_raw, averageVoltage, tdsValue, temperature);
  strcpy(vitalsvalue, jsonData);
  
  if ( (millis() - vitalsStart) >= VITALS_INTERVAL ) {      // Completed
    
    // This prints the value to the USB debugging serial port (for optional debugging purposes)
    Serial.printlnf("data: %s", vitalsvalue);
      
    vitalsStart += VITALS_INTERVAL; // this prevents drift in the intervals
    
    vitalsSuccess = Particle.publish("vitals", vitalsvalue, PRIVATE);
      
    Serial.printlnf("vitals published to the cloud: %s", (vitalsSuccess ? "success" : "failed"));
    
    return 1;
  } else {
    return 0;   
  }
}


int getMedianNum(int bArray[], int iFilterLen) {
  int bTab[iFilterLen];
  for (byte i = 0; i<iFilterLen; i++)
    bTab[i] = bArray[i];
  int i, j, bTemp;
  for (j = 0; j < iFilterLen - 1; j++) 
  {
    for (i = 0; i < iFilterLen - j - 1; i++) 
    {
      if (bTab[i] > bTab[i + 1]) 
      {
        bTemp = bTab[i];
        bTab[i] = bTab[i + 1];
        bTab[i + 1] = bTemp;
      }
    }
  }
  if ((iFilterLen & 1) > 0)
    bTemp = bTab[(iFilterLen - 1) / 2];
  else
    bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
  return bTemp;
}

I removed the pinMode declaration, thank you for that explanation.

I am not sure what you mean by prime the median filter but I have tried using the saw value from analogRead(TdsSensorPin) instead of the median and get pretty much the same type of inaccurate readings?

averageVoltage = analogRead(TdsSensorPin) * (float)VREF / ADCRANGE;

I am happy to check any variables you need to help clarify my setup.

Thank you

I have done a search for the Keyestudio TDS sensor and what this gives me is:
Input Voltage: DC 3.3 ~ 5.5V
Output Voltage: 0 ~ 2.3V
Working Current: 3 ~ 6mA
TDS Measurement Range: 0 ~ 1000ppm
TDS Measurement Accuracy: ± 10% F.S. (25 ℃)
Module Interface: XH2.54-3P
Electrode Interface: XH2.54-2P

I would therefore connect this module’s RED pin to VIN on Photon. BLACK to GND and YELLOW to A0.
What the specification says is that the voltage output and measured by A0 is in the range 0 to 2.3V which is within range.

A good rule with all code is to follow through and check all the calculations to see if they are correct.

So to turn the ADC measurement output which will be in the range 0-4095 for 0-3.3V back to the presented voltage.
in global variables definitions
const float VOLTAGE_CONSTANT = 3.3/4095;

when reading/needing the voltage
float measured_voltage = VOLTAGE_CONSTANT * analogRead(TdsSensorPin);

If you have a multimeter I would check the output voltage from the sensor to see if it is working correctly.

The comment about the median filter is that the way it works is that you are finding the middle value of the array of values used, these filters are usually have 3, 5, 7, 9 items to select a median value from by swapping and your’s has 30! I would suggest removing the median filter to check first that the value is in the right area and then when you are sure of that try introducing a median filter with a smaller window like 5. The filter will contain all 0’s to begin with and will therefore output 0 until half of the window is filled with real values - that’s what I mean by priming, so on first use if the median window is 5 I would push in 5 of the same readings.

2 Likes

Hi Armor,

I followed your suggestions. I appreciate your explanations and “good rules” :+1:

I wired the sensor to the photon via the VIN (red), GND (black) and A0 (yellow):

I also updated the firmware code following your instructions to avoid using a median reading and simply use the raw analog read value, however I continue to get inaccurate results when dipping the sensor probe into the 100ppm calibration solution:
serial

raw:1287   voltage:1.04V      compensationCoefficient:1.00   compensationVoltage:1.04   TDS Value:380.65ppm
raw:1282   voltage:1.03V      compensationCoefficient:1.00   compensationVoltage:1.03   TDS Value:379.13ppm
raw:1287   voltage:1.04V      compensationCoefficient:1.00   compensationVoltage:1.04   TDS Value:380.65ppm
raw:1287   voltage:1.04V      compensationCoefficient:1.00   compensationVoltage:1.04   TDS Value:380.65ppm
raw:1281   voltage:1.03V      compensationCoefficient:1.00   compensationVoltage:1.03   TDS Value:378.82ppm
raw:1285   voltage:1.04V      compensationCoefficient:1.00   compensationVoltage:1.03   TDS Value:380.04ppm
raw:1283   voltage:1.03V      compensationCoefficient:1.00   compensationVoltage:1.03   TDS Value:379.43ppm

firmware

#define TdsSensorPin A0
#define VREF 3.3 // analog reference voltage(Volt) of the ADC
#define SCOUNT 30 // sum of sample point
#define ADCRANGE 4096.0 // 1024.0
#define KVALUE 0.5 // 2.12

const float VOLTAGE_CONSTANT = 3.3/4095;

int analogBuffer[SCOUNT]; // store the analog value in the array, read from ADC
int analogBufferTemp[SCOUNT];
int analogBufferIndex = 0, copyIndex = 0;
float averageVoltage = 0, tdsValue = 0;

float temperature = 24.9;
float median_raw = 0;

char vitalsvalue[255];
unsigned long VITALS_INTERVAL = 60000;
unsigned long vitalsStart = 0; // the time the vitals check started
bool vitalsSuccess;

void setup() {
  Serial.begin(9600);
  Particle.variable("vitalsvalue", vitalsvalue, STRING);
}

void loop() {   
   static unsigned long printTimepoint = millis();
   if(millis()-printTimepoint > 800U)
   {
      printTimepoint = millis();
      
      int raw_reading = analogRead(TdsSensorPin);
      float measured_voltage = VOLTAGE_CONSTANT * raw_reading;
      float compensationCoefficient = (1.0 - (0.02*(temperature-25.0)));    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0));
      float compensationVoltage=measured_voltage/compensationCoefficient;  //temperature compensation
      tdsValue=(133.42*compensationVoltage*compensationVoltage*compensationVoltage - 255.86*compensationVoltage*compensationVoltage + 857.39*compensationVoltage)*KVALUE; //convert voltage value to tds value
      
      Serial.print("raw:");
      Serial.print(raw_reading);
      Serial.print("   voltage:");
      Serial.print(measured_voltage);
      Serial.print("V   ");
      Serial.print("   compensationCoefficient:");
      Serial.print(compensationCoefficient);
      Serial.print("   compensationVoltage:");
      Serial.print(compensationVoltage);
      Serial.print("   TDS Value:");
      Serial.print(tdsValue);
      Serial.println("ppm");
      
      publishVitals();
   }
}

int publishVitals() {
  char jsonData[255];
  // assuming lightvalue to be char* or similar
  snprintf(jsonData, sizeof(jsonData), "{ \"status\": \"active\", \"raw\": \"%f\", \"voltage\": \"%f\", \"tds\": \"%f\", \"temperature\": \"%f\" }", median_raw, averageVoltage, tdsValue, temperature);
  strcpy(vitalsvalue, jsonData);
  
  if ( (millis() - vitalsStart) >= VITALS_INTERVAL ) {      // Completed
    
    // This prints the value to the USB debugging serial port (for optional debugging purposes)
    Serial.printlnf("data: %s", vitalsvalue);
      
    vitalsStart += VITALS_INTERVAL; // this prevents drift in the intervals
    
    vitalsSuccess = Particle.publish("vitals", vitalsvalue, PRIVATE);
      
    Serial.printlnf("vitals published to the cloud: %s", (vitalsSuccess ? "success" : "failed"));
    
    return 1;
  } else {
    return 0;   
  }
}


int getMedianNum(int bArray[], int iFilterLen) {
  int bTab[iFilterLen];
  for (byte i = 0; i<iFilterLen; i++)
    bTab[i] = bArray[i];
  int i, j, bTemp;
  for (j = 0; j < iFilterLen - 1; j++) 
  {
    for (i = 0; i < iFilterLen - j - 1; i++) 
    {
      if (bTab[i] > bTab[i + 1]) 
      {
        bTemp = bTab[i];
        bTab[i] = bTab[i + 1];
        bTab[i + 1] = bTemp;
      }
    }
  }
  if ((iFilterLen & 1) > 0)
    bTemp = bTab[(iFilterLen - 1) / 2];
  else
    bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
  return bTemp;
}

I also tried to check the output voltage from the sensor and I saw a 1.038v readings coming out from the sensor yellow cable. I am also new to using a multimeter but I think I took a valid reading.

One more thing to consider. I took a reading coming out of the VIN pin of the photon just to see how much voltage was being sent to the sensor and it read 4.74v. Not sure if this would mean we should change const float VOLTAGE_CONSTANT = 4.74/4095;?

Please let me know your thoughts on where you feel my issue might lie. I feel we’re so close to figuring out this riddle.

I appreciate your help.

I think you have made progress.

The voltage constant is most definitely 3.3/4095. The sensor module will probably have a DC:DC converter that takes the photon Vin and generates an accurate 3.3V to run the electronics.

Sometimes these sensors aren’t very accurate - it could be that they haven’t been tested or maybe there are incorrect component values on the board. If you buy a more expensive brand you may get a better result!

I see. So we’ve pretty much reached the limits of what the sensor can deliver. It’s good to know.

Thank you for your help Will :+1:.