PID Autotune for Infrared Temperature Controller Project

Hello everyone.

Code in the next message

I have been developing an IR temperature controller for induction heating, with two operating modes as On/Off and PID. The On/Off mode works pretty good, however since some of the induction heating projects deal with high overshoots, we need to add the PID feature.

I have used the Sous Vide Controller code from Adafruit as the base of my code and am trying to modify it into what suits our kinds of projects. The main issue comes from the natural differences between the Adafruit project and ours. In the Adafruit project, the heating occurs pretty slow and they have developed the autotuning in a way compatible with very slow temperature changes (in the order of several minutes). However, in induction heating we sometimes deal with very high heating rates (several tens of degrees in seconds). This has been caused issues with the autotuning of the PID controller that I am using.

The other issue is that in the Adafruit code, the autotuning is activated when the system is in a steady state condition. However I need to change the code in a way that the autotuning starts from the beginning and considers the time to temperature too.

I think there are some parameters from the PID Autotune library that are not introduced in the Adafruit code and need to be modified in order to get a smooth response considering the time to temperature, but I am not sure which ones.

The code below (On/Off mode is deleted for abstracting) currently takes the inputs (Temperature Setpoint, Kp, Ki, Kd) and then runs the PID control. When the object reaches close to the temperature setpoint, the autotune mode can be activated. I have attached the connections diagram to this message.

Any help on how to modify this code to run the PID Autotune mode in the correct way is highly appreciated.

Connections.jpg

// PID Library
#include <PID_v1.h>
#include <PID_AutoTune_v0.h>

// Libraries for the Adafruit RGB/LCD Shield
#include <Wire.h>
#include <Adafruit_RGBLCDShield.h>

// So we can save and retrieve settings
#include <EEPROM.h>

// ************************************************
// Pin definitions
// ************************************************

#define RelayPin 9 // Output Relay, goes to the induction heater
int tempPin = 0; // temperature reading analogue input
// ************************************************
// Variables and constants
// ************************************************

const float Pi = 3.14159;

//Define Variables we'll be connecting to
double Setpoint;
double Input;
double Output;
unsigned long Starttime;

volatile long onTime = 0;

// pid tuning parameters
double Kp;
double Ki;
double Kd;

// EEPROM addresses for persisted data
const int SpAddress = 0;
const int KpAddress = 8;
const int KiAddress = 16;
const int KdAddress = 24;


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

// 1 second Time Proportional Output window
int WindowSize = 1000; 
unsigned long windowStartTime;

// ************************************************
// Auto Tune Variables and constants
// ************************************************
byte ATuneModeRemember=2;

double aTuneStep=500;
double aTuneNoise=1;
unsigned int aTuneLookBack=20;

boolean tuning = false;

PID_ATune aTune(&Input, &Output);

// ************************************************
// Display Variables and constants
// ************************************************

Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
// These #defines make it easy to set the backlight color
#define RED 0x1
#define YELLOW 0x3
#define GREEN 0x2
#define TEAL 0x6
#define BLUE 0x4
#define VIOLET 0x5
#define WHITE 0x7

#define BUTTON_SHIFT BUTTON_SELECT

unsigned long lastInput = 0; // last button press

byte degree[8] = // define the degree symbol 
{ 
 B00110, 
 B01001, 
 B01001, 
 B00110, 
 B00000,
 B00000, 
 B00000, 
 B00000 
}; 

const long logInterval = 5000; // log every 5 seconds
unsigned long lastAdjTime = 0;
unsigned long lastLogTime = 0;

// ************************************************
// States for state machine
// ************************************************
enum operatingState { OFF = 0, SETP, RUN, TUNE_P, TUNE_I, TUNE_D, AUTO};
operatingState opState = OFF;


