top of page

Connecting HITA E0 Electrical Conductivity and Temperature Sensor to Arduino (C++ Codes)

Updated: Apr 24

The APAS T1 soil moisture sensor and HITA E0 EC sensor can be connected to the same hardware. Please read this post to learn how to take care of wiring your sensor to the Arduino.


In this post, I will explain the details of the code / Arduino sketch you can compile and program into your Arduino. The sketch will allow you to connect and read up to four sensors simultaneously. The code is capable of automatically detecting sensors connected to Arduino digital pins, so it works even if you have only one sensor. Along the way, I will dig into important subroutines and equations that are used. I will attach the complete sketch file at the end for you to download and use.




Libraries

You need to add a few libraries in order for your code to work. These libraries are listed below:

// Libraries
#include <DallasTemperature.h>
#include <ArduinoUniqueID.h>
#include <ADS1115_WE.h>
#include <Wire.h>
#include <Adafruit_SleepyDog.h>


Arduino Serial Number

Each Arduino board comes with a serial number (device ID), which can be used to identify specific board. This is useful specially if you're using a large number of boards in a network and/or in IoT applications. The subroutine below allows you to obtain the serial number:


void getSerialNumber()
{
  String strSN = ""; 
  for (size_t i = 0; i < UniqueIDsize; i++)
  {
    if (UniqueID[i] < 100)
      strSN += '0';
      if (UniqueID[i] < 10)
        strSN += '0';
        strSN += UniqueID[i];
        if (i < 15) strSN += " ";
  } 

  Serial.print(F("Device ID: "));
  Serial.println(strSN); 
  Serial.flush(); // Wait untill all data are sent to computer
}


Temperature

The HITA E0 sensor relies on the DS18B20 chip for temperature measurements. To simplify the code, each temp sensor is assigned to a different Arduino pin. If there are limited number of GPIO pins available, you can connect several temp sensor to one Arduino pin.


// Temperature Sensors
const int ONE_WIRE_BUS[] = {2, 3, 4, 5}; // Assign Arduino pin to temp sensor
OneWire oneWire[4] = { // Setup a oneWire instance to communicate with any OneWire devices
  // initializers for each instance in the array go here
  OneWire(ONE_WIRE_BUS[0]),
  OneWire(ONE_WIRE_BUS[1]),
  OneWire(ONE_WIRE_BUS[2]),
  OneWire(ONE_WIRE_BUS[3])
};
DallasTemperature tempSensors[4] = { // Pass our oneWire reference to Dallas Temperature sensor 
   // initializers for each instance in the array go here
   DallasTemperature(&oneWire[0]),
   DallasTemperature(&oneWire[1]),
   DallasTemperature(&oneWire[2]),
   DallasTemperature(&oneWire[3])
};
DeviceAddress deviceAddress; // Arrays to hold device address

The DS18B20 resolution can be set anywhere from 9 to 12. The resolution directly affects the conversion time. In my experience, a resolution of 11 (0.125 °C) is a good place to start. At this resolution, it takes about 375 ms for the sensor to complete the conversion and update its registers with a new temp value.


int measSensorTemp(byte bytSensor)
{
// Number of milliseconds to wait till conversion is complete based on resolution: 94, 188, 375, 750 ms for 9, 10, 11, 12, respectively.
// Thisequates  to  a  temperature  resolution  of  0.5°C,  0.25°C,  0.125°C,  or  0.0625°C.  
  // Set the resolution
  byte resolution = 11; 
  tempSensors[bytSensor].setResolution(deviceAddress, resolution);
  // Request temperature conversion - non-blocking / async
  tempSensors[bytSensor].setWaitForConversion(false);  
  tempSensors[bytSensor].requestTemperatures(); // Takes 14 ms       
  delay(375);
  // Get Sensor temp
  TpC[bytSensor] = tempSensors[bytSensor].getTempC(deviceAddress);
}

Before each temperature measurement, we check to see if a sensor is connected:



boolean IsSensorConnected(byte bytSensor) {  
//Check see if a sensor is connected 
  getAddResult[bytSensor] = tempSensors[bytSensor].getAddress(deviceAddress, 0);  
  return getAddResult[bytSensor]; 
}


