• Email: info@sparkyswidgets.com

LeoPhi USB enabled Arduino pH sensor interface

LeoPhi USB enabled Arduino pH sensor interface

Project description

LeoPhi Arduino pH sensor is the next extension to our pH interface tutorial, by adding an Atmega32u4 and using a modified Arduino Leonardo bootloader we have made an easy to use, easy to program and very flexible unit. Many users asked for the added flexibility of multiple communication protocols on top of the analog front end digitization for transmission! This unit has quite popular, but there wasn't enough documentation and information available. I decided to create a project page for this project and gather all the firmware, bootloader schematics and example code here. LeoPhi Arduino pH sensor has been updated to Version 2 with some nice features added!


LeoPhi Arduino pH sensor interface running a 20x4 char display, its easy to make a standalone meter with LeoPhi!

GitHub Reposidget for WordPress

SparkysWidgets / WinArdBuild

Current Windows Arduino Build, features some fixes and additions to include out Leo Line of sensors

GitHub Reposidget for WordPress

SparkysWidgets / OSXArdBuild

Current OSX Arduino Build, features some fixes and additions to include our Leo Line of sensors


This project combines the basic analog portion of the pH interface tutorial with a micro controller. For a number of reasons we settled on the USB enabled Atmega32u4, which is a fully USB compatible, with 2.5k of RAM and 32K of flash. It is also the chosen MCU for the Arduino Leonardo and allows users a lot of flexibility in adding pH reading and control to any project. Since the release of the Leonardo and Arduino1.0.1 it is very easy to use and program this unit to match whatever a project demands.

Since there is an on board MCU that is fully programmable it makes communication over various common protocols very easy. The Atmega32u4 supports Serial over USB, Serial over hardware USART, SPI, I2C, 1wire and more. This was a common request and I felt it was a good feature to base the design around, easy to use and expand for many communication methods.

Version 2 of LeoPhi Arduino pH sensor, brings the full digital and analog ports of the Arduino Leonardo out to 2 different headers, and still includes the ISP header should those extra IO be needed. This is a fully functioning Arduino in a different form factor to allow pH sensing as well! To make usage even easier the board has been updated to use Dangerous Prototypes sick of biege 50mmx50mm form factor, and all pins are labeled according to the Arduino equivalents. Overall version 2 should be a great improvement and will become familiar as the form factor is passed on to LeoEc and LeoDO!

The analog portion consists of the fixed gain offset circuit out of the pH tutorial, by using various types of Op-Amps one can definitely improve performance but the ST tl0xx series seems to be a great bang for buck although a couple really good alternatives are the MCP662, TLC2262 or with great care the OPA series (129) produce great fronts ends but are quite costly. Another alternative is to use the instrumentation style amplifiers, but great care should be taken on the PCB layout, especially since there is a digital circuit very close. Luckily there are many great resources for analog layout (some can be found here), and they helped greatly in finding a good balance for size, noise and function. Thanks to all the App note writers you guys help us all out so much!

Usage Info

Out of the box the firmware allows for this basic usage

Usage of LeoPhi is very easy, with on board USB a fully CDC compatible bootloader (modified leonardo) all you need to do is plug it in and send some serial commands! Send an S to calibrate to ph 7 solution, F to calibrate to 4 an R to read and etc...

Simply add the LeoPhi.inf to the Arduino1.0.1 hardware/drivers folder. Thanks to Openmoko we have out own PID using the VID and have created a full setup around this, including a separate driver, boards def and some example code. Simply append the board def to the Arduino boards.txt and off you go! We plan on defining the pins better but for now we are using the Leonardo variant.


Since Arduino 1.0.2 A separate IDE build is needed Please see Github for current builds

There are 2 serial ports one for the USB and one is the hardware serial port(Tx,Rx), this makes it useful are a USB to serial converter as well, but mean that the proper port must be select for use in any project. For example a Raspberry pie could interface LeoPhi over USB serial while the same data is dumped over the Hardware Serial to another Arduino. An I2C slave example code is also implemented which uses some of the same commands and shows how to easily implement a pH sensor with an I2C interface!

Some of the commands are:

  • C - Continous Read Mode: Dump readings and data every second
  • R - Single pH reading: response "pH: XX.XX" where XX.XX is the pH
  • E - Exit continous read mode
  • S - set pH7 Calibration point
  • F - set pH4 Calibration point: also relalcs probe slope and saves settings to EEPROM
  • T - set pH10 Calibration point: also recalcs probe slope and saves settings to EEPROM
  • X - restore settings to default and idela probe conditions

Standalone pH meter

Just with a few parts and an enclosure one can make a fully operation pH meter

One of the cool things that can be done with LeoPhi is building a standalone sensor whether its a handheld unit or a controller which can also maitain pH levels.

