PID autotune with Relay not working

I've 'butchered' some code together to create an autotune PID that should operate a relay. Input temperature is controlled by a rotary switch.

Except, it doesn't quite work.

the output doesn't switch on/off correctly...

this is my first real bit of coding, so I'm a bit out of my depth! any Ideas?

https://gist.github.com/anonymous/8e4d15c4c99b0c1cd75bb11dae3825d2

OK, I’m not going to answer your question, but rather ask a question that’s had me scratching my head forever:

Why on earth use a PID to control a heater with a relay-based control. I can see no advantage to this over a simple on/off controller with hysteresis. Using a PID seems like a lot of added complexity with zero functional benefit. I have used a trivial hysteresis controller for temperature control with excellent results:

if (currentTemp >= (setPoint + hysteresis))
    heaterOff();
else if  (currentTemp <= (setPoint - hysteresis))
    heaterOn();

I use exactly this code to control my powder-coating oven, and it maintains temperature within +/-1 degreeF, which is about as good as it gets.

A PID makes perfect sense with a PWM control, but with a relay, I just don’t see the point.

Regards,
Ray L.

I think even with a relay PID is better at finer temp controls than hysteresis (less overshoot).

For this application (Sous Vide) a coupe of degrees can make a difference.

nick9one1: I think even with a relay PID is better at finer temp controls than hysteresis (less overshoot).

For this application (Sous Vide) a coupe of degrees can make a difference.

For critical applications, with a system that responds very fast, perhaps. For 99% of real-world applications, I doubt it. And, for critical applications, why not take the absolutely trivial extra step of using PWM control, and get the full benefit. The cost is a very few $. As I said, a simple hysteresis controller is quite capable of maintaining +/-1F control. What are you doing that requires more than that? And what are you doing to ensure you have that control over more than the very small volume immediately around the temperature sensor? Whether you're heating air, water, or something else, unless you're taking steps to "stir" the target, you'll end up with large temperature gradients that will completely swamp any inaccuracy in the control. It doesn't pay to optimize one parameter, if you're ignoring other, equally, or more, important parameters.

Regards, Ray L.

You have some code that you didn't post.

It does something that you did not describe.

You expect it to do something. Presumably, not what it actually does, or it would be "working".

You don't really expect help, do you?

I've 'butchered' some code together

That is the problem, right there.

PaulS: You have some code that you didn't post.

It does something that you did not describe.

You expect it to do something. Presumably, not what it actually does, or it would be "working".

You don't really expect help, do you?

Asked for help Described what it should do, and what it doesnt I posted the code

I fail to see what the problem is?

jremington: That is the problem, right there.

thanks... helpful!

Described what it should do, and what it doesnt

Not in enough detail. What does the code ACTUALLY do? You talk about a heater and a relay, but there is an ISR that deals with an encoder. What the hell is that for?

    if(tuning)
    {
    tuning=false;
    
//    changeAutoTune();

    tuning=true;
    }

What is THAT supposed to accomplish? Why is it even in the code?

Ok I see your point, I’ll try to explain better.

I’m trying to create a PID that will maintain the temperature of a body of water by switching an SSR that will energise a heating element.

I’m also using a rotary encoder to allow the target temperature to be changed easily (thats what the ISR is for).

There is a 20x4 LCD that displays set temp, current temp, heating status.

I’m using code from -
http://playground.arduino.cc/Code/PIDAutotuneLibrary

I’d temporarily commented out ‘’// changeAutoTune();’’ to test and forgot to enable it again before posting the code. Sorry my mistake!

It mostly seems to work, the display shows all the correct information, encoder works, temp works.
But the code doesn’t seem to operate the relay correctly.

It does cycle on/off but doesn’t have any relation to the current temperature. i.e. if the set temp is 10 degrees and current temp is 20, the relay stays on.

I think the problem is with this part of the code -

else myPID.Compute();

if(useSimulation)
{
theta[30]=output;
if(now>=modelTime)
{
modelTime +=100;
DoModel();
}
}
else
{
analogWrite(0,output);
}

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

  • turn the output pin on/off based on pid output
    ************************************************/
    if (millis() - windowStartTime > WindowSize)
    { //time to shift the Relay Window
    windowStartTime += WindowSize;
    }
    if (output < millis() - windowStartTime)
    {
    outputindicate = 1;
    digitalWrite(RelayPin, HIGH);
    }
    else
    {
    digitalWrite(RelayPin, LOW) ;
    outputindicate = 0;
    }