Sensor ID and Sensor-Specific Calibration Constant

Every time that a new sensor is connected, the sketch obtains its 64-bit ID and sends it to the user. You can this unique sensor ID to the beginning of every data string so that you can easily identify specific sensor anywhere on a network of sensors.


void ReadSensorMemory(byte bytSensor) 
{    
  // Get sensor calibration coefficients stored on DS18B20's EEPROM
  // Uncomment line below only if you have a research-grade APAS T1 sensor
  //getCalibrationCoefficients(bytSensor); // Takes 25 ms
  
  // Get sensor ID and send it to computer
  getSensorID(bytSensor);  // Get sensor ID           
}

void getSensorID(byte bytSensor) { 
  if (getAddResult[bytSensor] == false) return;
  
  byte i;
  byte ID[8];  
  String strID = "";

  // Turn Sensor on
  SensorOnFun(true, SensorEnablePin[bytSensor]); 
  delay(20); // Wait for the Sensor to wake up

  // Start up the library / locate devices on the bus
  tempSensors[bytSensor].begin(); // This line might not be necessary
  
  // Initiate a search for the OneWire object created and read its value into addr array we declared above  
  while(oneWire[bytSensor].search(ID)) {
    // Read each byte in the address array
    for( i = 0; i < 8; i++) {
      // Put each byte in the ID array
      strID += ID[i]; 
      if (ID[i] < 10) {
         strID += "00"; 
      }
      if (ID[i] > 10 && ID[i] < 100) {
         strID += '0'; 
      }
      if (i == 1) {
        strID += ' '; 
      }
      if (i == 3) {
        strID += ' '; 
      }
      if (i == 5) {
        strID += ' '; 
      }
    }
    // A check to make sure that what we read is correct.
    if ( OneWire::crc8( ID, 7) != ID[7]) {
        // CRC is not valid!
        return;
    }
  }   
  oneWire[bytSensor].reset_search();

  // Turn Sensor off
  SensorOnFun(false, SensorEnablePin[bytSensor]); 

  // Send sensor ID to GUI
  Serial.print(F("Sensor "));
  Serial.print(bytSensor);
  Serial.print(F(" ID: "));
  Serial.println(strID);   
  Serial.flush(); // Wait untill all data are sent to computer before putting Arduino into sleep

  return;
}

Research-grade HITA E0 sensors come with a sensor code and sensor-specific calibration coefficient. This data could be used to identify the type of sensor connected (soil moisture or EC sensor) and to improve sensor-to-sensor uniformity by applying the calibration.



Analog to Digital Convertor

Each ADS1115 chip supports up to two sensors if configured for differential inputs. To have multiple ADC boards connected to the Arduino and therefore support more than two sensors, you need give each ADC board a separate address. To do so, you can connect the ADDR pin of one to GND (address: 0x48) and the ADDR of another one to VCC (address: 0x49).


// ADS1115 settings; 16-bit version
ADS1115_WE ads1115a(0x48);  // ADC board 1: ADDR pin connected to GND
ADS1115_WE ads1115b(0x49);  // ADC board 2: ADDR pin connected to VCC

The HITA E0 analog output (EC / TDS) ranges from ~0.0 to 2.0 V (this range is approximate). Configuring the ADS1115 for 2x gain allows it to read in the range of +/- 2.048 V (1 bit = 0.0625 mV), which is very close to what we need. We also set the ADC for a conversation rate of 860 samples per second (SPS).


// ADS1115 A to D
Wire.begin();

/// First ADC
ads1115a.init();
// 2x gain   +/- 2.048V  1 bit = 0.0625mV (16 bit) / 1.0mV (12 bit)
ads1115a.setVoltageRange_mV(ADS1115_RANGE_2048); 
ads1115a.setConvRate(ADS1115_860_SPS); // Other options: ADS1115_8_SPS, ADS1115_16_SPS, ADS1115_32_SPS, ADS1115_64_SPS, ADS1115_128_SPS, ADS1115_250_SPS, ADS1115_475_SPS, ADS1115_860_SPS) 

