PID control of an AC heater

Hello everyone!

I am trying to control the temperature in a small volume as accurately as possible. I am using an AC heater (McMaster PN 20055K112) on a solid-state relay controlled by the Arduino. Temperature in the chamber is provided by a MAX6675. The fan is always on and the Arduino PID Library is controlling the SSR.

I get the feeling that I am on the right track since the heater relay is cycling on and off while the heater is warming up (there is a LED hooked to the relay). Although, the heater relay stays on when I reach my set temperature and continues to cycle on and off after I am past (higher than) my set temp.

I am still a bit fuzzy on how the PID library works. I used code from the relay example contained in the PID library. I have included my code below. I included the just main loop, but I will see if the whole code can be posted in a reply (size limitations). PID action is in the "else { // (PIDCycle == true)" loop

I am kinda new to the game, so the code very well may have unnecessary redundancies, pointless loops, and just a general bad form (off topic - any secrets to getting the most out of my coding hours besides practice? books, sites, exercises, etc?). Also, if there is a better place in the forum for this topic, let me know. Thanks everyone!

    else { //  (PIDCycle == true) 
      digitalWrite(FanRelayPin, HIGH); // fan is on
      Serial.println("asdfsadfsa"); // lets me know i'm running this loop /debug
      Input = thermocouple.readCelsius(); // start PIDCycle
      myPID.Compute();
      
      unsigned long now = millis();
      if (now - windowStartTime > WindowSize) {
        windowStartTime += WindowSize; //time to shift the Relay Window
      }
      if(Output > now - windowStartTime) {
        digitalWrite(HeaterRelayPin,HIGH);
      }
      else {
        digitalWrite(HeaterRelayPin,LOW);
      }
      
      if (thermocouple.readCelsius() < ChamberSetTemp - 1.0 && StartTimer == 0) { // minus 2 degrees so that we start counting when we are within two degrees
        //lcd.clear(); // clears the screen completely
        lcd.setCursor(0, 0); // places cursor at beginning of first line
        lcd.print("Heating Chamber"); // message to user when the unit is in a standby state (top row)
        lcd.setCursor(0,1); // sets the cursor at the first space of the second row
        lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        
        Time3 = millis(); // time that we start the 3 minute timer
        Time5 = Time3; // 
      }
      else { // were at temp, so start timing the heat time
      StartTimer = 1;
      }
      
      if(StartTimer == 1) {
        if (TickTock = false) { // TICK
          lcd.clear(); // clears the screen completely
          lcd.setCursor(0, 0); // places cursor at beginning of first line
          lcd.print("Time Rem "); lcd.print(TR); // time remaining (top row)
          lcd.setCursor(0,1); // sets the cursor at the first space of the second row
          lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        }
        else { // TOCK
          lcd.clear(); // clears the screen completely
          lcd.setCursor(0, 0); // places cursor at beginning of first line
          lcd.print("Heating Specimen"); // Heating Specimen (top row)
          lcd.setCursor(0,1); // sets the cursor at the first space of the second row
          lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        }
      
        BeenHeating = millis() - Time3;  
        TR = SpecimenHeatTime - BeenHeating; // 
        Serial.println(TR); Serial.println(SpecimenHeatTime); Serial.println(BeenHeating);
        
        
        if (millis() - Time5 > DisplaySwitchTime) {
          TickTock != TickTock; // switch the display
          Time5 = millis(); // 
        }
        if (TR <= 0) { // the heating cycle is over and we need to completely reset the device
          StartTimer = 0; // reset the heater timer
          HeatCycle = false;
          PrimeCycle = false;
          PIDCycle = false;
          digitalWrite(HeaterRelayPin, LOW); // turn off the heater
          lcd.clear(); lcd.print("COOLING"); // goodbye
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          
          delay(16000); // wait 5 seconds
          digitalWrite(FanRelayPin, LOW); // turn off the fan
        }
      }
    }
  }
}

Here is the first half of the entire code

