Temperature control using PID

Hi guys, I am using a Adafruit Thermocouple Sensor w/MAX31855K and require to control temperature at a setpoint of 30 degrees.
My code is as follows:

#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO   3
#define MAXCS   4
#define MAXCLK  5
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 2, Ki = 1, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 10000;
unsigned long windowStartTime;
void setup()
{
   Serial.begin(9600);
 
  while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

  Serial.println("MAX31855 test");
  // wait for MAX chip to stabilize
  delay(500);
  
  pinMode(RelayPin, OUTPUT);

  windowStartTime = millis();

  //initialize the variables we're linked to
  Setpoint = 30;

  //tell the PID to range between 0 and the full window size
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
     double c = thermocouple.readCelsius();
   if (isnan(c)) {
     Serial.println("Something wrong with thermocouple!");
   } else {
     Serial.print("C = "); 
     Serial.println(c);
   }
   delay(1000);
  Input = c;
  myPID.Compute();

  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/
  unsigned long now = millis();
  if (now - windowStartTime > WindowSize)
  { //time to shift the Relay Window
    windowStartTime += WindowSize;
  }
  if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
  else digitalWrite(RelayPin, LOW);
}

Despite the current temperature as measured by the sensor as 22 degrees, the output at pin 8 is not high at all. Can you please check and suggest me. Thanks a lot

What did your debug prints on the serial monitor show when you printed the 'Output' and 'windowStartTime' variables?

EDIT:
Are you sure you need a PID? A Bang-Bang controller with Hysteresis is much simpler and unconditionally stable. Think about how the thermostat in an HVAC system works.

Given this "Instrumentation and Control systems student at Murdoch University. Perth", a PID probably is required.

As suggested, put some debug prints in and you'll see what's happening. One problem is that output is very small compared to the window size, so if your code were working as intended, it would take a long time to wind up to a point where output is large enough for you to see - once it getup to 30 or so, it's only increasing by .8 a cycle, compared to a window size of 10000.

That delay(1000) is muddying the waters too. Some of the times the relay on will be missed and then when it does go on (which it does if you wait long enough) the delay means it stays on too long.

PID is a very simple (to employ) function. If you're serious about what you're doing, ditch the library and write your own, maybe 8 lines of code. You'll gain a lot more insight as to how it works. You'll understand perhaps too, how important it is to get your coefficients close to the mark at the start. You choice of {2, 1, 0} is actually not bad, but as a starting point you might find greater success with {2, 0.1, 0.1}.

You'll find soon also, how irrelevant I and D become in systems that react as slow as temperature control. There are better methods, but that's probably not part of your assignment.

Eight lines of code gets you the beginner's PID, which has some nasty behaviour. The library improves on that rather. Read about it here

wildbill:
Given this "Instrumentation and Control systems student at Murdoch University. Perth", a PID probably is required.

As suggested, put some debug prints in and you'll see what's happening. One problem is that output is very small compared to the window size, so if your code were working as intended, it would take a long time to wind up to a point where output is large enough for you to see - once it getup to 30 or so, it's only increasing by .8 a cycle, compared to a window size of 10000.

That delay(1000) is muddying the waters too. Some of the times the relay on will be missed and then when it does go on (which it does if you wait long enough) the delay means it stays on too long.

Exactly, 'PI' is a must the 'D' not as such. I have changed the windowsize to 5000 and decreased the delay to 500 . also i have put some debug points and here is what i get in the serial monitor:

C = 20.00
5510
36.72

where 5510 is windowStartTime and 36.72 is the output. Before connecting the output to the relay, i have placed an LED to see if there is any output on pin8. but its not happening.

Understood wildbill, but as a student, don't you think maybe it's better to use a simple algorithm to start and learn what it does rather than jump in with both feet and use a library that one knows nothing about and the simply tweak it 'til it works?

Many of the on-the-fly tuning used in more advanced controllers/libraries are exactly the things that someone new to control theory needs to be adjusting by hand to better gain an intuitive understanding of how it works. I don't at all disagree with you, I'm just a firm believer in sound basics being a better platform for learning.

