PID temp and humidity control with Arduino and SSR

5,121 views
Skip to first unread message

Paul

unread,
Jun 3, 2011, 2:03:13 PM6/3/11
to DIY PID Control
Hey Everybody,

I'm planning to use an Arduino with the PID library v1.0 to regulate
temperature and humidity in a spray booth. The setup will use a DHT11
temperature and humidity sensor to measure temp and humidity (this is
working very well). The output is via 3 solid state relays, one is
connected to a resistive heating element, the second to a ultrasonic
humidifier and the third is to a dehumidifier used for
dehumidification and cooling. The program that I am using works well
in a test setup when the PID refresh rate is 10sec or less, but I
don't want to kick the heating/cooling etc. elements on and off that
frequently. I would like to use a 1 - 2 min cycle, but when I do this,
the PID responds very slowly, if at all. Using a 10sec loop, the PID
ramps up the appropriate outputs over the course of a few min. Using a
60sec loop, the PID only ramps up by a fraction of a percent over more
than an hour. This is far more than 6x slower. I am confused about how
to proceed. I have tried making the PID parameters more aggressive,
but maybe I am not tuning it right. I would really appreciate any
input or suggestions.

Paul

unread,
Jun 3, 2011, 2:08:39 PM6/3/11
to DIY PID Control
here is the code if it helps

// Control program for PID control of spray booth thermal and humidity
control using DHT11 , with some modification DHT22 could be used for
higher sensing precision: Paul Verstegen May 2011

#define dht_dpin 6 //no ; here. Set equal to channel sensor is on,

byte bGlobalErr; //for passing error code back from complex functions.
byte dht_dat[4]; //Array to hold the bytes sent from sensor.
int tempF = 0; //holds C to F conversion result
float heatpower = 0; //holds heat power result from PID calculation
float coolpower = 0; //holds chiller power result from PID
calculation
float humidpower = 0; //holds humidifier power result from PID
calculation
float dehumidpower = 0; //holds dehumidifier power result from PID
calculation
float cooldehumidpower = 0; //holds which is greater value cool or
dehumid
int heatPin = 7; //digital pin to run heater SSR
int cooldehumidPin = 8; //digital pin to run chiller/dehumidifier SSR
int humidPin = 9; //digital pin to run humidifier SSR

float heatpercent;
float coolpercent;
float humidpercent;
float dehumidpercent;

#include <PID_v1.h> //include PID library

//Define Variables we'll be connecting to
double SetpointHeat, InputHeat, OutputHeat;
double SetpointCool, InputCool, OutputCool;
double SetpointHumid, InputHumid, OutputHumid;
double SetpointDehumid, InputDehumid, OutputDehumid;

//Specify the links and initial tuning parameters
PID heatPID(&InputHeat, &OutputHeat, &SetpointHeat,4,2,.5, DIRECT);
PID coolPID(&InputCool, &OutputCool, &SetpointCool,4,2,.5, REVERSE);
PID humidPID(&InputHumid, &OutputHumid, &SetpointHumid,4,2,.5,
DIRECT);
PID dehumidPID(&InputDehumid, &OutputDehumid, &SetpointDehumid,4,2,.5,
REVERSE);

float WindowSize = 60000; //this sets time factor of loop in msec
unsigned long WindowStartTime;
unsigned long ComputeStart;
unsigned long ComputeEnd;
unsigned long ComputeTime;

void setup(){
heatPID.SetSampleTime(WindowSize);
coolPID.SetSampleTime(WindowSize);
humidPID.SetSampleTime(WindowSize);
dehumidPID.SetSampleTime(WindowSize);
heatPID.SetOutputLimits(0, WindowSize);
coolPID.SetOutputLimits(0, WindowSize);
humidPID.SetOutputLimits(0, WindowSize);
dehumidPID.SetOutputLimits(0, WindowSize);
//set output pins for SSR control of heater, humidifier, and
dehumidifier/chiller
pinMode(heatPin, OUTPUT);
pinMode(cooldehumidPin,OUTPUT);
pinMode(humidPin,OUTPUT);

SetpointHeat = 25; //sets temperature setpoint in degrees celcius
(make adjustable in final version)
SetpointCool = SetpointHeat + 2; //sets cool to kick in with a 2
degree "dead band" to eliminate oscillation
SetpointHumid = 60; //sets humidity setpoint to 60% humidity (make
adjustable in final version)
SetpointDehumid = SetpointHumid + 2; //sets dehumidifier to kick in
with a 2% "dead band" to eliminate oscillation