/// Second ADC
ads1115b.init();
// 2x gain   +/- 2.048V  1 bit = 0.0625mV (16 bit) / 1.0mV (12 bit)
ads1115b.setVoltageRange_mV(ADS1115_RANGE_2048); 
ads1115b.setConvRate(ADS1115_860_SPS); // Other options: ADS1115_8_SPS, ADS1115_16_SPS, ADS1115_32_SPS, ADS1115_64_SPS, ADS1115_128_SPS, ADS1115_250_SPS, ADS1115_475_SPS, ADS1115_860_SPS 

The rest of the ADC routines (below) read individual differential channels and provide a value that should fall between 0 (minimum) and 32,768 (maximum).


unsigned int avgADC(byte bytSensor) {
  float avg = measureADC(bytSensor); 
  avg /= 2048.0;
  Reading[bytSensor] = avg * 32768;
  
  return Reading[bytSensor];
}

float measureADC(byte bytSensor) {
  float rawADCReading;   
  switch (bytSensor) {
    case 0:
      rawADCReading = ads1115a_readChannel(ADS1115_COMP_0_1);  
      break;
    case 1:
      rawADCReading = ads1115a_readChannel(ADS1115_COMP_2_3); 
      break;
    case 2:
      rawADCReading = ads1115b_readChannel(ADS1115_COMP_0_1); 
      break;
    case 3:
      rawADCReading = ads1115b_readChannel(ADS1115_COMP_2_3); 
      break;
    default:
      // if nothing else matches, do the default
      break;
  }
  return rawADCReading;
}

float ads1115a_readChannel(ADS1115_MUX channel) {
  ads1115a.setCompareChannels(channel);
  ads1115a.startSingleMeasurement();
  while(ads1115a.isBusy()){}
  float volt = ads1115a.getResult_mV(); 
  return volt;
}

float ads1115b_readChannel(ADS1115_MUX channel) {
  ads1115b.setCompareChannels(channel);
  ads1115b.startSingleMeasurement();
  while(ads1115b.isBusy()){}
  float volt = ads1115b.getResult_mV(); 
  return volt;
}


Excitation Voltage

When it comes to connecting a sensor to the Arduino, one might not think much, and quickly connect the voltage wire/pin to the VCC of the circuitry. If you would like to save some power you can take an alternative strategy. We can connect the voltage wire of the HITA E0 to a digital pin of the Arduino, and turn the sensor on only when we want to take measurements. Later, sensor enable pins are initialized as 'output' in the setup subroutine.


const int SensorEnablePin[] = {6, 7, 8, 9};      // Sensor Voltage pin

boolean SensorOnFun(boolean blnOn, const int Pin)
{
  if (blnOn == true)
  {
    digitalWrite(Pin, HIGH); 
  } else {
    digitalWrite(Pin, LOW);
  }
}


Take Sensor Measurements

Taking sensor measurements is carried out in a number of steps as the following:

  1. Turn sensor on.

  2. Measure sensor analog output (EC) and obtain temperature from the DS18B20 [embedded] temp sensor.

  3. Obtain sensor ID (64-bit) and sensor code (1 byte).

  4. Turn sensor off.

  5. Compensate raw voltage readings for the effect of temperature on the electronics.

  6. Calculate EC.

  7. Calibrate EC using a general calibration equation.

  8. Compensate EC readings for the effect of temperature on electrical conductivity measurements.

  9. Conduct sensor-specific calibration (research-grade sensors only).

  10. Convert EC to TDS using the 442â„¢ standard curve (equation).


void doSensorReadings(byte bytSensor)
{
  // Turn sensor on  
  SensorOnFun(true, SensorEnablePin[bytSensor]);  
  // Measure sensor analoge output voltage
  RAW[bytSensor] = measureSensor_Temp_VWC_EC(bytSensor); 
  // Obtain sensor ID and sensor code
  if (blnNewSensor[bytSensor] == true) {
    blnNewSensor[bytSensor] = false;
    ReadSensorMemory(bytSensor); // Do this only once right after a sensor is connected. 
  } 
  // Turn sensor off
  SensorOnFun(false, SensorEnablePin[bytSensor]);
   
  if (getAddResult[bytSensor] == false) {
    blnNewSensor[bytSensor] = true;
    return; // Sensor was not connected
  }

  // Calculate EC
  EC25[bytSensor] = calculateEC(RAW[bytSensor], TpC[bytSensor], bytSensor);

  // Calibrate EC
  EC25[bytSensor] = CalibrateEC(EC25[bytSensor]);

  // Temperature-compenscate EC
  EC25[bytSensor] = ECTempCompensate(TpC[bytSensor], EC25[bytSensor]);
  
  // Calculate TDS
  ConvertECtoTDSCurve(bytSensor); 
}