Hi , it took a while to actually get the LED start blinking. I have observed the windowstarttime gradually increasing but the output is ON for a very short duration like a second or 2, which wont be enough to heat up the liquid . whats the way to have an increased output duration ?

/********************************************************
   PID RelayOutput Example
   Same as basic example, except that this time, the output
   is going to a digital pin which (we presume) is controlling
   a relay.  The pid is designed to output an analog value,
   but the relay can only be On/Off.

     To connect them together we use "time proportioning
   control"  Tt's essentially a really slow version of PWM.
   First we decide on a window size (5000mS say.) We then
   set the pid to adjust its output between 0 and that window
   size.  Lastly, we add some logic that translates the PID
   output into "Relay On Time" with the remainder of the
   window being "Relay Off Time"
 ********************************************************/
#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO   3
#define MAXCS   4
#define MAXCLK  5
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 2, Ki = 1, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 5000;
unsigned long windowStartTime;
void setup()
{
   Serial.begin(9600);
 
  while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

  Serial.println("MAX31855 test");
  // wait for MAX chip to stabilize
  delay(500);
  
  pinMode(RelayPin, OUTPUT);

  windowStartTime = millis();

  //initialize the variables we're linked to
  Setpoint = 30;

  //tell the PID to range between 0 and the full window size
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
     double c = thermocouple.readCelsius();
   if (isnan(c)) {
     Serial.println("Something wrong with thermocouple!");
   } else {
     Serial.print("C = "); 
     Serial.println(c);
   }
   delay(500);
  Input = c;
  myPID.Compute();

  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/
  unsigned long now = millis();
  Serial.println(windowStartTime);
  if (now - windowStartTime > WindowSize)
  { //time to shift the Relay Window
    windowStartTime += WindowSize;
  }
  Serial.println(Output);
  if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
  else digitalWrite(RelayPin, LOW);
}

thank you very much for your help sirs.

DKWatson:
Understood wildbill, but as a student, don't you think maybe it's better to use a simple algorithm to start and learn what it does rather than jump in with both feet and use a library that one knows nothing about and the simply tweak it 'til it works?

Many of the on-the-fly tuning used in more advanced controllers/libraries are exactly the things that someone new to control theory needs to be adjusting by hand to better gain an intuitive understanding of how it works. I don't at all disagree with you, I'm just a firm believer in sound basics being a better platform for learning.

Sir watson,
i am aware of PID and have worked with it a lot but in Matlab and Simulink, I would have loved to develop my own algorithm but have other things in the project lined up that's why relying on a library which being on arduino official website is hopefully reliable. thank you for the help anyways

I'd be inclined to treat the output as a percentage and therefore constrain it to the 0-100 range. Then use that to determine what proportion of the window the heating should be on.

Gotcha. I'll leave it in wildbill's capable hands.

I'm still not able to get the tuning appropriate. The way it is switching on and off would damage the heater. can you please help me understand how its actually working with pwm. my code is updated as below and the switching on and off is a bit faster for the heating element.

/********************************************************
   PID RelayOutput Example
   Same as basic example, except that this time, the output
   is going to a digital pin which (we presume) is controlling
   a relay.  The pid is designed to output an analog value,
   but the relay can only be On/Off.

     To connect them together we use "time proportioning
   control"  Tt's essentially a really slow version of PWM.
   First we decide on a window size (5000mS say.) We then
   set the pid to adjust its output between 0 and that window
   size.  Lastly, we add some logic that translates the PID
   output into "Relay On Time" with the remainder of the
   window being "Relay Off Time"
 ********************************************************/