// ************************************************
// Setup and display initial screen
// ************************************************
void setup()
{
   Serial.begin(9600);

   // Initialize Relay Control:

   pinMode(RelayPin, OUTPUT);    // Output mode to drive relay
   digitalWrite(RelayPin, LOW);  // make sure it is off to start



   // Initialize LCD DiSplay 

   lcd.begin(16, 2);
   lcd.createChar(1, degree); // create degree symbol from the binary
   

   // Initialize the PID and related variables
   LoadParameters();
   myPID.SetTunings(Kp,Ki,Kd);

   myPID.SetSampleTime(1000);
   myPID.SetOutputLimits(0, WindowSize);

  // Run timer2 interrupt every 15 ms 
 TCCR2A = 0;
  TCCR2B = 1<<CS22 | 1<<CS21 | 1<<CS20;

  //Timer2 Overflow Interrupt Enable
  TIMSK2 |= 1<<TOIE2;
}

// ************************************************
// Timer Interrupt Handler
// ************************************************
SIGNAL(TIMER2_OVF_vect) 
{
  if (opState == OFF)
  {
    digitalWrite(RelayPin, LOW);  // make sure relay is off
  }
  else if (opState == RUN)
  {
    DriveOutput();
  }
}

// ************************************************
// Main Control Loop
//
// All state changes pass through here
// ************************************************
void loop()
{
   // wait for button release before changing state
   while(ReadButtons() != 0) {}

   lcd.clear();

   switch (opState)
   {
   case OFF:
      Off();
      break;
 
   case SETP:
      Tune_Sp();
      break;

   case RUN:
      Run();
      break;
   case TUNE_P:
      TuneP();
      break;
   case TUNE_I:
      TuneI();
      break;
   case TUNE_D:
      TuneD();
      break;

   }
}

// ************************************************
// Initial State - press RIGHT to start heating
// ************************************************
void Off()
{
   myPID.SetMode(MANUAL);
   lcd.setBacklight(GREEN);
   digitalWrite(RelayPin, LOW);  // make sure it is off
   lcd.print(F("     Hi"));
   lcd.setCursor(0, 1);
   lcd.print(F(" PID-OFF"));
   uint8_t buttons = 0;
   
   while(!(buttons & (BUTTON_RIGHT)))
   {
      buttons = ReadButtons();
   }
   // Prepare to transition to the RUN state
   // turn the PID on:
   myPID.SetMode(AUTOMATIC);
   windowStartTime = millis();
   opState = SETP; // start control
}

/////////////////////////////////////////////////////////////////////////////////////////


// ************************************************
// Setpoint / Target Temp Entry State
// UP/DOWN to change setpoint
// RIGHT for tuning parameters
// SHIFT for 10x tuning
// ************************************************
void Tune_Sp()
{
   lcd.setBacklight(VIOLET);
   lcd.print(F("Set Temp ["));
   lcd.write(1);
   lcd.print(F("C]:"));
   uint8_t buttons = 0;
   while(true)
   {
      buttons = ReadButtons();

      float increment = 1;
      if (buttons & BUTTON_SHIFT)
      {
        increment *= 10;
      }

      if (buttons & BUTTON_RIGHT)
      {
         opState = TUNE_P;
         return;
      }
      if (buttons & BUTTON_UP)
      {
         Setpoint += increment;
         Setpoint = int(Setpoint);
         delay(200);
      }
      if (buttons & BUTTON_DOWN)
      {
         Setpoint -= increment;
         Setpoint = int(Setpoint);
         delay(200);
      }
 
         lcd.setCursor(0,1);
         lcd.print(int(Setpoint));
         lcd.print("   ");
         lcd.setCursor(10,1);
      }
   }




// ************************************************
// Proportional Tuning State
// UP/DOWN to change Kp
// RIGHT for Ki
// SHIFT for 10x tuning
// ************************************************
void TuneP()
{
   lcd.setBacklight(VIOLET);
   lcd.print(F("Set Kp"));

   uint8_t buttons = 0;
   while(true)
   {
      buttons = ReadButtons();

      float increment = 1.0;
      if (buttons & BUTTON_SHIFT)
      {
        increment *= 10;
      }

      if (buttons & BUTTON_RIGHT)
      {
         opState = TUNE_I;
         return;
      }
      if (buttons & BUTTON_UP)
      {
         Kp += increment;
         delay(200);
      }
      if (buttons & BUTTON_DOWN)
      {
         Kp -= increment;
         delay(200);
      }

      lcd.setCursor(0,1);
      lcd.print(Kp);
      lcd.print(" ");
   }
}