As explained, before each temperature measurement, we check to see if a sensor is connected. If a sensor was available at the port, we first measure the temperature and then read the HITA E0 analog output (EC).


unsigned int measureSensor_Temp_VWC_EC(byte bytSensor)
{      
  delay(5); // Start with a delay to make sure sensor is ready 
  IsSensorConnected(bytSensor); 
  if (getAddResult[bytSensor] == false) { 
    return 0; // If sensor is not connected just return.
  }     
  measSensorTemp(bytSensor); // Measure temperature
  avgReading[bytSensor] = avgADC(bytSensor); // Takes -- ms
  
  return avgReading[bytSensor]; 
}


Temperature Compensation

Substrate/soil temperature (Ts) is a useful parameter in monitoring crop growth. It is also a key factor contributing to the inaccuracy of EC measurements, because Ts changes sensor readings. It is also known that electrolytic conductivity increases by about 1.9% per degree centigrade increase in temperature. This can cause EC to Go Up when the substrate/soil EC is actually decreasing and vice versa. It is worth knowing that sensor temperature sensitivity also affects the calibration process and contributes even more to the inaccuracy. This is why we have added an accurate temperature sensor to our design to measure Ts and account for its effect on EC.


Having this in mind, we have developed a mathematical model (equation) that takes EC readings and temperature as input variables and provides EC readings that are compensated for temperature change. This procedure reduces the error to a negligible level.


To learn about the equations used in the code, please refer to the HITA E0 sensor user manual.


float ECTempCompensate(float Tp, float fltEC) { 
  // Simplified method
  //const float ft = 0.0185; // considered the standard for plant nutrient solutions
  //float ECTemp = fltEC/( 1 + ft * (Tp - 25.0)); 

  // Advanced method: US  Salinity  Laboratory  Staff,  1954 (http://www.fao.org/3/x2002e/x2002e.pdf)
  float dT = (Tp - 25.0) / 10.0;
  float ft = 1 - 0.20346 * (dT) + 0.03822 * pow(dT, 2) - 0.00555 * pow(dT, 3);
  float ECTemp = ft * fltEC;
  
  if (ECTemp <= 0) ECTemp = 0.0;
  if (ECTemp >= 10.00) ECTemp = 10.0;
  
  return ECTemp;
}


Calculate Electrical Conductivity

EC measures the ability of a solution or substrate to conduct an electric current. EC is directly related to the amount of salts (ions) available in the solution. The higher the amount of salts, the higher the EC. Daily EC measurements can tell you if your solution has lost water or nutrients. You can adjust high EC by adding water and low EC by adding more nutrients.

​

The HITA E0 is an amperometric sensor, meaning that it uses an Alternating Current (AC) to measure EC. The current is applied through the carbon ink electrodes of the sensor. The amount of current that passes between the two electrodes is a function of the of salts (ions) present in the substrate or solution, and is translated into the resistance between the electrodes. EC is then calculated as the inverse of the resistance (R) (inverse of resistance is called conductance) multiplied by a value referred to as "cell constant" (C).


The equations used to calculate EC from HITA E0 output voltage readings are explained in its user manual in detail.



Calculate TDS

TDS stands for Total Dissolved Solids and quantifies the concentration of dissolved solids in a nutrient solution. TDS is argubaly a more suitable parameter than EC for measuring nutrient concentration, considering it quantifies the amount of nutirents rather through the implication of conductivity. We can use a polynomial equation (based on the 442â„¢ curve for natural water) to convert EC reading to TDS. Please refer to the user manual of the HITA E0 for more information.