#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO   3
#define MAXCS   4
#define MAXCLK  5
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 5, Ki = 2, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 1000;
unsigned long windowStartTime;
void setup()
{
   Serial.begin(9600);
 
  while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

  Serial.println("MAX31855 test");
  // wait for MAX chip to stabilize
  delay(500);
  
  pinMode(RelayPin, OUTPUT);

  windowStartTime = millis();

  //initialize the variables we're linked to
  Setpoint = 30;

  //tell the PID to range between 0 and the full window size
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
     double c = thermocouple.readCelsius();
   if (isnan(c)) {
     Serial.println("Something wrong with thermocouple!");
   } else {
     Serial.print("C = "); 
     Serial.println(c);
   }
   delay(500);
  Input = c;
  myPID.Compute();
  Serial.print("myPID.Compute() = ");
Serial.println(myPID.Compute());
  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/
  unsigned long now = millis();
  Serial.println(windowStartTime);
  if (now - windowStartTime > WindowSize)
  { //time to shift the Relay Window
    windowStartTime += WindowSize;
  }
  Serial.print("Output = ");
  Serial.println(Output);
  Serial.print("Now = ");
  Serial.println(now);
  if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
  else digitalWrite(RelayPin, LOW);
}

So, keep the window size bigger for now and rather than using Output directly, derive the time to keep the heater on from it. As I suggested, using it as a percentage is a simple way to start.

You may want to consider having a minimum time to run the heater too, but get it working better before you add that.

DKWatson:
Understood wildbill, but as a student, don't you think maybe it's better to use a simple algorithm to start and learn what it does rather than jump in with both feet and use a library that one knows nothing about and the simply tweak it 'til it works?

Many of the on-the-fly tuning used in more advanced controllers/libraries are exactly the things that someone new to control theory needs to be adjusting by hand to better gain an intuitive understanding of how it works. I don't at all disagree with you, I'm just a firm believer in sound basics being a better platform for learning.

Obviously, your viewpoint is a valid one. I have a (mild) personal preference for "Here's a black box to use to start with. Now you've got it working with the crutch (library), write your own".

sal_murd:
I'm still not able to get the tuning appropriate. The way it is switching on and off would damage the heater. can you please help me understand how its actually working with pwm. my code is updated as below and the switching on and off is a bit faster for the heating element.

/********************************************************

PID RelayOutput Example
  Same as basic example, except that this time, the output
  is going to a digital pin which (we presume) is controlling
  a relay.  The pid is designed to output an analog value,
  but the relay can only be On/Off.

To connect them together we use "time proportioning
  control"  Tt's essentially a really slow version of PWM.
  First we decide on a window size (5000mS say.) We then
  set the pid to adjust its output between 0 and that window
  size.  Lastly, we add some logic that translates the PID
  output into "Relay On Time" with the remainder of the
  window being "Relay Off Time"
********************************************************/
#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO   3
#define MAXCS   4
#define MAXCLK  5
Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 5, Ki = 2, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 1000;
unsigned long windowStartTime;
void setup()
{
  Serial.begin(9600);

while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

Serial.println("MAX31855 test");
 // wait for MAX chip to stabilize
 delay(500);
 
 pinMode(RelayPin, OUTPUT);

windowStartTime = millis();

//initialize the variables we're linked to
 Setpoint = 30;

//tell the PID to range between 0 and the full window size
 myPID.SetOutputLimits(0, WindowSize);

//turn the PID on
 myPID.SetMode(AUTOMATIC);
}

void loop()
{
    double c = thermocouple.readCelsius();
  if (isnan(c)) {
    Serial.println("Something wrong with thermocouple!");
  } else {
    Serial.print("C = ");
    Serial.println(c);
  }
  delay(500);
 Input = c;
 myPID.Compute();
 Serial.print("myPID.Compute() = ");
Serial.println(myPID.Compute());
 /************************************************
    turn the output pin on/off based on pid output
  ************************************************/
 unsigned long now = millis();
 Serial.println(windowStartTime);
 if (now - windowStartTime > WindowSize)
 { //time to shift the Relay Window
   windowStartTime += WindowSize;
 }
 Serial.print("Output = ");
 Serial.println(Output);
 Serial.print("Now = ");
 Serial.println(now);
 if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
 else digitalWrite(RelayPin, LOW);
}

you will need to consider these changes I have documented in the modified code. sorry I couldn't compile and test, I am lacing one of the libraries you are using

