Hello! Check out this PID Thermostat code I have working at the moment with an analog input on an arduino microcontroller and a LCD to give some temp readings and display the 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 few readings from some buttons and switches . (That change the setpoint and turn the controller on/off)
-a output signal, from the arduino, 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:
#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();
}
}
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 infrared 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 input portion of 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:
#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
}
from the sensor guide: http://bildr.org/2011/02/mlx90614-arduino/
One of the first differences I notice is the command indication 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:
/**
* 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. When triggered, IDEALLY start the output at its max and let the PID adjust it from there.. This STILL needs to be added to the code.
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;
}
Any ideas? Thanks in advance!