//PID and PID Autotune Library Initialization
#include <PID_v1.h>
#include <PID_AutoTune_v0.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"
#include <LiquidCrystal.h>
//
#define MAXDO 6
#define MAXCS 7
#define MAXCLK 8
#define relayPin 9
int referenceLow = 769; //for calibration I used the curie point and salt. Temperature is in Celsius.
int referenceHigh = 801;
int tempLow = 769;
int tempHigh = 820;
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
byte ATuneModeRemember=2;
double Input, Output, Setpoint=650; //KP Set initial Setpoint
double kp=10,ki=0.4,kd=0.1; //KP Set intial PID parameters, if you have any.
double kpmodel=1.5, taup=100, theta[50];
double OutputStart=5;
double aTuneStep=750, aTuneNoise=1, aTuneStartValue=750; //KP Set aTuneStep to ensure noticeable heating and cooling from aTuneStartValue
unsigned int aTuneLookBack=200; //KP How far back to look back to find maxima and minima.
// For Slow processes this will be large, for fast processes, this will be smaller.
unsigned long modelTime, serialTime;
PID myPID(&Input, &Output, &Setpoint,kp,ki,kd, DIRECT);
PID_ATune aTune(&Input, &Output);
//set to false to connect to the real world
boolean useSimulation = false; //KP Keep false unless simulating
//Tuning?
boolean tuning = false; //KP Keep False
int WindowSize = 1000;
unsigned long windowStartTime;
void setup()
{
windowStartTime = millis();
pinMode(relayPin, OUTPUT); //KP Set relayPin as Output
pinMode(MAXCS, OUTPUT);
pinMode(MAXCLK, OUTPUT);
pinMode(12, OUTPUT);
pinMode(11, OUTPUT);
pinMode(5, OUTPUT);
pinMode(4, OUTPUT);
pinMode(3, OUTPUT);
pinMode(2, OUTPUT);
pinMode(relayPin, OUTPUT);
lcd.begin(16, 2);
if(useSimulation)
{
for(byte i=0;i<50;i++)
{
theta[i]=OutputStart;
}
modelTime = 0;
}
//tell the PID to range between 0 and the full window size
myPID.SetOutputLimits(0, WindowSize);
//Setup the pid
myPID.SetMode(AUTOMATIC);
if(tuning)
{
tuning=false;
changeAutoTune();
tuning=true;
}
aTune.SetControlType(0); //KP Set 0 for PI control and 1 for PID control autotuning parameters
serialTime = 0;
Serial.begin(9600);
}
void loop()
{
int i = 0; // Counter for arrays
double internalTemp = thermocouple.readInternal(); // Read the internal temperature of the MAX31855.
double rawTemp = thermocouple.readCelsius(); // Read the temperature of the thermocouple. This temp is compensated for cold junction temperature.
double thermocoupleVoltage= 0;
double internalVoltage = 0;
double correctedTemp = 0;
// Check to make sure thermocouple is working correctly.
if (isnan(rawTemp)) {
correctedTemp = 0;
}
else {
// Steps 1 & 2. Subtract cold junction temperature from the raw thermocouple temperature.
thermocoupleVoltage = (rawTemp - internalTemp)*0.041276; // C * mv/C = mV
// Step 3. Calculate the cold junction equivalent thermocouple voltage.
if (internalTemp >= 0) { // For positive temperatures use appropriate NIST coefficients
// Coefficients and equations available from http://srdata.nist.gov/its90/download/type_k.tab
double c[] = {-0.176004136860E-01, 0.389212049750E-01, 0.185587700320E-04, -0.994575928740E-07, 0.318409457190E-09, -0.560728448890E-12, 0.560750590590E-15, -0.320207200030E-18, 0.971511471520E-22, -0.121047212750E-25};
// Count the the number of coefficients. There are 10 coefficients for positive temperatures (plus three exponential coefficients),
// but there are 11 coefficients for negative temperatures.
int cLength = sizeof(c) / sizeof(c[0]);
// Exponential coefficients. Only used for positive temperatures.
double a0 = 0.118597600000E+00;
double a1 = -0.118343200000E-03;
double a2 = 0.126968600000E+03;
// From NIST: E = sum(i=0 to n) c_i t^i + a0 exp(a1 (t - a2)^2), where E is the thermocouple voltage in mV and t is the temperature in degrees C.
// In this case, E is the cold junction equivalent thermocouple voltage.
// Alternative form: C0 + C1*internalTemp + C2*internalTemp^2 + C3*internalTemp^3 + ... + C10*internaltemp^10 + A0*e^(A1*(internalTemp - A2)^2)
// This loop sums up the c_i t^i components.
for (i = 0; i < cLength; i++) {
internalVoltage += c[i] * pow(internalTemp, i);
}
// This section adds the a0 exp(a1 (t - a2)^2) components.
internalVoltage += a0 * exp(a1 * pow((internalTemp - a2), 2));
}
else if (internalTemp < 0) { // for negative temperatures
double c[] = {0.000000000000E+00, 0.394501280250E-01, 0.236223735980E-04 - 0.328589067840E-06, -0.499048287770E-08, -0.675090591730E-10, -0.574103274280E-12, -0.310888728940E-14, -0.104516093650E-16, -0.198892668780E-19, -0.163226974860E-22};
// Count the number of coefficients.
int cLength = sizeof(c) / sizeof(c[0]);
// Below 0 degrees Celsius, the NIST formula is simpler and has no exponential components: E = sum(i=0 to n) c_i t^i
for (i = 0; i < cLength; i++) {
internalVoltage += c[i] * pow(internalTemp, i) ;
}
}
// Step 4. Add the cold junction equivalent thermocouple voltage calculated in step 3 to the thermocouple voltage calculated in step 2.
double totalVoltage = thermocoupleVoltage + internalVoltage;
// Step 5. Use the result of step 4 and the NIST voltage-to-temperature (inverse) coefficients to calculate the cold junction compensated, linearized temperature value.
// The equation is in the form correctedTemp = d_0 + d_1*E + d_2*E^2 + ... + d_n*E^n, where E is the totalVoltage in mV and correctedTemp is in degrees C.
// NIST uses different coefficients for different temperature subranges: (-200 to 0C), (0 to 500C) and (500 to 1372C).
if (totalVoltage < 0) { // Temperature is between -200 and 0C.
double d[] = {0.0000000E+00, 2.5173462E+01, -1.1662878E+00, -1.0833638E+00, -8.9773540E-01, -3.7342377E-01, -8.6632643E-02, -1.0450598E-02, -5.1920577E-04, 0.0000000E+00};
int dLength = sizeof(d) / sizeof(d[0]);
for (i = 0; i < dLength; i++) {
correctedTemp += d[i] * pow(totalVoltage, i);
}
}
else if (totalVoltage < 20.644) { // Temperature is between 0C and 500C.
double d[] = {0.000000E+00, 2.508355E+01, 7.860106E-02, -2.503131E-01, 8.315270E-02, -1.228034E-02, 9.804036E-04, -4.413030E-05, 1.057734E-06, -1.052755E-08};
int dLength = sizeof(d) / sizeof(d[0]);
for (i = 0; i < dLength; i++) {
correctedTemp += d[i] * pow(totalVoltage, i);
}
}
else if (totalVoltage < 54.886 ) { // Temperature is between 500C and 1372C.
double d[] = {-1.318058E+02, 4.830222E+01, -1.646031E+00, 5.464731E-02, -9.650715E-04, 8.802193E-06, -3.110810E-08, 0.000000E+00, 0.000000E+00, 0.000000E+00};
int dLength = sizeof(d) / sizeof(d[0]);
for (i = 0; i < dLength; i++) {
correctedTemp += d[i] * pow(totalVoltage, i);
}
} else { // NIST only has data for K-type thermocouples from -200C to +1372C. If the temperature is not in that range, set temp to impossible value.
// Error handling should be improved.
correctedTemp = NAN;
}
}
//pid-related code
int c = correctedTemp;
if (c==0){ //Somehow my TC gets grounded at higher Temperature. In this case the Input gets 0°C as Value and affects the PID performance. So I simply filter those 0°C Values with this If line.
} else {
Input = (((c-tempLow)*(referenceHigh-referenceLow))/(tempHigh-tempLow)) + referenceLow; //Despite the linearization, the displayed temperature seems to be off. While at the Curipoint everything seems to be right, the Salt melts at displayed 820°C (should at 801°C).
}
unsigned long now = millis();
if(tuning)
{
byte val = (aTune.Runtime());
if (val!=0)
{
tuning = false;
}
if(!tuning)
{ //we're done, set the tuning parameters
kp = aTune.GetKp();
ki = aTune.GetKi();
kd = aTune.GetKd();
myPID.SetTunings(kp,ki,kd);
AutoTuneHelper(false);
}
}
else myPID.Compute();
if(useSimulation)
{
theta[30]=Output;
if(now>=modelTime)
{
modelTime +=100;
DoModel();
}
}
else //KP This is optimized for Relay output.
{
if(now - windowStartTime>WindowSize)
{ //time to shift the Relay Window
windowStartTime += WindowSize;
}
if(Output > now - windowStartTime) digitalWrite(relayPin,HIGH);
else digitalWrite(relayPin,LOW);
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SollTemp: ");
lcd.print(Setpoint);
lcd.setCursor(0, 1);
lcd.print("IstTemp: ");
lcd.print(c);
//send-receive with processing if it's time
if(millis()>serialTime)
{
SerialReceive();
SerialSend();
serialTime+=500;
}
}
/********************************************
* Serial Communication functions / helpers
********************************************/
union { // This Data structure lets
byte asBytes[24]; // us take the byte array
float asFloat[6]; // sent from processing and
} // easily convert it to a
foo; // float array
// getting float values from processing into the arduino
// was no small task. the way this program does it is
// as follows:
// * a float takes up 4 bytes. in processing, convert
// the array of floats we want to send, into an array
// of bytes.
// * send the bytes to the arduino
// * use a data structure known as a union to convert
// the array of bytes back into an array of floats
// the bytes coming from the arduino follow the following
// format:
// 0: 0=Manual, 1=Auto, else = ? error ?
// 1: 0=Tuning OFF, 1=Tuning ON, else = ? error ?
// 2-5: float setpoint
// 6-9: float input
// 10-13: float output
// 14-17: float P_Param
// 18-21: float I_Param
// 22-24: float D_Param
void changeAutoTune()
{
if(!tuning)
{
//Set the Output to the desired starting frequency.
aTuneStartValue = Output; //KP Initial aTuneStartValue will be = Output at Toggle
aTune.SetNoiseBand(aTuneNoise);
aTune.SetOutputStep(aTuneStep);
aTune.SetLookbackSec((int)aTuneLookBack);
AutoTuneHelper(true);
tuning = true;
}
else
{ //cancel autotune
aTune.Cancel();
tuning = false;
AutoTuneHelper(false);
}
}
void AutoTuneHelper(boolean start)
{
if(start)
ATuneModeRemember = myPID.GetMode();
else
myPID.SetMode(ATuneModeRemember);
}
void SerialSend()
{
Serial.print("PID ");
Serial.print(Setpoint);
Serial.print(" ");
Serial.print(Input);
Serial.print(" ");
Serial.print(Output);
Serial.print(" ");
Serial.print(myPID.GetKp());
Serial.print(" ");
Serial.print(myPID.GetKi());
Serial.print(" ");
Serial.print(myPID.GetKd());
Serial.print(" ");
if(myPID.GetMode()==AUTOMATIC) Serial.print("Automatic");
else Serial.print("Manual");
Serial.print(" ");
// if(myPID.GetDirection()==DIRECT) Serial.print("Direct");
//else Serial.print("Reverse");
// Serial.print(" ");
if(tuning==false) Serial.println("Off"); //KP Added the On/Off for Tuning Toggle
else Serial.println("On");
}
void SerialReceive()
{
// read the bytes sent from Processing
int index=0;
byte Auto_Man = -1;
// byte Direct_Reverse = -1;
byte Tuning_Mode = -1; //KP Tuning Mode?
while(Serial.available()&&index<26)
{
if(index==0) Auto_Man = Serial.read();
else if(index==1) Tuning_Mode = Serial.read(); //KP Tuning Mode?
//else if(index==1) Direct_Reverse = Serial.read();
//else if(index==2) Tuning_Mode = Serial.read(); //KP Tuning Mode?
else foo.asBytes[index-2] = Serial.read();
index++;
}
// if the information we got was in the correct format,
// read it into the system
if(index==26 && (Auto_Man==0 || Auto_Man==1) && (Tuning_Mode==0 || Tuning_Mode==1))
{
Setpoint=double(foo.asFloat[0]);
//Input=double(foo.asFloat[1]); // * the user has the ability to send the
// value of "Input" in most cases (as
// in this one) this is not needed.
if(Auto_Man==0) // * only change the Output if we are in
{ // manual mode. otherwise we'll get an
Output=double(foo.asFloat[2]); // Output blip, then the controller will
} // overwrite.
double p, i, d; // * read in and set the controller tunings
p = double(foo.asFloat[3]); //
i = double(foo.asFloat[4]); //
d = double(foo.asFloat[5]); //
myPID.SetTunings(p, i, d); //
if(Auto_Man==0) myPID.SetMode(MANUAL);// * set the controller mode
else myPID.SetMode(AUTOMATIC); //
// if(Direct_Reverse==0) myPID.SetControllerDirection(DIRECT);// * set the controller Direction
// else myPID.SetControllerDirection(REVERSE); //
if(Tuning_Mode == 0) tuning=false; // Set Tuning mode on/off
else tuning=true;
}
Serial.flush(); // * clear any random data from the serial buffer
}