/********************************************************
   PID RelayOutput Example
   Same as basic example, except that this time, the output
   is going to a digital pin which (we presume) is controlling
   a relay.  The pid is designed to output an analog value,
   but the relay can only be On/Off.

     To connect them together we use "time proportioning
   control"  Tt's essentially a really slow version of PWM.
   First we decide on a window size (5000mS say.) We then
   set the pid to adjust its output between 0 and that window
   size.  Lastly, we add some logic that translates the PID
   output into "Relay On Time" with the remainder of the
   window being "Relay Off Time"
 ********************************************************/
#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO   3
#define MAXCS   4
#define MAXCLK  5


#define PIDSampleRate 100  //default is 100 miliseconds


Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 5, Ki = 2, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 1000;
unsigned long windowStartTime;
void setup()
{
  Serial.begin(9600);

  while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

  Serial.println("MAX31855 test");
  // wait for MAX chip to stabilize
  delay(500);

  pinMode(RelayPin, OUTPUT);
  windowStartTime = millis();

  //initialize the variables we're linked to
  Setpoint = 30;

  //tell the PID to range between 0 and the full window size
  myPID.SetSampleTime(PIDSampleRate); //default is 100 miliseconds
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
  static unsigned long timerDelay;

  if ((millis() - timerDelay) >= (PIDSampleRate - 2)) {// lets get the temperature 2 miliseconds prior to the next PID calculation
    timerDelay = millis(); //


    double c = thermocouple.readCelsius();
    if (isnan(c)) {
      Serial.println("Something wrong with thermocouple!");
    } else {
      Serial.print("C = ");
      Serial.println(c);
    }
    Input = c;
  }
  if (myPID.Compute()) { // Compute returns true when it actually does the calculation
    timerDelay = millis(); // adjust the timer to match Compute delay timer
    
  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/
    unsigned long now = millis();
    Serial.println(windowStartTime);
    if (now - windowStartTime > WindowSize)
    { //time to shift the Relay Window
      windowStartTime += WindowSize;
    }
    Serial.print("Output = ");
    Serial.println(Output);
    Serial.print("Now = ");
    Serial.println(now);
    if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
    else digitalWrite(RelayPin, LOW);
  }
}

The Compute has a built-in blink without delay timer which messes with your code.
also your delay(500); is corrupting your PID calculations.
Start tuning with Integral slowly increasing until you start to swing. then back down
now by adding a little proportional to the code will speed up the landing of the setpoint
Dirivitive is more for reacting to sudden changes either in setpoint or in the environment. derivative shouldn't be needed until fine-tuning for quick recovery after a change occurs
Hope this helps

zhomeslice:
you will need to consider these changes I have documented in the modified code. sorry I couldn't compile and test, I am lacing one of the libraries you are using

/********************************************************

PID RelayOutput Example
  Same as basic example, except that this time, the output
  is going to a digital pin which (we presume) is controlling
  a relay.  The pid is designed to output an analog value,
  but the relay can only be On/Off.

To connect them together we use "time proportioning
  control"  Tt's essentially a really slow version of PWM.
  First we decide on a window size (5000mS say.) We then
  set the pid to adjust its output between 0 and that window
  size.  Lastly, we add some logic that translates the PID
  output into "Relay On Time" with the remainder of the
  window being "Relay Off Time"
********************************************************/
#include <PID_v1.h>
#include <SPI.h>
#include "Adafruit_MAX31855.h"

#define RelayPin 8

// Example creating a thermocouple instance with software SPI on any three
// digital IO pins.
#define MAXDO  3
#define MAXCS  4
#define MAXCLK  5

#define PIDSampleRate 100  //default is 100 miliseconds