// ************************************************
// Integral Tuning State
// UP/DOWN to change Ki
// RIGHT for Kd
// SHIFT for 10x tuning
// ************************************************
void TuneI()
{
   lcd.setBacklight(VIOLET);
   lcd.print(F("Set Ki"));

   uint8_t buttons = 0;
   while(true)
   {
      buttons = ReadButtons();

      float increment = 0.01;
      if (buttons & BUTTON_SHIFT)
      {
        increment *= 10;
      }

      if (buttons & BUTTON_RIGHT)
      {
         opState = TUNE_D;
         return;
      }
      if (buttons & BUTTON_UP)
      {
         Ki += increment;
         delay(200);
      }
      if (buttons & BUTTON_DOWN)
      {
         Ki -= increment;
         delay(200);
      }
      lcd.setCursor(0,1);
      lcd.print(Ki);
      lcd.print(" ");
   }
}

// ************************************************
// Derivative Tuning State
// UP/DOWN to change Kd
// RIGHT for Run
// SHIFT for 10x tuning
// ************************************************
void TuneD()
{
   lcd.setBacklight(VIOLET);
   lcd.print(F("Set Kd"));

   uint8_t buttons = 0;
   while(true)
   {
      buttons = ReadButtons();
      float increment = 0.01;
      if (buttons & BUTTON_SHIFT)
      {
        increment *= 10;
      }

      if (buttons & BUTTON_RIGHT)
      {
         opState = RUN;
         return;
      }
      if (buttons & BUTTON_UP)
      {
         Kd += increment;
         delay(200);
      }
      if (buttons & BUTTON_DOWN)
      {
         Kd -= increment;
         delay(200);
      }

      lcd.setCursor(0,1);
      lcd.print(Kd);
      lcd.print(" ");
   }
}

Final part of the code:

// ************************************************
// PID Control State
// SHIFT and RIGHT for autotune
// RIGHT - Setpoint
// ************************************************
void Run()
{
   SaveParameters();
   myPID.SetTunings(Kp,Ki,Kd);

   uint8_t buttons = 0;
   while(true)
   {

        lcd.setCursor(0,0);
        lcd.print(F(">"));
        lcd.print(int(Setpoint));
        lcd.print(F(" ... "));
        lcd.write(1);
        lcd.print(F("C"));
        lcd.print(F("    "));
      
      setBacklight();  // set backlight based on state

      buttons = ReadButtons();
 
      if ((buttons & BUTTON_SHIFT) 
         && (buttons & BUTTON_RIGHT) 
         && (abs(Input - Setpoint) < 20.0))  // Should be at steady-state
      {
         StartAutoTune();
      }

      else if (buttons & BUTTON_RIGHT)
      {
        opState = SETP; 
        return;
      }

    
      DoControl();

      lcd.setCursor(0,1);
      lcd.print(Input);
      lcd.write(1);
      lcd.print(F("C : "));
      
      float pct = map(Output, 0, WindowSize, 0, 1000);
      lcd.setCursor(10,1);
      lcd.print(F("      "));
      lcd.setCursor(10,1);
      lcd.print(pct/10,1);
      lcd.print(Output);
      lcd.print("%");

      
      delay(100);
   }
}


// ************************************************
// Execute the control loop
// ************************************************
void DoControl()
{
  // Read the input; this converts the reading voltage from the IR temperature sensor into degC input value
  int tempReading = analogRead(tempPin);
  float tempReadingVol = tempReading * (5.0 / 1023.0);
  Input = 140*tempReadingVol + 200.0;

  
  if (tuning) // run the auto-tuner
  {
     if (aTune.Runtime()) // returns 'true' when done
     {
        FinishAutoTune();
     }
  }
  else // Execute control algorithm
  {
     myPID.Compute();
  }
  
  // Time Proportional relay state is updated regularly via timer interrupt.
  onTime = Output; 
}

