Refactor PID code to work with serial I2C instead of analog.

Hello! Check out this PID Thermostat code I have working at the moment with an analog input on an seeeduino microcontroller and a LCD to give some info on current temp and setpoint.

The components the current code controls/reads are :
-a 2x16 LCD
-a temperature reading via voltage on the analog input (0), usually fed from a thermocouple sensor.
-a couple signals from two pushbuttons and a switch. (That change the setpoint and turn the PID controller on/off)
-a output signal, from the seeeduino, to a mechanical or solid state relay that controls whatever heating or cooling device you’re playing with.

So here’s my present code with the analog input:

[code]#include <EEPROM.h>
#include <LiquidCrystal.h>
#include <PID_v1.h>
#include <SPI.h>

double Setpoint, Input, Output;
double aggKp=2, aggKi=1, aggKd=0, LoLIM=15, HiLIM=255;
double consKp=1, consKi=4, consKd=3;
double SampleTime=250;
LiquidCrystal lcd(12, 11, 8, 4, 3, 2);

const int onPin = 5; //pin to trigger heater/cooler
const int upPin = 6; // pin to increase temp
const int downPin = 7; //pin to decrease temp
int buttonState = 0; // variable for reading the pin status
int DownPushed = 0;
int UpPushed = 0;
int OnPushed = 0;
int PID_ON = 0;
int UpdScr = 0;
int inpt;
int gap;
int S_gap;
int count;
int EEPROM_ADDR;

void UpdateEEPROM() {

int val, wr;
val = (int) Setpoint;
wr = val>>8;
EEPROM.write(EEPROM_ADDR, wr);
wr = val & 0XFF;
EEPROM.write(EEPROM_ADDR+1, wr);

};

void LoadEEPROM() {
int val;
val = EEPROM.read(EEPROM_ADDR);
val = val <<8;
val +=EEPROM.read(EEPROM_ADDR+1);
Setpoint = val;

};
unsigned long lastTime, lShowTime;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, aggKp, aggKi, aggKd, DIRECT);

void setup()
{
Serial.begin( 19200 );
//initialize the variables we’re linked to
Input = analogRead(0);
LoadEEPROM();
if( (Setpoint +1.) < 0.001)
Setpoint = 199;
// Setpoint = xxx;

// Declare inputs
pinMode(onPin, INPUT); // declare pushbutton as input
pinMode(upPin, INPUT); // declare pushbutton as input
pinMode(downPin, INPUT); // declare pushbutton as input

//turn the PID on STANDBY
myPID.SetOutputLimits(LoLIM, HiLIM);
myPID.SetMode(AUTOMATIC);
myPID.SetSampleTime(SampleTime);
myPID.SetTunings(aggKp, aggKi, aggKd);

lShowTime = lastTime = millis();

// set up the LCD’s number of columns and rows:
lcd.begin(16,2);

count = 0;
EEPROM_ADDR = 30;

//lcd.clear();
lcd.print(“tstat”);
lcd.setCursor( 0,1);
lcd.print(aggKp);
lcd.print("-");
lcd.print((int)(aggKi) );
lcd.print("-");
lcd.print(aggKd);
delay(2000);
lcd.clear();
lcd.print(“S_Rate = “);
lcd.print((int)(SampleTime));
lcd.print(” ms”);
delay(1000);
lcd.clear();
lcd.print(“Limits “);
lcd.print((int)(LoLIM));
lcd.print(”,”);
lcd.print((int)(HiLIM));
delay(1000);
lcd.clear();
}