//send-receive with processing if it’s time
if(millis()>serialTime)
{
SerialReceive();
SerialSend();
serialTime+=500;

}

The standard auto tune example looks like this -

else myPID.Compute();

if(useSimulation)
{
theta[30]=output;
if(now>=modelTime)
{
modelTime +=100;
DoModel();
}
}
else
{
analogWrite(0,output);
}

//send-receive with processing if it’s time
if(millis()>serialTime)
{
SerialReceive();
SerialSend();
serialTime+=500;
}
}

but obviously wont control a relay.

There is another example in the PID library called PID_RelayOutput

myPID.Compute();

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

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

I tried to combine this code into mine, but unsuccessfully.

I think the problem is with this part of the code -

If you are not using the simulation portion of the sample, GET RID OF IT.

If you are, GET RID OF THE NON-SIMULATION portion of the code.

Concentrate on the code that is actually executed.

For now, put the encoder and LCD in a drawer, and lock it. They have nothing to do with the problem, so they should not even be in the code you post.

Now that you have a lot less code, with hardcoded setpoint, post it again, showing some sample serial output, and explaining exactly what happens (the explanation was a LOT better this time).

Ok here goes again -

The simulation is part of the playgound example. It helps when troubleshooting.

This is the relay example from the playground;

#include <PID_v1.h>

#define PIN_INPUT 0
#define RELAY_PIN 6

//Define Variables we’ll be connecting to
double Setpoint, Input, Output;

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

int WindowSize = 5000;
unsigned long windowStartTime;

