A Updated PH Probe, Now gives A Readout For Probe Life and a time alarm for when to Calibrate.
DFRobot were nice enough to send me one of their pH pro MCU probes [Longer lasting] but this tutorial and code will work for any analog pH MCU meter [ebay special etc] these are coming down in price rapidly but I recommend calibrating often.
The Analog pH breakout boards often have a little pot for adjusting during calibration, this is a pain in the ass at calibration time. This new code uses a different method and is all code based.
[Turn Pot on PH meter fully anticlockwise, we are now using a better Calibration method]
GUI
The LCD has a few useful Screens
How It works:
pH Measurement and Calibration
pH probes output a small voltage change [from-170mv to 170mv, for ph 4-10], the breakout board is essentially a op amp with an offset. So it now outputs a 0-5v signal.
These boards are made with a potentiometer to change the gain of the amplifier to calibrate, good idea but very awkward, we instead will be giving it a set gain and calibrating in the code.
So what are the steps if you have a different board:
>Turn Pot fully clockwise
>Short BNC connector together and read mv [from serial] This is your Ideal pH reading, the DF robot is 1.96v
>Connect probe and read mv [from serial] when in pH4 and pH7, The probe change will be 170mv, you now have a way to work out the gain of the amp.
pH = 7 -(Ph_Calibration_mv - measured_mv)*gain // Simple?
Doing it this way we can read pH and calibrate without messing with that pot ever again, by taking three readings 1:Ideal pH 7 mv , 2:Actual pH7 mv , 3: pH4mv
The LCD GUI will guide you thru a Calibration process:
Probe Life
The LCD screen with Probe life should have all readings above 50% to be considered a probe in good health, if ones lower I recommend cleaning it or ordering a replacement [It will fail soon].
There are three indicators of probe life when we calibrate.
http://reagecon.com/pdf/technicalpapers/Electrode_CM_v5_TSP-02_Issue_4.pdf
In the Code these are showed as:
H1: pH7 offset [pH7 should output the ideal voltage of 1.96, as they age they get an offset
H2 Slope, change in mv between pH4 calibration and pH7
H3:Drift , Healthy probes should have reached a s.s value after 60 seconds, this one checks for a difference between a reading at 60 seconds and 120
A more Technical Explanation of the health checks:
//there are three ways we can measure a proble life during calibration:
//1:Asymmetry potential (Eo), the Millivoltes at pH7 [mV reading in pH 7.00 buffer ± 25 mV]
//2:Slope, mv per Ph change [mV reading in pH 7.00 buffer - mV reading in pH 4.00 buffer 160 – 180 mV]
//3:Drift mV reading in pH 4.00 buffer (1 min) – mV reading in pH 4.00 buffer (2 min) ± 1.5 mV [we are checking for stable readings]
I have tested this code with good probes, failed probes and aged b probes and it works very well at identifying them.
Got a probe with a low Health indicator one or three? check out the blog on reviving a probe:
Blog:13.9 - PH Probe Revival: Ultrasonic Cleaning
The Code
This reads the ph probes shield [its an analog value] averages ten readings to reduce any noise and converts the ADC reading into a ph value. outputting the reading to serial.
The code works and it works great, I did however group into one variable the gain and slope, this means the health check values are only valid for analog probes, there will be an updated I2C version soon.
| Header 1 |
|---|
/* Script to print PH to serial
* Turn Pot fully counter clockwise[All the way it should take multiple turns], we arnt ussing it anymore for calibration
28/8/2015 Michael Ratcliffe Mike@MichaelRatcliffe.com
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Parts: -Arduino - Uno/Mega -df Robot Ph Probe Kit [SKU:SEN0169] This is a great PH sensor compared to the ones ive had in the past
See www.MichaelRatcliffe.com/Projects for a Pinout and user guide or consult the Zip you got this code from
*/
//************************** Libraries Needed To Compile The Script [See Read me In Download] ***************// // Both below Library are custom ones [ SEE READ ME In Downloaded Zip If You Dont Know how To install] Use them or add a pull up resistor to the temp probe
#include "OneWire.h" #include "DallasTemperature.h" #include "LiquidCrystal.h" //Standard LCD Lbrary #include "EEPROM.h" //Standard EEPROM Library
//*********************** User defined variables ****************************// //The Number of Days before the LCD will tell you its time to Calibrate int Cal_Period_Warining=30; //pH meter Analog output to Arduino Analog Input 0 int PHPin =A15; //I got this from shorting the nbc's cnetre pin to outer pin [simulated the perfect probe] open serial and its the mv reading that you want to put here float Healthy1_mv=1.96;
//************ Temp Probe Related *********************************************// #define ONE_WIRE_BUS 26 // Data wire For Temp Probe is plugged into pin 10 on the Arduino const int TempProbePossitive =22; //Temp Probe power connected to pin 9 const int TempProbeNegative=24; //Temp Probe Negative connected to pin 8 float Temperature=0.0; float MinT=100; float MaxT=0;
//********************** End Of Recomended User Variables ******************//
//************** Some values for working out the ph*****************//
float mvReading=0; float Vs=5; float phValue=0; int i=0; long reading=0; unsigned long sum=0; float average=0; //used for min/max logs float MinPH=10; float MaxPH=0;
//************** Variables used to determin probe health **********// //there are three ways we can measure a proble life during calibration: //1:Asymmetry potential (Eo), the Millivoltes at pH7 [mV reading in pH 7.00 buffer ± 25 mV] //2:Slope, mv per Ph change [mV reading in pH 7.00 buffer - mV reading in pH 4.00 buffer 160 – 180 mV] //3:Drift mV reading in pH 4.00 buffer (1 min) – mV reading in pH 4.00 buffer (2 min) ± 1.5 mV [we are checking for stable readings] int ProbeLife1=0; float mvReading_7=0;
//Some major brands will have this set at 35mv, we have a gain of around 8 in our op amp float Health1_range =0.28; float Healthy1_mv2=1.96;
//I got these from a healthy probe and extrapolating the slope deviation int ProbeLife2=0; float Slope=0; float mvReading_4=0; float Healthy2_Slope=2.15; float Healthy2_range=0.25;
int ProbeLife3=0; float Drift=0; float mvReading_4_Delayed=0; float Healthy3_Drift=0; float Healthy3_range=0.02;
//some Variables for Probe Calibration Time int Last_Day=0; int Days_Since_Calibration=0; int CalibrationWarning=0;
float offset=0;
// select the pins used on the LCD panel LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
// define some values used by the panel and buttons int lcd_key = 0; int adc_key_in = 0; int button =0; #define btnRIGHT 1 #define btnUP 2 #define btnDOWN 3 #define btnLEFT 4 #define btnSELECT 5 #define btnNONE 6
int Screen =1; //Max number of screens on lcd const int Last_Screen_no =6; //used to debounce input button int buttonLast=0;
OneWire oneWire(ONE_WIRE_BUS);// Setup a oneWire instance to communicate with any OneWire devices DallasTemperature sensors(&oneWire);// Pass our oneWire reference to Dallas Temperature.
//************************** Just Some basic Definitions used for the Up Time LOgger ************// long Day=0; int Hour =0; int Minute=0; int Second=0; int HighMillis=0; int Rollover=0;
//Used For Calibration timing unsigned long StartCalibration1=0;
//***************** Specifying where to sotre the calibration value [non volatile memory **// int value=0; //we use this to check if memory has been writen or not int addresCalibrationPH4=0; int addresCalibrationPH7=50; int addresProbleLife1=100; int addresProbleLife2=150; int addresProbleLife3=200; int addresseCalibrationDays=250;
//************************************** Setup Loop Runs Once ****************// void setup() { Serial.begin(9600); pinMode(TempProbeNegative , OUTPUT ); //seting ground pin as output for tmp probe digitalWrite(TempProbeNegative , LOW );//Seting it to ground so it can sink current pinMode(TempProbePossitive , OUTPUT );//ditto but for positive digitalWrite(TempProbePossitive , HIGH ); read_Temp();// getting rid of the first bad reading Read_Eprom(); Slope_calc(); Splash_Screen();
if(Days_Since_Calibration>=Cal_Period_Warining) CalibrationWarning=1;
//Adjusting some values for use later in map function Healthy1_mv2=Healthy1_mv*1000; Health1_range =Health1_range*1000;
Healthy2_Slope=Healthy2_Slope*1000; Healthy2_range=Healthy2_range*1000;
Healthy3_Drift=Healthy3_Drift*1000; Healthy3_range=Healthy3_range*1000;
} //******************************** End Of Setup **********************************//
//******************** Main Loops runs Forver ************************************// void loop() {
//All these functions are put below the main loop, keeps the loop logic easy to see read_LCD_buttons(); read_Temp(); Log_Min_MaxTemp(); ReadPH(); Log_Min_MaxPH(); uptime(); //Used to see hoe many days ago the probe was calibrated Day_Change(); CalibratePH(); PrintReadings(); delay(100); };
//************************** End Of Main Loop ***********************************//
//*************************Print Some useful startup info **************************// void startupinfo(){ Serial.println("pH Probe Script for arduino"); Serial.println("Released under GNU by Michael Ratcliffe"); Serial.println("www.MichaelRatcliffe.com"); Serial.println("Element14 'Adapted_Greenhouse'"); Serial.println("Using DFRobot PH Probe Pro "); Serial.println("How to Use:"); Serial.println("1:Place Probe into pH7 calibration fluid, open serial "); Serial.println("2:Take Recomened cell constand and change it in the top of code"); Serial.println("3:Rinse Probe and place in pH4 calibration fluid"); Serial.println("4:Adjust potentiometer on pH meter shield until ph reading in serial is 4"); Serial.println(" "); Serial.println("Thats it your calibrated and your readings are accurate!");
}
//***************************** Function to read temperature ********************************// void read_Temp(){ sensors.requestTemperatures();// Send the command to get temperatures Temperature=sensors.getTempCByIndex(0); //Stores Value in Variable
}
//*************************Take Ten Readings And Average ****************************// void ReadPH(){ i=0; sum=0; while(i<=20){ reading=analogRead(PHPin); sum=sum+reading; delay(10); i++; } average=sum/i;
//Converting to mV reading and then to pH mvReading=average*Vs/1024; //phValue=mvReading*K_PH; phValue=(7-((mvReading_7-mvReading)*Slope));
}
//****************************** Reading LCd Buttons ****************************// void read_LCD_buttons(){ adc_key_in = analogRead(0); // read the value from the sensor // my buttons when read are centered at these valies: 0, 144, 329, 504, 741 // we add approx 50 to those values and check to see if we are close if (adc_key_in > 1000) button =0;
else if (adc_key_in < 50) button =1; else if (adc_key_in < 250) button =2; else if (adc_key_in < 450) button =3; else if (adc_key_in < 650) button =4; else if (adc_key_in < 850) button =5;
//Second bit stops us changing screen multiple times per input if(button==2&&buttonLast!=button){ Screen++;
} else if (button==3&&buttonLast!=button){ Screen--; };
if (Screen>=Last_Screen_no) Screen=Last_Screen_no; if(Screen<=1) Screen=1;
buttonLast=button; };
//************************ Uptime Code - Makes a count of the total up time since last start ****************//
void uptime(){ //** Making Note of an expected rollover *****// if(Day>=30){ HighMillis=1;
} //** Making note of actual rollover **// if(millis()<=100000&&HighMillis==1){ Rollover++; HighMillis=0; } //Calculating the uptime long secsUp = millis()/1000;
Second = secsUp%60;
Minute = (secsUp/60)%60;
Hour = (secsUp/(60*60))%24;
Day = (Rollover*50)+(secsUp/(60*60*24)); //First portion takes care of a rollover [around 50 days]
};
//************************** Printing somthing useful to LCd on start up **************************// void Splash_Screen(){
lcd.begin(16, 2); // start the library lcd.setCursor(0,0); delay(1000); lcd.print("PH meter "); lcd.setCursor(0,1); delay(1000); lcd.print("Mike Ratcliffe"); lcd.setCursor(0,1); delay(1000); lcd.setCursor(0,1); lcd.print("Free Software "); delay(1000); lcd.setCursor(0,1); lcd.print("Mike Ratcliffe"); delay(1000); lcd.setCursor(0,1); lcd.print("Free Software "); delay(1000); lcd.setCursor(0,0); lcd.print("To Navigate "); lcd.setCursor(0,1); lcd.print("Use Up-Down "); delay(3000); lcd.setCursor(0,0); lcd.print("To Calibrate "); lcd.setCursor(0,1); lcd.print("Hold Select "); delay(3000);
};
//******************** calculating the PhMeter Parameters ***************************************// void Slope_calc(){
offset=Healthy1_mv-mvReading_7; Slope=3/(Healthy1_mv-mvReading_4-offset);
}
//*************************** Checking what we stored in non volatile memory last time ************// void Read_Eprom(){
//************** Restart Protection Stuff ********************// //the 254 bit checks that the adress has something stored to read [we dont want noise do we?] value = EEPROM.read(addresCalibrationPH7); mvReading_7=value*Vs/256; delay(10);
value = EEPROM.read(addresCalibrationPH4); mvReading_4=value*Vs/256; delay(10); //Checking for Probe Life 1 Indicator value = EEPROM.read(addresProbleLife1); ProbeLife1=value; delay(10); //Probe:ife 2 saved value = EEPROM.read(addresProbleLife2); ProbeLife2=value; delay(10); //Probe:ife 3 saved value = EEPROM.read(addresProbleLife3); ProbeLife3=value; delay(10);
//Checking memory slot for probe calibration time Days_Since_Calibration = EEPROM.read(addresseCalibrationDays);
};
//******************************* Checks if Select button is held down and enters Calibration routine if it is ************************************// void CalibratePH(){
//we check if we are on ph screen and the select button is held if(Screen!=4) return; if(button!=5) return; else delay(1000); read_LCD_buttons(); if(button!=5) return;
//we need to stop in this loop while the user calibrates while(1){ read_LCD_buttons(); lcd.setCursor(0,0); lcd.print("Ph Probe in pH7 "); lcd.setCursor(0,1); lcd.print("Press Right "); //user pressed right? if(button==1) break; delay(100); }; StartCalibration1=millis();
//We are giving the probe 1 minute to settle while(millis()<StartCalibration1+60000){ lcd.setCursor(0,0); lcd.print("Calibrating "); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); lcd.print(StartCalibration1+60000-millis()); lcd.setCursor(7,1); lcd.print(":mSeconds"); delay(1000); }; ReadPH(); delay(100); ReadPH();
//*******Saving the new value to EEprom**********// mvReading_7=mvReading;
while (1) { // wee need to keep this function running until user opts out with return function
read_LCD_buttons(); if(button==3) return; //exits the loop without saving becauser user asked so
if(millis()%4000>=2000){
lcd.setCursor(0,0); lcd.print("Calibrated "); lcd.setCursor(0,1); lcd.print("Select To Save ");
} else{
lcd.setCursor(0,1); lcd.print("Down to Exit "); };
if (button==5) break; } //read the ph probe
//Saving the value steight from ADC, removes conversion errors value= average/4; EEPROM.write(addresCalibrationPH7,value); //Resetting the days sinc calibration Days_Since_Calibration=0; CalibrationWarning=0; Slope_calc(); ProbeLife_Check_1(); EEPROM.write(addresseCalibrationDays,Days_Since_Calibration);
//Displaying the new offset lcd.setCursor(0,0); lcd.print("Saved Calibration ");
lcd.setCursor(0,1); lcd.print("Offset "); lcd.setCursor(3,1); lcd.print(offset); delay(2000); //move onto pH4 Calibration
lcd.setCursor(0,0); lcd.print("Rinse and ");
lcd.setCursor(0,1); lcd.print("Placein pH4 ");
delay(4000);
while(1){ lcd.setCursor(0,0); lcd.print("Press Right ");
lcd.setCursor(0,1); lcd.print("If Probe in 4 "); //move onto next stage if select is held read_LCD_buttons(); if (button==1) break; } StartCalibration1=millis();
//We are giving the probe 1 minute to settle while(millis()<StartCalibration1+60000){ lcd.setCursor(0,0); lcd.print("Calibrating "); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); lcd.print(StartCalibration1+60000-millis()); lcd.setCursor(7,1); lcd.print(":mSeconds"); delay(1000); }; ReadPH(); delay(100); ReadPH();
mvReading_4=mvReading;
//We are giving the probe 1 minute to settle StartCalibration1=millis(); while(millis()<=StartCalibration1+60000){ lcd.setCursor(0,0); lcd.print("Health Check "); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); lcd.print(StartCalibration1+60000-millis()); lcd.setCursor(7,1); lcd.print(":mSeconds"); delay(1000); }; ReadPH(); delay(100); ReadPH(); mvReading_4_Delayed=mvReading; //Saving ADc readout, to remove conversion errors value= average/4; EEPROM.write(addresCalibrationPH4,value); Slope_calc(); ProbeLife_Check_2(); ProbeLife_Check_3();
//Put back to main screen and exit calibration lcd.setCursor(0,0); lcd.print("Saved Calibration ");
delay(1000); //Informing the use about the probe life while(1){ read_LCD_buttons();
if(ProbeLife2>=50 && ProbeLife2>=50 &&ProbeLife3>=50){ lcd.setCursor(0,0); lcd.print("Probe Condition "); lcd.setCursor(0,1); lcd.print("Good "); }
if(ProbeLife2<50 || ProbeLife2<50 || ProbeLife3<50){ lcd.setCursor(0,0); lcd.print("Probe Condition "); lcd.setCursor(0,1); lcd.print("Faulty "); }
if(millis()%6000 <=3000){ lcd.setCursor(0,0); lcd.print("Press Right ");
lcd.setCursor(0,1); lcd.print("To exit "); };
//user pressed right? if(button==1) break; delay(100); }; Screen=1; return;
};
//******************************* LOGS Min/MAX Values*******************************// void Log_Min_MaxTemp(){
if(Temperature>=MaxT) MaxT=Temperature; if(Temperature<=MinT) MinT=Temperature;
};
//******************************* LOGS Min/MAX Values*******************************// void Log_Min_MaxPH(){
if(phValue>=MaxPH) MaxPH=phValue; if(phValue<=MinPH) MinPH=phValue;
};
void PrintReadings(){
Serial.print("pH: "); Serial.print(phValue); Serial.print(Temperature); Serial.println(" *C "); Serial.print("mv: "); Serial.print(mvReading); Serial.print(" mvPh7: "); Serial.print(mvReading_7); Serial.print(" mvPh4: "); Serial.println(mvReading_4); Serial.print("H1: "); Serial.print(ProbeLife1); Serial.print(" H2: "); Serial.print(ProbeLife2); Serial.print("H3: "); Serial.println(ProbeLife3);
Serial.print("Slope "); Serial.print(Slope);
//** First Screen Shows Temp and EC **// if(Screen==1){ lcd.setCursor(0,0); lcd.print("Arduino pH "); lcd.setCursor(0,1); lcd.print("pH: "); lcd.setCursor(3,1); lcd.print(phValue); lcd.setCursor(9,1); lcd.print(Temperature); lcd.print("'C"); }
//**Third Screen Shows Min and Max **// else if(Screen==2){ lcd.setCursor(0,0); lcd.print("Min: "); lcd.setCursor(4,0); lcd.print(MinPH); lcd.setCursor(9,0); lcd.print(MinT); lcd.print("'C"); lcd.setCursor(0,1); lcd.print("Max: "); lcd.setCursor(4,1); lcd.print(MaxPH); lcd.setCursor(9,1); lcd.print(MaxT); lcd.print("'C"); }
else if(Screen==6){
lcd.setCursor(0,0); lcd.print("Uptime Counter: ");
lcd.setCursor(0,1); lcd.print(" ");//Clearing LCD lcd.setCursor(0,1); lcd.print(Day); lcd.setCursor(3,1); lcd.print("Day"); lcd.setCursor(8,1); lcd.print(Hour); lcd.setCursor(10,1); lcd.print(":"); lcd.setCursor(11,1); lcd.print(Minute); lcd.setCursor(13,1); lcd.print(":"); lcd.setCursor(14,1); lcd.print(Second);
}
else if(Screen==4){
lcd.setCursor(0,0); lcd.print("Calibrate pH ");
lcd.setCursor(0,1); lcd.print("Hold Select ");
} else if(Screen==3){
lcd.setCursor(0,0); lcd.print("Probe Health % ");
lcd.setCursor(0,1); lcd.print("H1: "); lcd.setCursor(3,1); lcd.print(ProbeLife1); lcd.setCursor(9,1); lcd.print("H2:"); lcd.setCursor(12,1); lcd.print(ProbeLife2); if(millis()%6000<3000){ lcd.setCursor(0,1); lcd.print("H3: "); lcd.setCursor(3,1); lcd.print(ProbeLife3); } }
else if(Screen==5){
lcd.setCursor(0,0); lcd.print("Days Since Cal ");
lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); lcd.print(Days_Since_Calibration);
}
if((millis()%10000<=2000) && (CalibrationWarning==1)){
lcd.setCursor(0,0); lcd.print("Calibrate the "); lcd.setCursor(0,1); lcd.print("Probe ");
};
};
//************************************* Used For Probe time Since Last Calibration *************************************// //This wont keep counting if unit has been powered off, so it is only really useful for use when in permanant use void Day_Change(){ if(Day!=Last_Day){ Last_Day=Day; Days_Since_Calibration++; EEPROM.write(addresseCalibrationDays,Days_Since_Calibration); if(Days_Since_Calibration>=Cal_Period_Warining) CalibrationWarning=1; };
};
//********************** The Section Below will give you a read out of probe Life ****************************************// //there are three ways we can measure a proble life during calibration: //1:Asymmetry potential (Eo), the Millivoltes at pH7 [mV reading in pH 7.00 buffer ± 25 mV] //2:Slope, mv per Ph change [mV reading in pH 7.00 buffer - mV reading in pH 4.00 buffer 160 – 180 mV] //3:Drift mV reading in pH 4.00 buffer (1 min) – mV reading in pH 4.00 buffer (2 min) ± 1.5 mV [we are checking for stable readings]
void ProbeLife_Check_1(){ //Structure, get pH7 mv reading and map life vs mv reading , variables Good value, band for bad reading //We already just got the mvReading int he calibration function //map(value, fromLow, fromHigh, toLow, toHigh);
value=mvReading_7*1000; if (value>=Healthy1_mv2) ProbeLife1=map(value, Healthy1_mv2, (Healthy1_mv2+Health1_range), 100, 0); if (value<Healthy1_mv2) ProbeLife1=map(value, Healthy1_mv2, (Healthy1_mv2-Health1_range), 100, 0); if (ProbeLife1<=1) ProbeLife1=0; EEPROM.write(addresProbleLife1,ProbeLife1); };
void ProbeLife_Check_2(){ //structure compare ph7 mv to ph4 mv , variables Good value Band for bad reading
value=Slope*1000; if (value>=Healthy2_Slope) ProbeLife2=map(value, Healthy2_Slope,(Healthy2_Slope+ Healthy2_range), 100, 0); if (value<Healthy2_Slope) ProbeLife2=map(value, Healthy2_Slope,(Healthy2_Slope-Healthy2_range), 100, 0); if (ProbeLife2<=1) ProbeLife2=0; EEPROM.write(addresProbleLife2,ProbeLife2); };
void ProbeLife_Check_3(){ //structure, leave probe in ph 4 for 1 minute and then take another average at 2 mins map from 0 drift to 5mv for probe life Drift=(mvReading_4-mvReading_4_Delayed); value=Drift*1000; if (value>=Healthy3_Drift) ProbeLife3=map(value, Healthy3_Drift,(Healthy3_Drift+ Healthy3_range), 100, 0); if (value<Healthy3_Drift) ProbeLife3=map(value, Healthy3_Drift,(Healthy3_Drift- Healthy3_range), 100, 0); if (ProbeLife3<=1) ProbeLife3=0; EEPROM.write(addresProbleLife3,ProbeLife3); }; |



Top Comments