void loop()
{
Input = analogRead(0);

buttonState = digitalRead(onPin);

if (buttonState == HIGH) {
// turn HEATER on, but prime first:
myPID.SetMode(AUTOMATIC);
PID_ON =1;
}
else {
myPID.SetMode(MANUAL);
Output = 0;
PID_ON =0;
}

myPID.Compute();
analogWrite(10,Output);
// I would not change these lines, because you are expecting 250 ms for a “push”
// that is if you hold button for more then 1/4 second,
if(digitalRead(upPin)==HIGH) {
if (millis()-lastTime >= 250) {
Setpoint+=5;
UpdateEEPROM();
lastTime=millis();
UpdScr = 1;
}
}

if(digitalRead(downPin)==HIGH) {
if (millis()-lastTime >= 250) {
Setpoint-=5;
UpdateEEPROM();
lastTime=millis();
UpdScr = 1;
}
}

//and output to LCD

if( (millis() - lShowTime > 100 ) || UpdScr ) {
UpdScr = 0;
lcd.setCursor(0,0);
//if heater is on - show *
//if not - empty
if( PID_ON ==1 ) {
lcd.print("*");
}
else {
lcd.print(" ");
}
lcd.print("SET.TEMP-> “);
lcd.print((int)(Setpoint) );
lcd.setCursor( 0,1);
lcd.print(” AIR.TEMP-> ");
inpt = (inpt *5 + Input)/6;
count = (count +1)%6;

if(count == 0) {
  if( inpt < 100) lcd.print( " ");
  lcd.print( inpt );
}


lShowTime = millis();  

}

}

[/code]

The code works fantastic with everything right now. However, what I am trying to do now is make the PID code able to work with serial data that this new temp sensor that I have outputs, instead of using the old analog temp signal that was fed into the analog(0) input. A friend suggested I refactor the code… “restructuring the existing body of code, altering its internal structure without changing its external behavior”.

He said, “create and use a function that returns the input reading” kind of like so:

static double GetInput( void )
{
return( analogRead( 0 ) );
}

and also said," Replace this (appears twice in your sketch)…"
Code:
Input = analogRead(0);
…with this…
Code:
Input = GetInput();

So… I guess I need to refactor my old PID code to dance with the new sensor that uses the “serial I2C communication” (aka Two Wire Interface).

Here’s a sample of a code the new sensor is supposed to work with:

[code]#include <i2cmaster.h>

void setup(){
Serial.begin(9600);
Serial.println(“Setup…”);

i2c_init(); //Initialise the i2c bus
PORTC = (1 << PORTC4) | (1 << PORTC5);//enable pullups
}

void loop(){
int dev = 0x5A<<1;
int data_low = 0;
int data_high = 0;
int pec = 0;

i2c_start_wait(dev+I2C_WRITE);
i2c_write(0x07);

// read
i2c_rep_start(dev+I2C_READ);
data_low = i2c_readAck(); //Read 1 byte and then send ack
data_high = i2c_readAck(); //Read 1 byte and then send ack
pec = i2c_readNak();
i2c_stop();

//This converts high and low bytes together and processes temperature, MSB is a error bit and is ignored for temps
double tempFactor = 0.02; // 0.02 degrees per LSB (measurement resolution of the MLX90614)
double tempData = 0x0000; // zero out the data
int frac; // data past the decimal point

// This masks off the error bit of the high byte, then moves it left 8 bits and adds the low byte.
tempData = (double)(((data_high & 0x007F) << 8) + data_low);
tempData = (tempData * tempFactor)-0.01;

float celcius = tempData - 273.15;
float fahrenheit = (celcius*1.8) + 32;

Serial.print("Celcius: ");
Serial.println(celcius);

Serial.print("Fahrenheit: ");
Serial.println(fahrenheit);

delay(1000); // wait a second before printing again

}[/code]

from the sensor guide: bildr.org/2011/02/mlx90614-arduino/

One of the first differences I notice is the line stating the serial communication speed :
My code —> “Serial.begin( 19200 );”
Sensor Example—> “Serial.begin( 9600 );”

Here’s the modification that was suggested but I can’t seem to get it to work:

[code]/**

  • Infrared Thermometer MLX90614
  • by Jaime Patarroyo
  • based on ‘Is it hot? Arduino + MLX90614 IR Thermometer’ by bildr.blog
  • Returns the temperature in Celcius and Fahrenheit from a MLX90614
  • Infrared Thermometer, connected to the TWI/I²C pins.
    */

#include <i2cmaster.h>
#include <EEPROM.h>
#include <LiquidCrystal.h>
#include <PID_v1.h>
#include <SPI.h>

double Setpoint, Input, Output;
double aggKp=1, aggKi=3, aggKd=0;
double consKp=0.5, consKi=5, consKd=3;
double LoLIM=5, HiLIM=255;
double SampleTime=250;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, aggKp, aggKi, aggKd, DIRECT);

LiquidCrystal lcd(12, 11, 8, 3, 2, 1);

const int onPin = 9; //pin to trigger heater
const int upPin = 6; // pin to increase temp
const int downPin = 7; //pin to decrease temp
int buttonState = 0; // variable for reading the pin status
int DownPushed = 0;
int UpPushed = 0;
int OnPushed = 0;
int PID_ON = 0;
int UpdScr = 0;
int inpt;
int gap;
int S_gap;
int count;
int EEPROM_ADDR;

void UpdateEEPROM() {

int val, wr;
val = (int) Setpoint;
wr = val>>8;
EEPROM.write(EEPROM_ADDR, wr);
wr = val & 0XFF;
EEPROM.write(EEPROM_ADDR+1, wr);

};

void LoadEEPROM() {
int val;
val = EEPROM.read(EEPROM_ADDR);
val = val << 8;
val |=EEPROM.read(EEPROM_ADDR+1);
Setpoint = val;

};
unsigned long lastTime, lShowTime;

int deviceAddress = 0x5A<<1; // From MLX906114 datasheet’s, 0x5A is
// the default address for I²C communication.
// Shift the address 1 bit right, the
// I²Cmaster library only needs the 7 most
// significant bits for the address.

void setup() {
Serial.begin(19200); // Start serial communication at 19200bps.

i2c_init(); // Initialise the i2c bus.
PORTC = (1 << PORTC4) | (1 << PORTC5); // Enable pullups.

Input = GetInput();
LoadEEPROM();
if( (Setpoint +1.) < 0.001)
Setpoint = 199;
// Setpoint = xxx;

// Declare inputs
pinMode(onPin, INPUT); // declare pushbutton as input
pinMode(upPin, INPUT); // declare pushbutton as input
pinMode(downPin, INPUT); // declare pushbutton as input

//turn the PID on STANDBY.
myPID.SetOutputLimits(0, 255);
myPID.SetMode(AUTOMATIC);
myPID.SetSampleTime(SampleTime);
myPID.SetTunings(aggKp, aggKi, aggKd);

lShowTime = lastTime = millis();

// set up the LCD’s number of columns and rows:
lcd.begin(16,2);

count = 0;
EEPROM_ADDR = 30;

lcd.print(“Settings”);
lcd.setCursor( 0,1);
lcd.print(aggKp);
lcd.print("-");
lcd.print((int)(aggKi) );
lcd.print("-");
lcd.print(aggKd);
delay(3000);
lcd.clear();
lcd.print(“S_Rate = “);
lcd.print((int)(SampleTime));
lcd.print(” ms”);
delay(2000);
lcd.clear();
}

void loop() {

/* Read’s data from MLX90614 with the given address, transform’s it into
temperature in Celcius and store’s it in the Celcius variable. */
Input = GetInput( deviceAddress );

buttonState = digitalRead(onPin);

if (buttonState == HIGH) {
myPID.SetMode(AUTOMATIC); //Turn on heat. Adjust output acordinglly via myPIDs computations.
PID_ON =1;
}
else {
myPID.SetMode(MANUAL); // Turn off heat.
Output = 0;
PID_ON =0;
}

myPID.Compute();
analogWrite(10,Output);

// Setpoint control section.
// I would not change these lines, because you are expecting 250 ms for a “push”
// that is if you hold button for more then 1/4 second,
if(digitalRead(upPin)==HIGH) {
if (millis()-lastTime >= 250) {
Setpoint+=5;
UpdateEEPROM();
lastTime=millis();
UpdScr = 1;
}
}

if(digitalRead(downPin)==HIGH) {
if (millis()-lastTime >= 250) {
Setpoint-=5;
UpdateEEPROM();
lastTime=millis();
UpdScr = 1;
}
}

//and output to LCD

if( (millis() - lShowTime > 100 ) || UpdScr ) {
UpdScr = 0;
lcd.setCursor(0,0);
//if heater is on - show *
//if not - empty
if( PID_ON ==1 ) {
lcd.print("*");
}
else {
lcd.print(" ");
}
lcd.print("SET.TEMP-> ");
lcd.print((int)(Setpoint) );
lcd.setCursor( 0,1);
lcd.print("SENS.TEMP-> ");
inpt = (inpt *5 + Input)/6;
count = (count +1)%6;

if(count == 0) {
  if( inpt < 100) lcd.print( " ");
  lcd.print( inpt );
}


lShowTime = millis();  

}

}

static float GetInput(int address) {
int dev = address;
int data_low = 0;
int data_high = 0;
int pec = 0;

// Write
i2c_start_wait(dev+I2C_WRITE);
i2c_write(0x07);

// Read
i2c_rep_start(dev+I2C_READ);
data_low = i2c_readAck(); // Read 1 byte and then send ack.
data_high = i2c_readAck(); // Read 1 byte and then send ack.
pec = i2c_readNak();
i2c_stop();

// This converts high and low bytes together and processes temperature,
// MSB is a error bit and is ignored for temps.
double tempFactor = 0.02; // 0.02 degrees per LSB (measurement
// resolution of the MLX90614).
double tempData = 0x0000; // Zero out the data
int frac; // Data past the decimal point

// This masks off the error bit of the high byte, then moves it left
// 8 bits and adds the low byte.
tempData = (double)(((data_high & 0x007F) << 8) + data_low);
tempData = (tempData * tempFactor)-0.01;
float celcius = tempData - 273.15;

// Returns temperature un Celcius.
return celcius;
}[/code]

Any ideas? Thanks in advance!