// ************************************************
// Called by ISR every 15ms to drive the output
// ************************************************
void DriveOutput()
{  
  long now = millis();
  // Set the output
  // "on time" is proportional to the PID output
  if(now - windowStartTime>WindowSize)
  { //time to shift the Relay Window
     windowStartTime += WindowSize;
  }
  if((onTime > 100) && (onTime > (now - windowStartTime)))
  {
     digitalWrite(RelayPin,HIGH);
  }
  else
  {
     digitalWrite(RelayPin,LOW);
  }
}

// ************************************************
// Set Backlight based on the state of control
// ************************************************
void setBacklight()
{
   if (tuning)
   {
      lcd.setBacklight(VIOLET); // Tuning Mode
   }
   
   else if (abs(Input - Setpoint) > 20.0)  
   {
      lcd.setBacklight(RED);  // High Alarm - off by more than 1 degree
   }
   else
   {
      lcd.setBacklight(GREEN);  // We're on target!
   }
}

// ************************************************
// Start the Auto-Tuning cycle
// ************************************************

void StartAutoTune()
{
   // REmember the mode we were in
   ATuneModeRemember = myPID.GetMode();

   // set up the auto-tune parameters
   aTune.SetNoiseBand(aTuneNoise);
   aTune.SetOutputStep(aTuneStep);
   aTune.SetLookbackSec((int)aTuneLookBack);
   tuning = true;
}

// ************************************************
// Return to normal control
// ************************************************
void FinishAutoTune()
{
   tuning = false;

   // Extract the auto-tune calculated parameters
   Kp = aTune.GetKp();
   Ki = aTune.GetKi();
   Kd = aTune.GetKd();

   // Re-tune the PID and revert to normal control mode
   myPID.SetTunings(Kp,Ki,Kd);
   myPID.SetMode(ATuneModeRemember);
   
   // Persist any changed parameters to EEPROM
   SaveParameters();
}

// ************************************************
// Check buttons and time-stamp the last press
// ************************************************
uint8_t ReadButtons()
{
  uint8_t buttons = lcd.readButtons();
  if (buttons != 0)
  {
    lastInput = millis();
  }
  return buttons;
}

// ************************************************
// Save any parameter changes to EEPROM
// ************************************************
void SaveParameters()
{
   if (Setpoint != EEPROM_readDouble(SpAddress))
   {
      EEPROM_writeDouble(SpAddress, int(Setpoint));
   }

   if (Kp != EEPROM_readDouble(KpAddress))
   {
      EEPROM_writeDouble(KpAddress, Kp);
   }
   if (Ki != EEPROM_readDouble(KiAddress))
   {
      EEPROM_writeDouble(KiAddress, Ki);
   }
   if (Kd != EEPROM_readDouble(KdAddress))
   {
      EEPROM_writeDouble(KdAddress, Kd);
   }

}

// ************************************************
// Load parameters from EEPROM
// ************************************************
void LoadParameters()
{
  // Load from EEPROM
   Setpoint = int(EEPROM_readDouble(SpAddress));
   Kp = EEPROM_readDouble(KpAddress);
   Ki = EEPROM_readDouble(KiAddress);
   Kd = EEPROM_readDouble(KdAddress);
   
   // Use defaults if EEPROM values are invalid

   if (isnan(Setpoint))
   {
     Setpoint = 200;
   }

   if (isnan(Kp))
   {
     Kp = 850;
   }
   if (isnan(Ki))
   {
     Ki = 0.5;
   }
   if (isnan(Kd))
   {
     Kd = 0.1;
   }  
 
}


// ************************************************
// Write floating point values to EEPROM
// ************************************************
void EEPROM_writeDouble(int address, double value)
{
   byte* p = (byte*)(void*)&value;
   for (int i = 0; i < sizeof(value); i++)
   {
      EEPROM.write(address++, *p++);
   }
}