// Brian Leach Dec 28 2011

// LIBRARIES
#include <Bounce.h> // debouncing
#include <LiquidCrystal.h> // LCD library
#include <max6675.h> // thermocouple amplifier library
#include <PID_v1.h> // PID library
#include <Wire.h> // ????????

// VARIABLES

int HeatCycle = 0; // variable for heat cycle
int PrimeCycle = 0; // variable for heater priming cycle
int PIDCycle = 0; //
float ChamberSetTemp = 37; // --> 37°C <-- = 98.6°F
int HeaterPrimeTime = 1*1000; // amount of time that the heater runs before the fan kicks on (3 seconds)

boolean wakeUP; // runs the initialize sequence once

unsigned long WakeUpTime = 4*1000; // seconds to wait on initial setup Time1
unsigned long Time2;
unsigned long Time3;
unsigned long Time4;
unsigned long Time5;

unsigned long SpecimenHeatTime = 30*1000; // seconds remaining in the maintain heat mode (300 = 5 minutes)
long TR; // time remaining
unsigned long BeenHeating = 0;
int StartTimer = 0; // switch for llopp

// INPUTS
int StartButtPin = 3; // pin for momentary start button
// start button debounce variables
int StartButtReading = HIGH; // initially HIGH for "not pushed"
int buttonState = HIGH;           // the current reading from the input pin
int lastButtonState = HIGH;   // the previous reading from the input pin. HIGH because of pullup resistor

// the following variables are long's because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.

Bounce StartButton = Bounce( StartButtPin, 50); // initializes switch pin
int pushed; // for debounce

// OUTPUTS
int StartLEDPin = 4; //pin assignment for button LED 

// temperature sensor (K-type thermocouple)
int thermoDO = 5; // thermocouple pin
int thermoCS = 6; // thermocouple pin
int thermoCLK = 7; // thermocouple pin
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
//int vccPin_t = 3; // t is for temperature. Vcc is for Voltage common connector (+ power). Provided by Bb rail
//int gndPin_t = 2; // ground pin for max6675. Provided at Bb rail
float CurrentTempC; // current temperature in deg C

// heater and fan
int HeaterRelayPin = 11; // heater relay pin
int FanRelayPin = 2; // fan relay pin

// display info
LiquidCrystal lcd(0); // initialize the LCD library
const int numRows = 2; // number of rows in the matrix
const int numCols = 16; // number of columns in the matrix
String Display1; // top line of LCD
String Display2; // bottom line of LCD
uint8_t degree[8]  = {140,146,146,140,128,128,128,128}; // make a cute degree symbol
int DisplaySwitchTime = 5*1000; // amount of sec LCD dwells on "time remaining" and "temperature" display
boolean TickTock = false; // switches between "time remaining" and "temperature" display in the "maintain heat" mode

// PID
double Setpoint, Input, Output; //Define Variables we'll be connecting to
PID myPID(&Input, &Output, &Setpoint,55,75,110, DIRECT); //Specify the links and initial tuning parameters**********************************
int WindowSize = 5000;
unsigned long windowStartTime;

void setup() {
  
  
  // Start Button
  pinMode(StartButtPin, INPUT); // digital
  pinMode(StartLEDPin, OUTPUT); digitalWrite(StartLEDPin, LOW); // sets the digital pin as output
  pushed = StartButton.read();
  
  // LCD Matrix
  Serial.begin(9600); // activate serial
  lcd.begin(numCols, numRows); // set up the LCD's number of columns and rows: 
  lcd.createChar(0, degree); // activate the degree symbol in lcd custom spot #0
  //lcd.print("Centeno-Schultz"); // Print welcome message to the LCD.
  wakeUP = true; // initialize
  //delay(WakeUpTime);  // allow max6675 to stabilize, display welcome message.
  //pinMode(vccPin, OUTPUT); digitalWrite(vccPin, HIGH); gives 5 V power --> provided at Bb rail
  //pinMode(gndPin, OUTPUT); digitalWrite(gndPin, LOW); gives ground --> provided at Bb rail
  
  // PID
  Setpoint = ChamberSetTemp; //initialize the variable. 37°C 
  myPID.SetOutputLimits(0, WindowSize); //tell the PID to range between 0 and the full window size 
  myPID.SetMode(AUTOMATIC); //turn the PID on
  PIDCycle = 0; // PID Cycle
  
  // Heater-fan
  pinMode(HeaterRelayPin, OUTPUT);
  pinMode(FanRelayPin, OUTPUT);
   
// Process
TR = SpecimenHeatTime; // time remaining in specimen heat time
  
}

