PID control humidifier - library example code does not work

I’m trying to control a humidifier using a relay shield and a RH sensor (BME280). A simple on-off type control is not sufficient for my experiment and I want to use PID control. I used the example code provided on the GitHub page but that doesn’t work. I also tried out the variation that was suggested by another user (found here) with the same result.

I’ve attached my code - I’ve tried to print the output of myPID.Compute() function - it is always 0. If I try to implement a simple on-off control using if-else statements - that works as expected, so the problem is not the circuit/relay/humidifier/sensor.

Any help is appreciated.

bme_humidifier_pid.ino (3.09 KB)

gmarwaha:
A simple on-off type control is not sufficient for my experiment and I want to use PID control.

Why? Isn't a humidifier either "on" or "off"? Seems like a Bang-Bang Controller with appropriate hysteresis is just the thing. Certainly works well for your house's HVAC system.

What makes you think the function returns the output value? Isn't there an output variable for that?
From the examples:

void loop()
{
  Input = analogRead(PIN_INPUT);
  myPID.Compute();
  analogWrite(PIN_OUTPUT, Output);
}

You didn't read the documentation and/or examples enough.
Look at the definition of the function. What type does it return?

bool PID::Compute()

gmarwaha:
I used the example code provided on the GitHub page but that doesn't work.

"doesn't work" in what way?

@gfvalvo
An on-off control would be fine for a house hvac (perhaps with a range instead of a single setpoint) but I'm not using it for that purpose. I'm running an experiment to evaluate material properties in different conditions (incl. temperature, humidity, corrosive chemicals etc.) and would like to control it quickly and efficiently since the properties I'm investigating are also time dependant. An on-off control oscillates too much around the setpoint (for my equipment anyway) - which I'd like to avoid with PID control.

@aarg
I know that the output of PID.Compute() would be a boolean and assigned to the variable Output as declared in the preamble. From what I understand, it is then being mapped on to a 0-5000ms (windowSize) range to implement a slow PWM. But the output is always zero which seems suspect. I've re-read the examples a couple of times but couldn't figure it out. Tried printing the calculated values at each step to see what's happening but that didn't get me far either.

@johnwasser
It does not work in the sense that the output (which I'm constantly monitoring) on the serial monitor does not change - regardless of where the input lies wrt the setpoint.

Try logging the input, setpoint and output and posting it here?

gmarwaha:
An on-off control oscillates too much around the setpoint (for my equipment anyway) - which I'd like to avoid with PID control.

But that's how the PID RelayOutput works anyway. Did you read the comments at the top of the code?

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

  • PID RelayOutput Example
  • Same as basic example, except that this time, the output
  • is going to a digital pin which (we presume) is controlling
  • a relay. the pid is designed to Output an analog value,
    * but the relay can only be On/Off.
  • to connect them together we use "time proportioning
  • control" it's essentially a really slow version of PWM.
  • first we decide on a window size (5000mS say.) we then
  • set the pid to adjust its output between 0 and that window
  • size. lastly, we add some logic that translates the PID
    * output into "Relay On Time" with the remainder of the
    * window being "Relay Off Time"
    ********************************************************/

I am familiar with how PIDs work - when the coefficients are properly selected, they should approximate the desired behaviour. The on-off control overshoots by a huge margin and isn’t able to stabilize which is what I want to get rid of. Any form of PWM for controlling components that only have two states but we want a certain kind of output over a certain period of time. If the output does not change regardless of whethere input < setpoint or input > setpoint, isn’t there something amiss? I’ve used both DIRECT and REVERSE to see if that makes a difference but it doesnt.

Here’s a screenshot of the serial monitor. The humidity value (input) changes goes both over and under the setpoint with no change in output.

gmarwaha:
@johnwasser
It does not work in the sense that the output (which I’m constantly monitoring) on the serial monitor does not change - regardless of where the input lies wrt the setpoint.

Were you also displaying Input and Setpoint so you could see that they were what you thought they were?

I assume you modified the example to use your humidity reading rather than the raw analog input. Perhaps you made a mistake in that modification. If you could show your modified example we might be able to help get it running.

gmarwaha:
Here's a screenshot of the serial monitor. The humidity value (input) changes goes both over and under the setpoint with no change in output.

The PID "Output" is a float. Why is it showing as an integer?

Not sure why it’s always 0.
The Input is set to the corrected value of relative humidity in the code (Input = corrRH - Line 65)I printed values of both corrRH and Input in the serial monitor and they are the same.

You have no hysteresis in your on-off implementation. No wonder you think it "oscillates" more than you would like.

As I said in Reply #1, you have to include Hysteresis.
It’s the resulting dead zone that prevents excessive “oscillations”.

  if (corrRH < Setpoint - hysteresis)
  {
    digitalWrite(relay_pin, HIGH);
  }
  else if (corrRH > Setpoint + hysteresis)
  {
    digitalWrite(relay_pin, LOW);
  } else
  {
    // Dead Zone. Do Nothing!!!
  }

Agreed. I did suggest the same (see reply #4 - using a range instead of a setpoint). However, why does the PID output not change - can you help me figure that out.

  myPID.Compute(); // Call the PID compute function


  Serial.println(myPID.Compute()); // Trying to find out what the output is?

That explains why the last data field shows as ‘0’. You are not displaying 'Output" (a float) but the return value from ‘myPID.Compute()’ (a boolean). The .Compute() method returns ‘false’ (0) when it has been less than 100 milliseconds since the last time ‘.Compute()’ was called, and since it was JUST called it will always return ‘false’ (0).

Try this code:

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <PID_v1.h>


#define BME_SCK 47
#define BME_MISO 45 // same as SDO in schematic 
#define BME_MOSI 43 // same as SDI in schematic 
#define BME_CS 41


// software SPI setup
Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);




// Relay 1 used to control the humidifier - used in normally open (NO) mode
// which means when it is set to 'HIGH', the humidifier will turn on
const byte RelayPin = 7;


double Setpoint, Input, Output; // Variable names for PID


// PID coefficients
double Kp = 2;  // Proportional: Multiplier of raw error (tune first)
double Ki = 0;  // Integral: Multiplier of sum of error over time (tune second)
double Kd = 0;  // Differential: Multiplier of rate that error is changing (tune third)


// Creat an instance of PID
// Since Humidity goes up as Output goes up, use DIRECT
// (For a dehumidifier we would use REVERSE.)
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);