// ************************************************
// Read floating point values from EEPROM
// ************************************************
double EEPROM_readDouble(int address)
{
   double value = 0.0;
   byte* p = (byte*)(void*)&value;
   for (int i = 0; i < sizeof(value); i++)
   {
      *p++ = EEPROM.read(address++);
   }
   return value;
}

There are many who swear by the use of canned software, a.k.a. libraries. While these are convenient, they are hardly ever the best solution. In the commercial world we are not permitted to use libraries that were not developed 'in-house' and as such need to deal with many of these issues from first principles. PID is one such function and not a very difficult one to implement. I would suggest that you look at it a bit closer and decide if maybe you'd be better off writing your own.

Regardless of your path, PID is still used (in spite of better/faster algorithms) primarily because it is 100% guaranteed to converge. This does not translate into a guarantee that it will 100% converge in your lifetime. The algorithm is very sensitive to the constants you use (Kp, Ki and Kd), with a strong emphasis on Kp. How quickly your out put settles and how well it deals with overshoot are going to depend on the (initial) settings. In your code you show Kp = 850, Ki = 0.5 and Kd = 0.1. The values for Ki and Kd are reasonable, but Kp seems a bit out of sorts. Generally when we test for new settings, we start with Kp = 1.0, Ki = 0.1 and Kd = 0.1 and move around from there. You might want to have a second look at lines 625-635. These values will work, but it might be your grandchildren that see the results.

DKWatson:
There are many who swear by the use of canned software, a.k.a. libraries. While these are convenient, they are hardly ever the best solution. In the commercial world we are not permitted to use libraries that were not developed 'in-house' and as such need to deal with many of these issues from first principles. PID is one such function and not a very difficult one to implement. I would suggest that you look at it a bit closer and decide if maybe you'd be better off writing your own.

Regardless of your path, PID is still used (in spite of better/faster algorithms) primarily because it is 100% guaranteed to converge. This does not translate into a guarantee that it will 100% converge in your lifetime. The algorithm is very sensitive to the constants you use (Kp, Ki and Kd), with a strong emphasis on Kp. How quickly your out put settles and how well it deals with overshoot are going to depend on the (initial) settings. In your code you show Kp = 850, Ki = 0.5 and Kd = 0.1. The values for Ki and Kd are reasonable, but Kp seems a bit out of sorts. Generally when we test for new settings, we start with Kp = 1.0, Ki = 0.1 and Kd = 0.1 and move around from there. You might want to have a second look at lines 625-635. These values will work, but it might be your grandchildren that see the results.

Thank you very much for your reply.

I took your advice and tried to run the system with lower Kp values (Kp=1.0 to 10.0), however it did not help much. The output still fluctuates +/-20 degC around the setpoint.

The autotune mode does not converge in a reasonable time (I did not observe it converging at all, so I stopped it after 5 min). many different autotune settings tested, no success.

I think that I need to take your advice and try to develop my own autotune code, which will not be easy considering that I am a NB in coding.

If you know any autotune code that I can use as the base for my code, it will be highly appreciated.

Thanks again for your time.

I’d have a look at how to tune PID loops - a fast responding loop may well need some derivative if the rate of heating is very fast .
Give some thought to your system too - if there is a lot of thermal mass for example after tuning heat off the measured temperature may continue to rise. Where and how you measure temperature has an effect too .

A fast system with a lot of lag may be inherently unstable anyway and controlling may not be the fault of the PID . But while you are playing move the P term over a very wide range .1 to 100 ? and note it’s effect - when it’s better when it’s worse - optimally tuned , if it’s a fast system you are likely to get over shoot, without it you are likely to heat up slowly . Try a bit of derivative next , integral deals with offsets .

Your code looks pretty complex , I think someone has already suggested writing your own PID loop, well worth a go . All the autotuners I’ve seen are rubbish by the way .