Second half of entire code

void loop() {
  
  if (wakeUP == true) {
    lcd.print("Centeno-Schultz"); // Print welcome message to the LCD.
    delay(WakeUpTime);  // allow max6675 to stabilize, display welcome message.
    wakeUP = false;
  } 

 // start button stuff ***********************************%%%%%%%%%%%%%%%%%%%%%%%%%(((((((((((((((((((((

  StartButton.update();
  
//  StartButtReading = digitalRead(StartButtPin); // LOW is pushed, HIGH is not pushed
  if (StartButton.read() != pushed && StartButton.duration() > 50 ) {

    pushed = StartButton.read();

    if ( !pushed ) {

      Serial.println( "Waking Up!" );
      // Do Something Here...
      
      HeatCycle = 1;
    }
    
    else {

      Serial.println( "Falling Asleep" );
      // Do Something Here...
      lcd.clear();

    }
  }

//   ***********************************%%%%%%%%%%%%%%%%%%%%%%%%%(((((((((((((((((((((((((((((
   
  if (HeatCycle == 0) { // if the button has not been pressed, just loop
    digitalWrite(StartLEDPin, HIGH); // blue ring around start button is on
 
    // print messages on the screen
    //lcd.clear(); // clears the screen completely
    lcd.setCursor(0, 0); // places cursor at beginning of first line
    lcd.print("LOAD - PRESS"); // message to user when the unit is in a standby state (top row)
    lcd.setCursor(0,1); // sets the cursor at the first space of the second row
    lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
    delay(1); // 5 ms wait that hopefully saves the LCD screen
    
  }
  else if (HeatCycle == 1) { // if the start button HAS been pressed ************************************
  digitalWrite(StartLEDPin, LOW); // Turn off blue ring around start button
  //lcd.clear(); // clears the screen completely
    if (PIDCycle == 0) { // If it's not PID time, just chill
      if (thermocouple.readCelsius() < ChamberSetTemp) // temp is low
      {
        // start heater with no fan
        if (PrimeCycle == 0) {
          Time2 = millis(); // get Time2 
        }
        PrimeCycle = 1;
        digitalWrite(HeaterRelayPin, HIGH); // run the heater
      }
      else if (thermocouple.readCelsius() > ChamberSetTemp) { // temp is high, start PID
        PrimeCycle = 0;
        PIDCycle = 1;
        windowStartTime = millis();  
      }
      if (PrimeCycle == 1) { //  && millis() - Time2 < HeaterPrimeTime) // needs priming and time is still good
        if (millis() - Time2 > HeaterPrimeTime) { // needs priming and time has run out
          PrimeCycle = 0;
          PIDCycle = 1; // start the pidCycle 
          windowStartTime = millis();
        }
      } 
    }
    else { //  (PIDCycle == true) 
      digitalWrite(FanRelayPin, HIGH); // fan is on
      Serial.println("asdfsadfsa");
      Input = thermocouple.readCelsius(); // start PIDCycle
      myPID.Compute();
      
      unsigned long now = millis();
      if (now - windowStartTime > WindowSize) {
        windowStartTime += WindowSize; //time to shift the Relay Window
      }
      if(Output > now - windowStartTime) {
        digitalWrite(HeaterRelayPin,HIGH);
      }
      else {
        digitalWrite(HeaterRelayPin,LOW);
      }
      
      if (thermocouple.readCelsius() < ChamberSetTemp - 1.0 && StartTimer == 0) { // minus 2 degrees so that we start counting when we are within two degrees
        //lcd.clear(); // clears the screen completely
        lcd.setCursor(0, 0); // places cursor at beginning of first line
        lcd.print("Heating Chamber"); // message to user when the unit is in a standby state (top row)
        lcd.setCursor(0,1); // sets the cursor at the first space of the second row
        lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        
        Time3 = millis(); // time that we start the 3 minute timer
        Time5 = Time3; // 
      }
      else { // were at temp, so start timing the heat time
      StartTimer = 1;
      }
      
      if(StartTimer == 1) {
        if (TickTock = false) { // TICK
          lcd.clear(); // clears the screen completely
          lcd.setCursor(0, 0); // places cursor at beginning of first line
          lcd.print("Time Rem "); lcd.print(TR); // time remaining (top row)
          lcd.setCursor(0,1); // sets the cursor at the first space of the second row
          lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        }
        else { // TOCK
          lcd.clear(); // clears the screen completely
          lcd.setCursor(0, 0); // places cursor at beginning of first line
          lcd.print("Heating Specimen"); // Heating Specimen (top row)
          lcd.setCursor(0,1); // sets the cursor at the first space of the second row
          lcd.print("Temp "); lcd.print(thermocouple.readCelsius()); lcd.write(0); // message to user when the unit is in a standby state (bottom row)
        }
      
        BeenHeating = millis() - Time3;  
        TR = SpecimenHeatTime - BeenHeating; // 
        Serial.println(TR); Serial.println(SpecimenHeatTime); Serial.println(BeenHeating);
        
        
        if (millis() - Time5 > DisplaySwitchTime) {
          TickTock != TickTock; // switch the display
          Time5 = millis(); // 
        }
        if (TR <= 0) { // the heating cycle is over and we need to completely reset the device
          StartTimer = 0; // reset the heater timer
          HeatCycle = false;
          PrimeCycle = false;
          PIDCycle = false;
          digitalWrite(HeaterRelayPin, LOW); // turn off the heater
          lcd.clear(); lcd.print("COOLING"); // goodbye
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          lcd.setBacklight(LOW);
          delay(500);
          lcd.setBacklight(HIGH);
          delay(500);
          
          delay(16000); // wait 5 seconds
          digitalWrite(FanRelayPin, LOW); // turn off the fan
        }
      }
    }
  }
}