//turn the PID on assuming ambient conditions will likely require
steady state in a heating and humidifing condition
heatPID.SetMode(AUTOMATIC);
coolPID.SetMode(MANUAL);
humidPID.SetMode(AUTOMATIC);
dehumidPID.SetMode(MANUAL);

InitDHT();//Does what's necessary to prepare for reading DHT sensor
Serial.begin(9600);
delay(1000);//Let system settle
Serial.println("Humidity and temperature\n\n");
delay(2000); //time to read the message
}//end "setup()"

void loop(){

ComputeStart = millis();
ReadDHT();//This is the "heart" of the program.
//Fills global array dht_dpin[], and bGlobalErr, which
//will hold zero if ReadDHT went okay.
//Must call InitDHT once (in "setup()" is usual) before
//calling ReadDHT.
//Following: Displays what was seen...
switch (bGlobalErr){
case 0:
Serial.print("humidity= ");
Serial.print(dht_dat[0], DEC);
Serial.print(".");
Serial.print(dht_dat[1], DEC);
Serial.print("% ");
Serial.print("temp= ");
tempF = (((1.8)*dht_dat[2])+32); //c to f conversion
Serial.print(tempF, DEC);
//Serial.print(".");
//Serial.print(dht_dat[3], DEC);
Serial.print("F ");
Serial.print(dht_dat[2], DEC);
//Serial.print(".");
//Serial.print(dht_dat[3], DEC);
Serial.print("C ");

if (dht_dat[0] < SetpointDehumid){
humidPID.SetMode(AUTOMATIC);
dehumidPID.SetMode(MANUAL);
OutputDehumid = 0;
InputHumid = dht_dat[0];
humidPID.Compute();
humidpower = OutputHumid;
dehumidpower = OutputDehumid;
}
if (dht_dat[0] >= SetpointDehumid){
humidPID.SetMode(MANUAL);
dehumidPID.SetMode(AUTOMATIC);
OutputHumid = 0;
InputDehumid = dht_dat[0];
dehumidPID.Compute();
dehumidpower = OutputDehumid;
humidpower = OutputHumid;
}
if (dht_dat[2] < SetpointCool){
heatPID.SetMode(AUTOMATIC);
coolPID.SetMode(MANUAL);
OutputCool = 0;
InputHeat = dht_dat[2];
heatPID.Compute();
heatpower = OutputHeat;
coolpower = OutputCool;
}
if (dht_dat[2] >= SetpointCool){
heatPID.SetMode(MANUAL);
coolPID.SetMode(AUTOMATIC);
OutputHeat = 0;
InputCool = dht_dat[2];
coolPID.Compute();
coolpower = OutputCool;
heatpower = OutputHeat;
}

Serial.print("Heat: ");
heatpercent = (heatpower/WindowSize)*100;
Serial.print(heatpercent);
Serial.print("% ");
Serial.print("Cool: ");
coolpercent = (coolpower/WindowSize)*100;
Serial.print(coolpercent);
Serial.print("% ");
Serial.print("Humid: ");
humidpercent = (humidpower/WindowSize)*100;
Serial.print(humidpercent);
Serial.print("% ");
dehumidpercent = (dehumidpower/WindowSize)*100;
Serial.print("Dehumid: ");
Serial.print(dehumidpercent);
Serial.println("% ");

if(coolpower >= dehumidpower){
cooldehumidpower = coolpower;
}
if(dehumidpower >= coolpower){
cooldehumidpower = dehumidpower;
}

ComputeEnd = millis();
ComputeTime = ComputeEnd - ComputeStart;
Serial.print(ComputeTime);
Serial.println(" msec.");

WindowStartTime = millis();
while ((millis()-WindowStartTime) < WindowSize){ //while the
operating window time is less than the window size, the appropriate
pins are turned on or shut off
if (heatpower > (millis()-WindowStartTime)){ //for a
percentage of the window time determined by the PID
digitalWrite(heatPin,HIGH);
}
if (heatpower <= (millis()-WindowStartTime)){
digitalWrite(heatPin,LOW);
}
if (humidpower > (millis()-WindowStartTime)){
digitalWrite(humidPin,HIGH);
}
if (humidpower <= (millis()-WindowStartTime)){
digitalWrite(humidPin,LOW);
}
if (cooldehumidpower > (millis()-WindowStartTime)){
digitalWrite(cooldehumidPin,HIGH);
}
if (cooldehumidpower <= (millis()-WindowStartTime)){
digitalWrite(cooldehumidPin,LOW);
}

}



break;
case 1:
Serial.println("Error 1: DHT start condition 1 not met.");
delay(1000);
break;
case 2:
Serial.println("Error 2: DHT start condition 2 not met.");
delay(1000);
break;
case 3:
Serial.println("Error 3: DHT checksum error.");
delay(1000);
break;
default:
Serial.println("Error: Unrecognized code encountered.");
delay(1000);
break;
}//end "switch"
delay(1000);//Don't try to access too frequently... in theory
//should be once per two seconds, fastest,
//but seems to work after 0.8 second.
}// end loop()