Note: TDS is not calculated for EC values that are smaller than 0.1 dS/m.


void ConvertECtoTDSCurve(byte bytSensor) {
  if (EC25[bytSensor] > 0.1) {
    //intTDS[bytSensor]  = 8.2876 * EC25[bytSensor]^2 + 779.46 * EC25[bytSensor] - 74.853
    intTDS[bytSensor] = 8.2876 * pow(EC25[bytSensor], 2);
    intTDS[bytSensor] += 779.46 * EC25[bytSensor];
    intTDS[bytSensor] += 74.853;

  } else {
    intTDS[bytSensor] = 0;
  }  
} 

Sensor-Specific Calibration

As a result of electronic components tolerance and manufacturing process, there is a degree of non-uniformity among the HITA E0 sensors. To decrease the sensor-to-sensor non-uniformity, we calibrate individual sensors (research-grade only) and program a calibration value into their memory. This value could be positive or negative. Once added to sensor readings, all sensors will read the same EC value under similar conditions.



Send Data to Computer

Similar to the rest of the sketch, the serial data subroutine is organized in a way to send data from up to four HITA E0 sensors to the computer.


void sendAllDataToPC(byte bytSensor)
{
  // Send all measurements to PC
  // Sensors 0, 1, 2, 3  
  if (getAddResult[bytSensor] == true)
  {        
    /// If it's HITA E0 EC sensor
    serialPrintFunc (1000 + bytSensor, EC25[bytSensor], TpC[bytSensor], intTDS[bytSensor]);  // SI units
         
  } else { // Sensor is disconnected, so reset all related variables.     
    serialPrintFunc (100 + bytSensor, 0, 0, 0);  // Let computer know Sensor is disconnected 
  }   
}

int serialPrintFunc (int intCommandCode, float fltB, float fltC, float fltD) 
{ 
  Serial.print(F(">")); 
  Serial.print(intCommandCode);
  serialPrintFuncSum2(',',  fltB);
  serialPrintFuncSum2(',',  fltC);
  serialPrintFuncSum2(',',  fltD);
  Serial.println(F(",")); 

  Serial.flush(); // Wait untill all data are sent to computer before putting Arduino into sleep
}
void serialPrintFuncSum2(char Letter,  float fltValue)
{
  Serial.print(Letter); // Send coefficients to GUI!
  Serial.print(fltValue);
}

Each sensor data string starts with a unique code that can be used to identify specific sensor. Data strings that are sent to the computer are comprised of four different numbers with the following format:


> [sensor code],[EC(dS/m)],[temperature(°C)],[TDS],


Here's example strings that is generated for the HITA E0 sensor:


>1000,0.53,24.62,492.00,


Figure 1. Arduino IDE serial monitor.

Important Note: The sketch takes sensor readings every 5 sec. You can increase the sampling interval to 60 sec or more. It is also strongly recommended that you take averages of several readings (say every 15 min). The averaging will improve the quality of data by removing noise, and give you a smoother EC or TDS curve.



Watchdog Timer

It is always a good idea to use a watchdog timer to make sure the Arduino automatically restarts in the case of an internal error, and this is what we have here. The watchdog timer overflow time depends on the Arduino board.


// Configure Watchdog Timer
  Watchdog.enable(30000); 

The time is reset in the loop right after taking sensor measurements:


void loop(void)
{ 
  // Sensor 1
  doSensorReadings(0); // Read thesensor
  sendAllDataToPC(0); // Send all measurements to PC
  /*
  // Sensor 2
  doSensorReadings(1); // Read thesensor
  sendAllDataToPC(1); // Send all measurements to PC
  // Sensor 3
  doSensorReadings(2); // Read thesensor
  sendAllDataToPC(2); // Send all measurements to PC
  // Sensor 4
  doSensorReadings(3); // Read thesensor
  sendAllDataToPC(3); // Send all measurements to PC
  */
  
  // A little bit of delay between measurements
  delay(5000);
  
  Watchdog.reset(); // Reset watchdog.
}


Downloads

I have put the sketch file that was discussed here on GitHub.



0 comments
bottom of page