Although, the heater relay stays on when I reach my set temperature and continues to cycle on and off after I am past (higher than) my set temp.

Does that mean you want no over-shoot?

The amount of overshoot I was seeing was too much. Minimal percent overshoot is more important than transient response. definitely a design goal.

I do not clearly understand the three tuning parameters of the PID program. I tried changing a single number at a time and i couldn't get a feel for how things were affected. I think learning a bit of processing so i could better visualize the inner workings would be good...

PID myPID(&Input, &Output, &Setpoint,55,75,110, DIRECT); //Specify the links and initial tuning parameters**********************************

Start with just P. No D. No I.

Somewhere in this forum was a post about tuning... There it is. Give this a look...
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1280251311

How I found it...
http://www.google.com/search?q=arduino+pid+tuning

And in simple terms (excuse the pun) :

P or proportional control term gives a corrective action which is Proportional to the degree of error between the desired setpoint and the measured variable. Too high a proportional action will lead to oscillation, too low will produce a sluggish response

D or differential control term gives a corrective action which is a measure of the Differential rate that the measured variable approaches the setpoint. In other words if the measured variable is slowly reacting to the error the D action is great and if its reacting fast the D action is small (or nil)

I or integral control term gives a corrective action based upon how long the error exists ie the Integral of error versus time. With a large error, and a sluggish response, the effect of integral action will continually increase and can often be seen as the output ramping to saturation.