void setup()
{
windowStartTime = millis();

//initialize the variables we’re linked to
Setpoint = 100;

//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()
{
Input = analogRead(PIN_INPUT);
myPID.Compute();

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

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

}

And the autotune example;
http://playground.arduino.cc/Code/PIDAutotuneLibrary

#include <PID_v1.h>
#include <PID_AutoTune_v0.h>

byte ATuneModeRemember=2;
double input=80, output=50, setpoint=180;
double kp=2,ki=0.5,kd=2;

double kpmodel=1.5, taup=100, theta[50];
double outputStart=5;
double aTuneStep=50, aTuneNoise=1, aTuneStartValue=100;
unsigned int aTuneLookBack=20;

boolean tuning = false;
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 = true;

void setup()
{
if(useSimulation)
{
for(byte i=0;i<50;i++)
{
theta*=outputStart;*

  • }*

  • modelTime = 0;*

  • }*

  • //Setup the pid*

  • myPID.SetMode(AUTOMATIC);*

  • if(tuning)*

  • {*

  • tuning=false;*

  • changeAutoTune();*

  • tuning=true;*

  • }*

  • serialTime = 0;*

  • Serial.begin(9600);*
    }
    void loop()
    {

  • unsigned long now = millis();*

  • if(!useSimulation)*

  • { //pull the input in from the real world*

  • input = analogRead(0);*

  • }*

  • 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*

  • {*

  • analogWrite(0,output);*

  • }*

  • //send-receive with processing if it’s time*

  • if(millis()>serialTime)*

  • {*

  • SerialReceive();*

  • SerialSend();*

  • serialTime+=500;*

  • }*
    }
    void changeAutoTune()
    {
    if(!tuning)

  • {*

  • //Set the output to the desired starting frequency.*

  • output=aTuneStartValue;*

  • 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("setpoint: “);Serial.print(setpoint); Serial.print(” ");*

  • Serial.print("input: “);Serial.print(input); Serial.print(” ");*

  • Serial.print("output: “);Serial.print(output); Serial.print(” ");*

  • if(tuning){*

  • Serial.println(“tuning mode”);*

  • } else {*

  • Serial.print("kp: “);Serial.print(myPID.GetKp());Serial.print(” ");*

  • Serial.print("ki: “);Serial.print(myPID.GetKi());Serial.print(” ");*

  • Serial.print("kd: ");Serial.print(myPID.GetKd());Serial.println();*

  • }*
    }
    void SerialReceive()
    {

  • if(Serial.available())*

  • {*

  • char b = Serial.read();*

  • Serial.flush();*

  • if((b==‘1’ && !tuning) || (b!=‘1’ && tuning))changeAutoTune();*

  • }*
    }
    void DoModel()
    {

  • //cycle the dead time*

  • for(byte i=0;i<49;i++)*

  • {*
    _ theta = theta[i+1];_
    * }*
    * //compute the input*
    _ input = (kpmodel / taup) (theta[0]-outputStart) + input(1-1/taup) + ((float)random(-10,10))/100;_
    }[/quote]
    I seem cant get autotune working with the relay output from the first example.

I wonder if part of your problem is this line:   int percent = ((output / 255) * 100);
It is calculating a percentage based on the assumption that the Output range is set to the default 0-255 but the relay example uses 0-5000 milliseconds.

I could not compile your sketch or the AutoTune example sketch because both libraries define LIBRARY_VERSION. I modified the PID_v1 library to define PID_v1_VERSION instead.

Next problem was in the LiquidCrystal_I2C library:

sketch_oct11a:35: error: 'POSITIVE' was not declared in this scope
 LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
                                                     ^

The version of LiquidCrystal_I2C that I have doesn’t have a constructor that takes 10 arguments. It only takes Address, Columns, and Lines. After changing that line to " LiquidCrystal_I2C lcd(0x27, 20, 4);" I was left with only a few warnings:

/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino: In function 'void loop()':
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino:161:61: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
   if (reading == HIGH && previous == LOW && millis() - time > debounce) {
                                                             ^
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino:174:54: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
     if ( (unsigned long)(millis() - SpamTimerSerial) >= (t)) {
                                                      ^
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino:186:49: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
   if ( (unsigned long)(millis() - SpamTimerLCD) >= (t)) {
                                                 ^
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino:245:34: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
   if (millis() - windowStartTime > WindowSize)
                                  ^
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino: In function 'void SerialSend()':
/Users/john/Documents/Arduino/sketch_oct11a/sketch_oct11a.ino:306:7: warning: unused variable 'percent' [-Wunused-variable]
   int percent = ((output / 255) * 100);
       ^
/Users/john/Documents/Arduino/libraries/PID_AutoTune_v0/PID_AutoTune_v0.cpp: In member function 'int PID_ATune::Runtime()':
/Users/john/Documents/Arduino/libraries/PID_AutoTune_v0/PID_AutoTune_v0.cpp:41:19: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
  if((now-lastTime)<sampleTime) return false;
                   ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:66:39: warning: unused parameter 'cols' [-Wunused-parameter]
 void LiquidCrystal_I2C::begin(uint8_t cols, uint8_t lines, uint8_t dotsize) {

                                       ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:307:39: warning: unused parameter 'cmdDelay' [-Wunused-parameter]
 void LiquidCrystal_I2C::setDelay (int cmdDelay,int charDelay) {}

                                       ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:307:52: warning: unused parameter 'charDelay' [-Wunused-parameter]
 void LiquidCrystal_I2C::setDelay (int cmdDelay,int charDelay) {}

                                                    ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:310:50: warning: unused parameter 'graphtype' [-Wunused-parameter]
 uint8_t LiquidCrystal_I2C::init_bargraph(uint8_t graphtype){return 0;}

                                                  ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:311:55: warning: unused parameter 'row' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end){}

                                                       ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:311:68: warning: unused parameter 'column' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end){}

                                                                    ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:311:84: warning: unused parameter 'len' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end){}

                                                                                    ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:311:98: warning: unused parameter 'pixel_col_end' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_col_end){}

                                                                                                  ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:312:53: warning: unused parameter 'row' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_row_end){}

                                                     ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:312:66: warning: unused parameter 'column' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_row_end){}

                                                                  ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:312:82: warning: unused parameter 'len' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_row_end){}

                                                                                  ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:312:96: warning: unused parameter 'pixel_row_end' [-Wunused-parameter]
 void LiquidCrystal_I2C::draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len,  uint8_t pixel_row_end){}

                                                                                                ^
/Users/john/Documents/Arduino/libraries/LiquidCrystal_I2C/LiquidCrystal_I2C.cpp:313:45: warning: unused parameter 'new_val' [-Wunused-parameter]
 void LiquidCrystal_I2C::setContrast(uint8_t new_val){}

                                             ^
/Users/john/Documents/Arduino/libraries/DallasTemperature/DallasTemperature.cpp:781:60: warning: unused parameter 'deviceAddress' [-Wunused-parameter]
 void DallasTemperature::defaultAlarmHandler(const uint8_t* deviceAddress){}

                                                            ^

Sketch uses 13,854 bytes (42%) of program storage space. Maximum is 32,256 bytes.
Global variables use 1,193 bytes (58%) of dynamic memory, leaving 855 bytes for local variables. Maximum is 2,048 bytes.