Adafruit_MAX31855 thermocouple(MAXCLK, MAXCS, MAXDO);
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
double Kp = 5, Ki = 2, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int WindowSize = 1000;
unsigned long windowStartTime;
void setup()
{
  Serial.begin(9600);

while (!Serial) delay(1); // wait for Serial on Leonardo/Zero, etc

Serial.println("MAX31855 test");
  // wait for MAX chip to stabilize
  delay(500);

pinMode(RelayPin, OUTPUT);
  windowStartTime = millis();

//initialize the variables we're linked to
  Setpoint = 30;

//tell the PID to range between 0 and the full window size
  myPID.SetSampleTime(PIDSampleRate); //default is 100 miliseconds
  myPID.SetOutputLimits(0, WindowSize);

//turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
  static unsigned long timerDelay;

if ((millis() - timerDelay) >= (PIDSampleRate - 2)) {// lets get the temperature 2 miliseconds prior to the next PID calculation
    timerDelay = millis(); //

double c = thermocouple.readCelsius();
    if (isnan(c)) {
      Serial.println("Something wrong with thermocouple!");
    } else {
      Serial.print("C = ");
      Serial.println(c);
    }
    Input = c;
  }
  if (myPID.Compute()) { // Compute returns true when it actually does the calculation
    timerDelay = millis(); // adjust the timer to match Compute delay timer
   
  /************************************************
    turn the output pin on/off based on pid output
  ************************************************/
    unsigned long now = millis();
    Serial.println(windowStartTime);
    if (now - windowStartTime > WindowSize)
    { //time to shift the Relay Window
      windowStartTime += WindowSize;
    }
    Serial.print("Output = ");
    Serial.println(Output);
    Serial.print("Now = ");
    Serial.println(now);
    if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
    else digitalWrite(RelayPin, LOW);
  }
}




The Compute has a built-in blink without delay timer which messes with your code. 
also your delay(500); is corrupting your PID calculations.
Start tuning with Integral slowly increasing until you start to swing. then back down
now by adding a little proportional to the code will speed up the landing of the setpoint
Dirivitive is more for reacting to sudden changes either in setpoint or in the environment. derivative shouldn't be needed until fine-tuning for quick recovery after a change occurs
Hope this helps

Thanks for your help zhomeslice . The code compiled no errors. After uploading to arduino, In the beginning the switching on and off was faster than required, since it would damage the heater. But after a couple of minutes, it becomes stable i.e., not fast switching. Also when the temperature goes beyond the setpoint it takes a while to switch off the heater until temperature reaches 29 degrees. I removed the delay as you said its corrupting PID calculations, but i will have 4 more sensors later, that have their own measurement delays

How can the fast switching in the beginning be controlled and also the delays be accommodated in the calculations without messing with the PID ?
Thank you very much for support

sal_murd:
Thanks for your help zhomeslice . The code compiled no errors. After uploading to arduino, In the beginning the switching on and off was faster than required, since it would damage the heater. But after a couple of minutes, it becomes stable i.e., not fast switching. Also when the temperature goes beyond the setpoint it takes a while to switch off the heater until temperature reaches 29 degrees. I removed the delay as you said its corrupting PID calculations, but i will have 4 more sensors later, that have their own measurement delays

How can the fast switching in the beginning be controlled and also the delays be accommodated in the calculations without messing with the PID ?
Thank you very much for support

Welcome
How fast can you cycle the heater without any damage
for my temperature control project (reflow oven) I used a toaster oven I had a cycle time where the PID controlled how much time over a 2 second period the heating element was on.
for my code, I used a cycle duration of 2 seconds. How this worked is the heater could be on for a minimum of 0 seconds up to a max of 2 seconds every 2 seconds. so when the PID called for 2 seconds (2000 ms) the heater would remain on continuously. also if the PID calls for a little heat the heating element would pulse for a fraction of a second and remain off for the remainder of the 2 seconds.
This is a portion of my code with minor changes and simplified. You will need to add your thermocouple code to test it.

#include <PID_v1.h>

#define SSRPin1 8 // digital output to the heating element

double Kp=1, Ki=3.1, Kd=0.2;
unsigned long WindowSize = 2000;

double Setpoint, Input, Output;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT );
void setup() {
  Serial.begin(9600);
  pinMode(SSRPin1, OUTPUT);
  myPID.SetOutputLimits(0, 1000); //1000 steps in the control loop
  myPID.SetSampleTime(100); //default is 100 miliseconds
  myPID.SetMode(MANUAL);// clears windup anc verifies PID calculations are correct based on sample time
  myPID.SetMode(AUTOMATIC); // start the PID ready for first calculation
}

