top of page

Connecting APAS T1 Soil Moisture and Temperature Sensor to Arduino (Part 2: Arduino C++ Codes)

Updated: Apr 24

Please read Part 1 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. 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.



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 APAS T1 relies on the DS18B20 chip for temperature measurements. To simplify the code, each temp sensor is assigned to different Arduino pin. If you have 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 APAS T1 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 APAS T1 analog output (moisture) ranges from ~0.5 to 1.5 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 9500 (minimum) and 20000 (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 have connected the voltage wire of the APAS T1 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 few steps as the following:

  1. Turn sensor on.

  2. Measure sensor analog output (moisture) 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 soil moisture readings for temperature effect (research-grade sensors only).

  6. Calculate soil water content in %.

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


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;
    // Do this only once right after a sensor is connected. 
    ReadSensorMemory(bytSensor); 
  } 
  
  // Turn sensor off
  SensorOnFun(false, SensorEnablePin[bytSensor]); // Turn sensor off      
  
  if (getAddResult[bytSensor] == false) {
    blnNewSensor[bytSensor] = true;
    return; // Sensor was not connected
  }
   
  // Compensate raw WC readings for temp fluctuations
  RAW[bytSensor] = tempCompensation(RAW[bytSensor], TpC[bytSensor], bytSensor);
  
  // Calculate water content (%)
  VWC_P[bytSensor] = calculateWaterContent(RAW[bytSensor], bytSensor);      
  
  // Conduct sensor-specific calibration
  VWC_P[bytSensor] = sensorSpecificCalibration(VWC_P[bytSensor], 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 APAS T1 analog output (moisture).


unsigned int measureSensor_Temp_VWC_EC(byte bytSensor)
{      
  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]; 
}


Calculate Water Content

To calculate the water content (moisture) in percent (%) using raw data, we need to determine the minimum and maximum readings first. The minimum is basically sensors readings in the air, and the maximum is obtained by submerging the sensor in the water.

Note: Both the sensor blade (green section) and head (black) are sensitive to moisture. If you only submerge the blade you will read differently than if you submerge the whole sensor. We have calibrated the APAS T1 sensor for soilless media, in which the sensor head won't be in contact with the substrate/moisture.


const float MINRAW_WC = 9600.0;  // equaivalant of 0.6 V ((9600/2^16)*4.096) 
const float MAXRAW_WC = 18000.0; // equaivalant of 1.125 V ((18000/2^16)*4.096) 

float  calculateWaterContent(unsigned int rVWC, byte bytSensor)
{
  float pVWC; 
  float fltDenom = MAXRAW_WC  - MINRAW_WC;      
  // Set the scale to 0-100 using values obtained in the air and water.
  pVWC = 100.0 * ((rVWC - MINRAW_WC) / fltDenom);
  
  return pVWC;
}


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 APAS T1 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 APAS T1 soil moisture sensor      
    serialPrintFunc (10 + bytSensor, VWC_P[bytSensor], TpC[bytSensor], RAW[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],[soil moisture(%)],[temperature(°C)],[soil moisture(raw)],


Here's example strings that is generated for the APAS T1 sensor:


>10,2.69,24.62,9826.00,


Figure 1. Arduino IDE serial monitor.

Important Note: The sketch takes sensor readings every 5 sec. Considering the fact that soil moisture does not change that quickly, 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 60 min). The averaging will improve the quality of data by removing noise, and give you a smoother soil moisture 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.
}

Download

You can find the sketch here on GitHub.



0 comments
bottom of page