Below is a bit of code which shows how to implement the setup in the project picture (temp sensor, humidity sensor, character display and LeoPhi) Its quite simple to create although implementing the PID to control pH is a bit out of scope for this example.

 This is the base example sketch for using the LeoPhi. It can output via USB serial and Serial1(hardware USART)
 just use which one you need. Also there is an I2C slave enabled version of this for an example how to use via
 I2C. We will be using timer1 for an ISR to take a fixed frequency sample (to oversample) adding
 16 consecutive samples together. We will then decimate this number back down to a 12bit number.
 Since our pH readings will change relatively slowly we can use this method to gain the added resolution.
 There are a number of ways to calculate pH based on the E(electromagnetic potentional) reading from our circuit.
 We will use a simple method, but recommend using temperature compensation maths for a much better result.
 A rolling average method can also be applied to filter the reading before display. Dont forget there are many pins
 broken out on the headers(I2C,Serial1,PWM,AnanlogIn). Both the green and blue leds are on PWM(though the pins sink current)
 and Red is digital. By default the green led will fade in and out to indicate on and working status

 Usage is simple as passing in commands to read pH, set calibration points, read with temp, etc...
 Feel free to adjust per usage, and please share so others can learn from any additions too!!
 LeoPhi can operate from (VCC) 2.65 to 5 so remember to set your reference voltage in math to this VCC.
 Sparky's Widgets 2012

#include <LiquidCrystal.h>
LiquidCrystal lcd(13, 5, 2, 3, 1, 0);
#include <avr/eeprom.h> //we'll use this to write out data structure to eeprom
//Our pin definitions
int PHIN = A0;
int HIN = A9;
int TIN = A10;
int GREENLED = 6; //on PWM line :note all these led pins sink not source
int BLUELED = 11; //on PWM line
int REDLED = 12; //normal digital

//LED fade effects
int brightness = 0;
int fadeAmount = 5;

//EEPROM trigger check
#define Write_Check 0x1234
#define VERSION 0x0001

//Oversampling Globals
#define BUFFER_SIZE 16 // For 12 Bit ADC data
volatile uint32_t result[BUFFER_SIZE];
volatile int i = 0;
volatile uint32_t sum=0;

//Rolling average this should act as a digital filter (IE smoothing)
const int numPasses = 20; //Larger number = slower response but greater smoothin effect

int passes[numPasses]; //Our storage array
int passIndex = 0; //what pass are we on? this one
long total = 0; //running total
int pHSmooth = 0; //Our smoothed pHRaw number
//pH calc globals
int pHRaw,tRaw;
float temp, miliVolts, pH,Temp,Hum; //using floats will transmit as 4 bytes over I2C

//Continous reading flag
bool continousFlag,statusGFlag;

//Our parameter, for ease of use and eeprom access lets use a struct
struct parameters_T
 unsigned int WriteCheck;
 int pH7Cal, pH4Cal,pH10Cal;
 bool continous,statusLEDG;
 float pHStep;

