PID Assistance

I’ve been cobbling together examples and other sketches to build a basic espresso machine controller.

The issue i have is that the relay controlling the heater does not turn off despite the output of the PID calc.
I have serial print debugging showing me the input value (temperature from an NTC) and the output of the PID and whether the relay is on.
Initially i get above zero output values and the heater is correctly on and the temperature approaches the setpoint, it then overshoots (by a lot, it’s a thermoblock with minimal water in it), the output reduces to 0 but the heater relay stays on (i have to manually cut the power to keep it from boiling over.

I would appreciate some advice.
I’ve commented out some other experimental code, and i can clean it up/minimalise it properly if need be

// Thermistor Example #3 from the Adafruit Learning System guide on Thermistors
// https://learn.adafruit.com/thermistor/overview by Limor Fried, Adafruit Industries
// MIT License - please keep attribution and consider buying parts from Adafruit

// which analog pin to connect
#define THERMISTORPIN A0
// resistance at 25 degrees C
#define THERMISTORNOMINAL 100000
// temp. for nominal resistance (almost always 25 C)
#define TEMPERATURENOMINAL 25
// how many samples to take and average, more takes longer
// but is more 'smooth'
#define NUMSAMPLES 5
// The beta coefficient of the thermistor (usually 3000-4000)
#define BCOEFFICIENT 3950
// the value of the 'other' resistor
#define SERIESRESISTOR 100000
#define HeaterPin 6
#define PumpPin 7
#define ButtonPin 8
unsigned long startTime;
int oldState;
long shotStart;

int samples[NUMSAMPLES];

// include the library code:
#include <LiquidCrystal.h>
#include <math.h>
#include <PID_v1.h>

// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

//Define Variables we'll be connecting to
double Setpoint = 45, Input, Output;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, 2, 0, 0, DIRECT);


int WindowSize = 500;
unsigned long windowStartTime;
//int in1 = 7;