void loop() {
  double c = thermocouple.readCelsius();
  Input = c;
  if (myPID.Compute()){ // this only calculates once every 100 ms and returns true when it does.
     Serial.print(Input);
     Serial.print(" ");
     Serial.print(Output);
     Serial.println();
  }
  WriteToSSR(Output);
}


void WriteToSSR(double value){
  unsigned long wind = millis() % WindowSize; // (millis() - windowStartTime); // convert millis into a millisecond counter that resets at windowSize
  unsigned long oVal;
  oVal = (unsigned long)(value*(double)WindowSize/ 1000.0);// convert windowSize into seconds and multiply it by output which ranges from 0 to 1000
  digitalWrite(SSRPin1 ,(oVal>wind) ? HIGH : LOW); // Set the output
}

Z

zhomeslice:
Welcome
How fast can you cycle the heater without any damage
for my temperature control project (reflow oven) I used a toaster oven I had a cycle time where the PID controlled how much time over a 2 second period the heating element was on.
for my code, I used a cycle duration of 2 seconds. How this worked is the heater could be on for a minimum of 0 seconds up to a max of 2 seconds every 2 seconds. so when the PID called for 2 seconds (2000 ms) the heater would remain on continuously. also if the PID calls for a little heat the heating element would pulse for a fraction of a second and remain off for the remainder of the 2 seconds.
This is a portion of my code with minor changes and simplified. You will need to add your thermocouple code to test it.

#include <PID_v1.h>

#define SSRPin1 8 // digital output to the heating element

double Kp=1, Ki=3.1, Kd=0.2;
unsigned long WindowSize = 2000;

double Setpoint, Input, Output;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT );
void setup() {
  Serial.begin(9600);
  pinMode(SSRPin1, OUTPUT);
  myPID.SetOutputLimits(0, 1000); //1000 steps in the control loop
  myPID.SetSampleTime(100); //default is 100 miliseconds
  myPID.SetMode(MANUAL);// clears windup anc verifies PID calculations are correct based on sample time
  myPID.SetMode(AUTOMATIC); // start the PID ready for first calculation
}

void loop() {
  double c = thermocouple.readCelsius();
  Input = c;
  if (myPID.Compute()){ // this only calculates once every 100 ms and returns true when it does.
    Serial.print(Input);
    Serial.print(" ");
    Serial.print(Output);
    Serial.println();
  }
  WriteToSSR(Output);
}

void WriteToSSR(double value){
  unsigned long wind = millis() % WindowSize; // (millis() - windowStartTime); // convert millis into a millisecond counter that resets at windowSize
  unsigned long oVal;
  oVal = (unsigned long)(value*(double)WindowSize/ 1000.0);// convert windowSize into seconds and multiply it by output which ranges from 0 to 1000
  digitalWrite(SSRPin1 ,(oVal>wind) ? HIGH : LOW); // Set the output
}



Z

Hi dear,

I want the heater to be On for a minimum of 5 sec. It is an aquarium heater connected to the main supply through a relay. How is this achievable?

sal_murd:
Hi dear,

I want the heater to be On for a minimum of 5 sec. It is an aquarium heater connected to the main supply through a relay. How is this achievable?

A minimum of 5 second cycle time isn't difficult to achieve
We will need to make some simple changes. I provided lots of notes in this code and simplified it more. I tend to crunch things together as I code :slight_smile:
I also explain PID in detail and made changes to the PID better control you fish tank heater.

#include <PID_v1.h>

#define SSRPin1 8 // digital output to the heating element
int RelayState;