// Define window size - per example code
const unsigned long WindowSize = 5000;  // Cycle on and off every five seconds


unsigned long windowStartTime;


void setup() {
  Serial.begin(9600); // For serial monitor/DAQ - using Parallax DAQ
  delay(100); // for BME280 to boot up
  Serial.println("CLEARDATA"); // For DAQ sheet - clears up any data left from previous projects
  Serial.println("LABEL,Time,Timer,Corrected_Temperature,Corrected_RH, pid_output"); // Label tells Excel that the next values are column names
  Serial.println("RESETTIMER"); // reset timer for DAQ


  bme.begin(); // Start BME280


  pinMode(RelayPin, OUTPUT); // Define RelayPin as output


  windowStartTime = millis(); // Initialize start time
  Setpoint = 30.0; // RH threshold. Logic: for RH < 30, humidifier should be on, for RH > 30, humidifier should be off.


  myPID.SetOutputLimits(0, WindowSize); // from PID Github example
  myPID.SetMode(AUTOMATIC); // from PID Github example
}


void loop() {
  double bmeTemp = bme.readTemperature(); // Read temperature
  double corrTemp = bmeTemp * 0.9232144 - 0.1222692; // correct the temperature - based on my experimental curve
  //double bmePres = bme.readPressure(); // Read barometric pressure
  double bmeRH = bme.readHumidity(); // Read RH value
  const double m1 = 7.591386;
  double f1 = bmeTemp / (bmeTemp + 240.7263);
  double f2 = corrTemp / (corrTemp + 240.7263);
  double f3 = (bmeTemp + 273.15) / (corrTemp + 273.15);
  double corrRH = f3 * bmeRH * pow(10.0, (m1 * (f1 - f2))); // correct the RH - based on my experimental curve


  Input = corrRH; // Setting PID input to corrected value of RH
  myPID.Compute(); // Call the PID compute function


  // Print data to Serial Monitor/Parallax DAQ
  Serial.print("DATA, TIME, TIMER,");
  Serial.print(corrTemp);
  Serial.print(",");
  Serial.print(corrRH);
  Serial.print(",");
  Serial.println(Output);


  //time to shift the Relay Window?
  if ((millis() - windowStartTime) > WindowSize) {
    windowStartTime += WindowSize;
  }


  // Turn the relay ON for the first part of the window and off for the rest
  digitalWrite(RelayPin,  Output < (millis() - windowStartTime));


  //  simple on-off control - runs as expected.
  //  digitalWrite(RelayPin, corrRH < Setpoint);
  //  delay(2000); // delay for on-off control
}