/*Below here: Only "black box" elements which can just be plugged
unchanged into programs. Provide InitDHT() and ReadDHT(), and a
function
one of them uses.*/

void InitDHT(){
pinMode(dht_dpin,OUTPUT);
digitalWrite(dht_dpin,HIGH);
}//end InitDHT

void ReadDHT(){
/*Uses global variables dht_dat[0-4], and bGlobalErr to pass
"answer" back. bGlobalErr=0 if read went okay.
Depends on global dht_dpin for where to look for sensor.*/
bGlobalErr=0;
byte dht_in;
byte i;
// Send "start read and report" command to sensor....
// First: pull-down I/O pin for 23000us
digitalWrite(dht_dpin,LOW);
delay(23);
/*aosong.com datasheet for DHT22 says pin should be low at least
500us. I infer it can be low longer without any]
penalty apart from making "read sensor" process take
longer. */
//Next line: Brings line high again,
// second step in giving "start read..." command
digitalWrite(dht_dpin,HIGH);
delayMicroseconds(30);//DHT22 datasheet says host should
//keep line high 20-40us, then watch for sensor taking line
//low. That low should last 80us. Acknowledges "start read
//and report" command.

//Next: Change Arduino pin to an input, to
//watch for the 80us low explained a moment ago.
pinMode(dht_dpin,INPUT);
delayMicroseconds(40);

dht_in=digitalRead(dht_dpin);

if(dht_in){
bGlobalErr=1;//dht start condition 1 not met
return;
}//end "if..."
delayMicroseconds(80);

dht_in=digitalRead(dht_dpin);

if(!dht_in){
bGlobalErr=2;//dht start condition 2 not met
return;
}//end "if..."

/*After 80us low, the line should be taken high for 80us by the
sensor. The low following that high is the start of the first
bit of the forty to come. The routine "read_dht_dat()"
expects to be called with the system already into this low.*/
delayMicroseconds(80);
//now ready for data reception... pick up the 5 bytes coming from
// the sensor
for (i=0; i<5; i++)
dht_dat[i] = read_dht_dat();

//Next: restore pin to output duties
pinMode(dht_dpin,OUTPUT);

//Next: Make data line high again, as output from Arduino
digitalWrite(dht_dpin,HIGH);

//Next see if data received consistent with checksum received
byte dht_check_sum =
dht_dat[0]+dht_dat[1]+dht_dat[2]+dht_dat[3];
/*Condition in following "if" says "if fifth byte from sensor
not the same as the sum of the first four..."*/
if(dht_dat[4]!= dht_check_sum)
{bGlobalErr=3;}//DHT checksum error
};//end ReadDHT()

byte read_dht_dat(){
//Collect 8 bits from datastream, return them interpreted
//as a byte. I.e. if 0000.0101 is sent, return decimal 5.

//Code expects the system to have recently entered the
//dataline low condition at the start of every data bit's
//transmission BEFORE this function is called.

byte i = 0;
byte result=0;
for(i=0; i< 8; i++){
//We enter this during the first start bit (low for 50uS) of the
byte
//Next: wait until pin goes high
while(digitalRead(dht_dpin)==LOW);
//signalling end of start of bit's transmission.

//Dataline will now stay high for 27 or 70 uS, depending on
//whether a 0 or a 1 is being sent, respectively.
delayMicroseconds(30);//AFTER pin is high, wait further period,
to be
//into the part of the timing diagram where a 0 or a 1 denotes
//the datum being send. The "further period" was 30uS in the
software
//that this has been created from. I believe that a higher
number
//(45?) might be more appropriate.

//Next: Wait while pin still high
if (digitalRead(dht_dpin)==HIGH)
result |=(1<<(7-i));// "add" (not just addition) the 1
//to the growing byte
//Next wait until pin goes low again, which signals the START
//of the NEXT bit's transmission.
while (digitalRead(dht_dpin)==HIGH);
}//end of "for.."
return result;
}//end of "read_dht_dat()"

Brett Beauregard

unread,
Jun 3, 2011, 2:45:17 PM6/3/11
to diy-pid...@googlegroups.com
Hi Paul, 