double Kp = 0.0, Ki = 3.0, Kd = 0.0;
/* now with a 1 second PID cycle time these numbers are incredibly easier to explain
   PID_v1 uses a 1 second duration to simplify the Ki and Kd values. Kp is calculated every cycle time but time has no influence on the calculation
   Proportional is not affected by time -1° offset from setpoint == +1 on the output when Kp is set to 1 this is now equil to adding 30 miliseconds to the relay pulse 30000/1000
   Integral is a time based value that is additive integral starts at ZERO influence and then increases each second the amount of Ki for every degree of error
   if we have a -10° error from setpoint this cycle integral will add 300 miliseconds to the pulse each cycle times the value of Ki. with the Ki of 3 900 miliseconds will be added.
   ** this is why Ki must be tuned first to quickly find the best cycle time without having too much influence
   ** remember if we are at setpoint Kp alone will not provide any cycle time temperature == setpoint means error == ZERO Kp of any value * ZERO == ZERO influence on the output 
   Kd is looking at change only and when you reach adn maintain setpoint Kd does nothing!!!
   Kd looks for the rate of change from the last reading if the rate is +1° and Kd is at 1 then se will remove 30 milliseconds from the outpot for just this cycle
   for your project Kd will only create noise so lets keep it at ZERO
   Note that Kp's Ki's and Kd's influence are added together to create your Output value
*/
unsigned long WindowSize = 30000;// **** 30 seconds cycle
// to clarify what WindowSize gives us the max duration between cycles of the heater. it doesent mean that if things are changing fast that the heater couldn't suddenly turn on to accomidate the change as long as it is within the window of on time.
// because your window is much larger than mine we could see this behavior where the pid loop calculates in the middle of the 30 second cycle and activates the heater prior to the next cycle.
// to prevent short cycling we will ad a blink without delay


double Setpoint, Input, Output;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT );
void setup() {
  Serial.begin(9600);
  pinMode(SSRPin1, OUTPUT);
  myPID.SetOutputLimits(0, 1000); //1000 steps in the control loop
  myPID.SetSampleTime(1000); //default is 100 miliseconds **** Changing to 1 second which will make things simpler for you to tune
  myPID.SetMode(MANUAL);// clears windup anc verifies PID calculations are correct based on sample time
  delay(5000); // We need to prep the millis() timer below to have 5 seconds on it before PID starts calculating and turns on the heater
  myPID.SetMode(AUTOMATIC); // start the PID ready for first calculation
}

void loop() {
  double = thermocouple.readCelsius();
  int PIDCalculated;
  Input = c;
  PIDCalculated = myPID.Compute(); //**** this only calculates once every Second and returns true when it does.
  WriteToSSR(Output);
  if (PIDCalculated) {
    Serial.print(Input);
    Serial.print("°C ");
    Serial.print(Output / 10);
    Serial.print("% Relay is: ");
    Serial.print((RelayState) ? "On" : "OFF");
    Serial.println();
  }
}

void WriteToSSR(double value) {
  unsigned long windowTime = millis() % WindowSize; // (millis() - windowStartTime); // convert millis into a millisecond counter that resets at windowSize
  unsigned long onTimeVal;
  onTimeVal = (unsigned long)(value * (double)WindowSize / 1000.0); // convert windowSize into seconds and multiply it by output which ranges from 0 to 1000

  if (onTimeVal > windowTime) {
    RelayState = HIGH;

  } else {
    RelayState = LOW;
  }
  
  // short cycle prevention (Blink without delay)
  static unsigned long ShortCycleTimer;
  if ((millis() - ShortCycleTimer) >= (5000)) {
    if ((digitalRead(SSRPin1) == LOW) && (RelayState = HIGH)) ShortCycleTimer = millis(); //**** this sets a blink without delay timer that prevents the following line from triggeringfor 5 seconds after it changes the output to HIGH
    digitalWrite(SSRPin1 , RelayState); // Set the output
  }
}

Z

zhomeslice:
The Compute has a built-in blink without delay timer which messes with your code.
also your delay(500); is corrupting your PID calculations.

Thanks zhomeslice for your response. The problem remaining is I have got some other sensors in addition to the temperature sensor.
Light Sensor, Dissolved oxygen sensor, Electrical Conductivity sensor, pH sensor. The last 3 sensors have their measurement delay ,600ms each. As you mentioned the delay corrupts PID calculations, what is the way to accommodate for these measurements delays?