void setup(void) {
  Serial.begin(9600);

  lcd.begin(16, 2);  // set up the LCD's number of columns and rows:
  analogReference(EXTERNAL);
  pinMode(HeaterPin, OUTPUT);
  pinMode(PumpPin, OUTPUT);
  //pinMode(ButtonPin, INPUT); //no button as yet
  digitalWrite(HeaterPin, HIGH); //heater off
  digitalWrite(PumpPin, HIGH); //pump off


  windowStartTime = millis();


  //tell the PID to range between 0 and the full window size
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop(void) {
  uint8_t i;
  float average;

  // take N samples in a row, with a slight delay
  for (i = 0; i < NUMSAMPLES; i++) {
    samples[i] = analogRead(THERMISTORPIN);
    delay(10);
  }

  // average all the samples out
  average = 0;
  for (i = 0; i < NUMSAMPLES; i++) {
    average += samples[i];
  }
  average /= NUMSAMPLES;

  /* temp calculation debugging serial prints
    Serial.print("Average analog reading ");
    Serial.println(average);
  */
    // convert the value to resistance
    average = 1023 / average - 1;
    average = SERIESRESISTOR / average;
   /* temp calculation debugging serial prints
    Serial.print("Thermistor resistance ");
    Serial.println(average);
  */


  //Calculates temp C from NTC resistance readings gather above
  float temp;
  temp = average / THERMISTORNOMINAL;     // (R/Ro)
  temp = log(temp);                  // ln(R/Ro)
  temp /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
  temp += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
  temp = 1.0 / temp;                 // Invert
  temp -= 273.15;                         // convert to C

  //Prints calculated temperature to LCD and Serial
  Serial.print("Temperature ");
  Serial.print(temp);
  Serial.println(" *C");
  lcd.setCursor(0, 0);
  lcd.print("Heating Water...");
  lcd.setCursor(0, 1);
  lcd.print("Temp "); lcd.print(temp); lcd.print(" "); lcd.print((char)223); lcd.print("C");

  //PID
  //Input = analogRead(0); //removed to add temperature as input below
  Input = temp; //uses thermistor derived temperature reading as input for PID calc
  Serial.print("Input: ");
  Serial.println(Input); //debugging print
  myPID.Compute();
  Serial.print("Output: ");
  Serial.println(Output);  //debugging print

  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/

  unsigned long now = millis();
  if (now - windowStartTime > WindowSize)
  { //time to shift the Relay Window
    windowStartTime += WindowSize;
  }
  if (Output > now - windowStartTime)
  {
    digitalWrite(HeaterPin, HIGH);
    Serial.println("Heater Off"); //debugging print
  }
  else
  {
    digitalWrite(HeaterPin, LOW);
    Serial.println("Heater On"); //debugging print
  }


  /* below is basic on/off code for initial relay/heater testing


    //if(temp<Setpoint){
    //  digitalWrite(6, LOW);
    //  }
    //if(temp>=Setpoint+0.5){    //Temperature greater than setpoint
    //  digitalWrite(6, HIGH);
  */



  /* code for automatically starting an espresso shot once temperature is ready, and changing LCD to reflect this

     digitalWrite(7, LOW);  //starts a shot as soon as temperature is ready
    shotStart = millis();
    lcd.setCursor(0,0);
    lcd.print("Pulling Shot...");
    lcd.setCursor(0,1);
    lcd.print("Temp "); lcd.print(temp); lcd.print(" "); lcd.print((char)223); lcd.print("C");*/
  // }

  /*      if(millis() = shotStart + 23000){ //automatically stops shot at 23s, turns off both pump and heater
          digitalWrite(6, HIGH);
          digitalWrite(7, HIGH);
          }
  */

  /*Code to check for a button press to commence an espresso shot

     int ButtonState = digitalRead(ButtonPin);
    if (ButtonState == LOW) //inverted
    {
      if (oldState == HIGH)
      {
        Serial.println("Button Pushed");
        digitalWrite(PumpPin, LOW);   // turns the pump on
        startTime = millis();
      }
    }
    oldState = ButtonState;
    if (digitalRead(PumpPin == LOW))
    {
      if (millis() - startTime >= 23000UL) //23s pump run
      {
        Serial.println("Shot Ready");
        digitalWrite(PumpPin, HIGH);
      }
    }
  */
  delay(500);
}

analogReference(EXTERNAL);

Is this intentional? This means, that your adc use an external 5V reference voltage to calculate value between 0-255.

You sure, that this value is stable on 5V?

dr-o:
analogReference(EXTERNAL);

Is this intentional? This means, that your adc use an external 5V reference voltage to calculate value between 0-255.

You sure, that this value is stable on 5V?

Well, i have followed the relevant instructions for getting the NTC working and my temperature readings seem stable and correct?

PID library is kinda tricky, first try removing the debuggin Serial.print’s, more specific the ones between the PID calculation and the output.

If you have overshoot try setting Kp to a lower value, start in 0.5 and if it dont get to the setpoint then increase it in steps of 0.5, if you get close to the setpoint, you can do even more lower steps like 0.1.

And last but not least, the ending delay(), try lowering to 200.

Thanks, I'll try that. The delay is really just so I can actually read the output

Ok with the delay dropped to 200 and the crucial serial.prints removed it actually does some switching now but does not turn off anywhere near the set point, how much overshoot should I expect before I have tuned parameters?

dr-o:
analogReference(EXTERNAL);

Is this intentional? This means, that your adc use an external 5V reference voltage to calculate value between 0-255.

You sure, that this value is stable on 5V?

I have a wire connected to AREF, should this be changed to (DEFAULT)?

There is a nice simple PID function in this link. It uses the exact same calculations as the PID library in much less code.

I suggest starting with the K values at 0.3 and then adjusting them up or down by 0.1 within the range 0 to 1 - i.e. 0 to 100%

...R

Robin2:
There is a nice simple PID function in this link. It uses the exact same calculations as the PID library in much less code.

I suggest starting with the K values at 0.3 and then adjusting them up or down by 0.1 within the range 0 to 1 - i.e. 0 to 100%

...R

Thank you, I will try this.
Am i correct in that i want the PID set to DIRECT and not REVERSE for a water heating application?

as an side i have your guide to multiple things at once open and bookmarked in another tab for when i get this part of the project cracked.

Ok i changed it back to DIRECT, made sure the if statement was correct for an active-low relay, set all k values as 0.3, set the setpoint as 60C and let it fly (starting at about 45C).

It definitely behaved better to start with it, the heater was off more than on as it approached the setpoint. It did overshoot by about 12C before i had to abort to wake up my daughter (and daren't leave it running).

So i'm not sure how it would have behaved, i'll run a longer supervised test a bit later.
What is the next step reagrding k value tuning?

Being a thermoblock it reacts to heat input VERY quickly

Looks like you’re getting integral windup causing overshoot (assuming that you left the I Parameter unchanged).

You might consider using the PID only when you’re close to the set point. When you’re way low, the power is going to be 100% anyway so you don’t need the PID calc to tell you that. Then you can fine tune the calc for maintaining only.

Sometimes two PIDs can be used to similar effect but it seems like overkill for this purpose.

Thanks for your input, i've just turned down I and will re-run.

My concern over eschewing PID initially is that the temperature increase is ludicrously fast and when i tested bang-bang control it would over shoot by 30-40 degrees, so it seems like PID is a good fit here?

I'm starting to think i need to graph the serial output of the temperature so i can assess my tuning progress, currently i'm just eyeballing how fast it seems to react and how far it overshoots, obviously this is not going to be sufficient for accurate tuning.

Maybe two PIDs are the answer then.

Note that the serial monitor has a graphing mode.

wildbill:
Maybe two PIDs are the answer then.

This sounds daunting

Note that the serial monitor has a graphing mode.

well i never! I have been out of the game for a few years, i think the last IDE i used was 0021 or thereabouts

greenbeast:
This sounds daunting

Not really, it just allows you to avoid windup and the subsequent overshoot. PID1 does aggressive heating to get you close to the set point. It may well only have a P component. PID2 is much more gentle and takes over from PID1 to get the rest of the way and then keep you at that perfect espresso temperature.

wildbill:
Not really, it just allows you to avoid windup and the subsequent overshoot. PID1 does aggressive heating to get you close to the set point. It may well only have a P component. PID2 is much more gentle and takes over from PID1 to get the rest of the way and then keep you at that perfect espresso temperature.

Could you perhaps give me some guidance on how to implement such a solution?

Is it as simple as using if statements and the setpoint to determine how far from the setpoint the temp is and call a particular PID calculation?

Yes. You declare two PID objects with their own parameters. Start one and as you say, use the distance from the set point to tell you when to stop the first and switch to the second.

cheers, i'm narrowing in on parameters that maintain reasonably stable temperature if it doesn't start too far off (~5C)

Thinking about testing it starting with bang bang and switching to PID for the second stage of heating, should be able to get the bang bang to sling the temps into the right area with a bit of trial and error .