first of all, it's really exciting to see multiple PIDs in the same program.  exactly what I had in mind when I wrote the library!

without looking at the complete program I can only say this in regards to windowSize:  if you make your windowSize X times bigger you will need to increase your tuning parameters (all 3 of them) by that same factor to get similar performance.  this is because 100% is now X times farther away from 0%.  if you want your controller to make the same size moves in a percentage sense, it will now need to be X times more aggressive.  this might not be precisely true due to some nonlinear effects in your equipment, but it's a good place to start.

another thing I noticed.  using separate pid controllers for heating and cooling can sometimes lead to fighting. I have seen situations where both PIDs are asking for 100%.  not very efficient!  if you notice this problem you may wat to try the following: have 1 controller do both heating and cooling.  this will ensure that only one is on at a time.

void setup()
{
...
tempPID.SetOutputLimits(-WindowSize, WindowSize); //cooling range = -WindowSize->0,  heating range = 0->WindowSize
...
}

void :loop()
{
...
tempPID.Compute();
if(tempOutput<0)
{
   //we want to cool
   HeatOutput=0;
   CoolOutput = -1*tempOutput;
}
else //tempOutput>=0
{
   //we want to heat
  HeatOutput=tempOutput;
  CoolOutput=0;
--
Brett

Paul

unread,
Jun 3, 2011, 6:05:18 PM6/3/11
to DIY PID Control
Thanks for the reply! I have to be honest, I'm quite humbled that the
author of the library responded to my post! Thanks for writing this
library by the way. It's excellent to be able to utilize industry
level algorithms with this much ease. I have the heat/cooling and
humidification/dehumidification hopefully not fighting each other by
if below setpoint heat is set to automatic and cool is set to manual.
I was hoping this would work well.

if (dht_dat[0] < SetpointDehumid){
humidPID.SetMode(AUTOMATIC);
dehumidPID.SetMode(MANUAL);
OutputDehumid = 0;
InputHumid = dht_dat[0];
humidPID.Compute();
humidpower = OutputHumid;
dehumidpower = OutputDehumid;
}
if (dht_dat[0] >= SetpointDehumid){
humidPID.SetMode(MANUAL);
dehumidPID.SetMode(AUTOMATIC);
OutputHumid = 0;
InputDehumid = dht_dat[0];
dehumidPID.Compute();
dehumidpower = OutputDehumid;
humidpower = OutputHumid;
}

I also made the setpoint for the cooling and dehumidification a bit
higher than the actual "target setpoint" to give the positive PID
loops a chance to "throttle down" before the cooling/dehumidifing
loops kick in. I'll look at doing your way though, that seems cleaner.
Initially I used the settings 2,5,1 for all of the PIDs. So, I would
multiply all of these by 6x or so to see a more aggressive response
for the window size? 12,30,6 for example? I really appreciate your
help with this!

Paul

Paul

unread,
Jun 3, 2011, 6:09:04 PM6/3/11
to DIY PID Control
whoops! Also, is it appropriate to use SetSampleTime(WindowSize); to
let the PID "know" about how frequently compute will be called? Or is
this pointless?

Brett Beauregard

unread,
Jun 3, 2011, 6:57:06 PM6/3/11
to diy-pid...@googlegroups.com

As far as the tunings adjustment goes, yep that's what I was suggesting: 12,30,6 as a new starting point.

I didn't see that bit of setpoint logic in my skimming.  That would defnitely keep them from fighting.  There are times
however, where I think you might want to dehumidify even when below setpoint.  If you're rising to setpoint too quickly for example.  I don't think this logic would allow for that.  Still.  If it works, go for it!

with sample time, I think evaluating the pid every 60 seconds might be a little slow, but I can't say that definitively.  maybe every 10 seconds?

-Brett
--
Brett

Paul

unread,
Jun 5, 2011, 11:54:39 PM6/5/11
to DIY PID Control
Brett

Thanks for the guidance. I also added a call to compute each PID to
the "activity loop" this greatly improved the responsiveness of the
system. There is some serious fine tuning to do once the system is
installed and I can get a feeling of how it actually behaves, but at
least now I have a good start. I'll keep you posted on the progress!

Paul

loli LITA

unread,
Jan 27, 2023, 3:27:27 AM1/27/23
to DIY PID Control
HI PAUL WHY AM I WRITING ERROR3 CHECKSUM ERROR
WRITE ME HOW TO CONTACT YOU?


пятница, 3 июня 2011 г. в 22:08:39 UTC+4, Paul:
Reply all
Reply to author
Forward
0 new messages