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
}
}
}
}
}
// 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
}
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
}
}
}
}
}
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**********************************
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.