void setup()
 setupADC(0,100); //Setup our ISR sampling routine analog pin 0 100hz
 //Serial1.begin(57600); //Enable basic sesrial commands in base version
 eeprom_read_block(¶ms, (void *)0, sizeof(params));
 continousFlag = params.continous;
 statusGFlag = params.statusLEDG;
 if (params.WriteCheck != Write_Check){
 // initialize smoothing variables to 0:
 for (int thisPass = 0; thisPass < numPasses; thisPass++)
 passes[thisPass] = 0;


void loop()
 //Our smoothing portion
 //subtract the last pass
 total = total - passes[passIndex];
 //grab our pHRaw this should pretty much always be updated due to our Oversample ISR
 //and place it in our passes array this mimics an analogRead on a pin
 passes[passIndex] = pHRaw;
 total = total + passes[passIndex];
 passIndex = passIndex + 1;
 //Now handle end of array and make our rolling average portion
 if(passIndex >= numPasses)
 passIndex = 0;
 pHSmooth = total/numPasses;
 analogWrite(GREENLED, brightness);
 // change the brightness for next time through the loop:
 brightness = brightness + fadeAmount;
 // reverse the direction of the fading at the ends of the fade:
 if (brightness == 0 || brightness == 255) {
 fadeAmount = -fadeAmount ;
 if(Serial.available() )
 String msgIN = "";
 char c;
 c = Serial.read();
 msgIN += c;
tRaw = LM335ATempConvert(analogRead(TIN),'F');
// The max voltage value drops down 0.00372549 for each degree F over 32F. The voltage at 32F is 3.27 (corrected for zero precent voltage)
float max_voltage = (3.27-(0.00372549*tRaw));
Hum = (((((float)analogRead(HIN)/1023)*5)-.8)/max_voltage)*100;
lcd.print(" ");
lcd.print(" ");
 Serial.print("pHRaw: ");
 Serial.print(" | ");
 Serial.print("pH10bit: ");
 Serial.print(" | ");
 Serial.print("Milivolts: ");
 Serial.print(" | ");
 Serial.print("pH: ");
 Serial.print("Hum: ");

void calcpH()
 miliVolts = (((float)pHSmooth/4096)*5)*1000;
 temp = ((((5*(float)params.pH7Cal)/4096)*1000)- miliVolts)/5.25; //5.25 is the gain of our amp stage we need to remove it
 pH = 7-(temp/params.pHStep);

void processMessage(String msg)
 if (msg.substring(2,1) == "0")
 //Status led visual indication of a working unit on powerup 0 means off
 statusGFlag = false;
 digitalWrite(GREENLED, HIGH);
 Serial.println("Status led off");
 params.statusLEDG = statusGFlag;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 if (msg.substring(2,1) == "1")
 //Status led visual indication of a working unit on powerup 0 means off
 statusGFlag = true;
 Serial.println("Status led on");
 params.statusLEDG = statusGFlag;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));

 //take a pH reading
 continousFlag = true;
 Serial.println("Continous Reading On");
 params.continous = continousFlag;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 //exit continous reading mode
 continousFlag = false;
 Serial.println("Continous Reading Off");
 params.continous = continousFlag;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 //Calibrate to pH7 solution, center on this for zero
 Serial.println("Calibrate 7");
 params.pH7Cal = pHSmooth;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 //calibrate to pH4 solution, recalculate our slope to account for probe
 Serial.println("Calibrate 4");
 params.pH4Cal = pHSmooth;
 //RefVoltage * our deltaRawpH / 12bit steps *mV in V / OP-Amp gain /pH step difference 7-4
 params.pHStep = ((((5*(float)(params.pH7Cal - params.pH4Cal))/4096)*1000)/5.25)/3;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 //calibrate to pH10 solution, recalculate our slope to account for probe
 Serial.println("Calibrate 10");
 params.pH10Cal = pHSmooth;
 //RefVoltage * our deltaRawpH / 12bit steps *mV in V / OP-Amp gain /pH step difference 10-7
 params.pHStep = ((((5*(float)(params.pH10Cal - params.pH7Cal))/4096)*1000)/5.25)/3;
 eeprom_write_block(¶ms, (void *)0, sizeof(params));
 //Lets read in our parameters and spit out the info!
 eeprom_read_block(¶ms, (void *)0, sizeof(params));
 Serial.print("LeoPhi Info: Firmware Ver ");
 Serial.print("pH 7 cal: ");
 Serial.print(" | ");
 Serial.print("pH 4 cal: ");
 Serial.print(" | ");
 Serial.print("pH probe slope: ");
 //restore to default settings
 Serial.println("Reseting to default settings");

void reset_Params(void)
 //Restore to default set of parameters!
 params.WriteCheck = Write_Check;
 params.statusLEDG = true;
 params.continous = false; //turn off continous readings
 params.pH7Cal = 2048; //assume ideal probe and amp conditions 1/2 of 4096
 params.pH4Cal = 1286; //using ideal probe slope we end up this many 12bit units away on the 4 scale
 params.pH10Cal = 2810;//using ideal probe slope we end up this many 12bit units away on the 10 scale
 params.pHStep = 59.16;//ideal probe slope
 eeprom_write_block(¶ms, (void *)0, sizeof(params)); //write these settings back to eeprom

//LM335a temp conversion routine this just makes stuff a lot easier
int LM335ATempConvert(int tempIn, char unitSystem)
 int KelvinC=273;
 int KelvinTemp = (long(tempIn) * 5 * 100) / 1023; // convert
 int CelsiusTemp = KelvinTemp-KelvinC;
 int FahrenheitTemp = (CelsiusTemp)*(9/5)+32;
 int tempOut;

 case 'K':
 tempOut = KelvinTemp;
 case 'C':
 tempOut = CelsiusTemp;
 case 'F':
 tempOut = FahrenheitTemp;
 return tempOut;

//Our oversampling read functions we will access the hardware directly setting up a counter and a read frequency
//based on the default ADC clock of 250khz, this is all under an Interrupt Service Routine this means we need to keep
//everything contained within as fast as possible especially if we intend on using I2C (clock dragging)
void setupADC(uint8_t channel, int frequency)
 ADMUX = channel | _BV(REFS0);
 ADCSRB |= _BV(ADTS2) | _BV(ADTS0); //Compare Match B on counter 1
 TCCR1A = _BV(COM1B1);
 TCCR1B = _BV(CS11)| _BV(CS10) | _BV(WGM12);
 uint32_t clock = 250000;
 uint16_t counts = clock/(BUFFER_SIZE*frequency);
 OCR1B = counts;


 result[i] = ADC;
 for(int j=0;j<BUFFER_SIZE;j++)
 sum = sum>>2;
 //We will simply set a variable here and perform a rolling average on the pH.
 pHRaw = sum;

Creative Commons License

LeoPhi pH interface by Sparky's Widgets is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.Based on a work at http://www.sparkyswidgets.com/portfolio-item/leophi-usb-enable-ph-interface/.