Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Noisy thermocouple input #3

Closed
robcazzaro opened this issue Jan 7, 2022 · 44 comments
Closed

Noisy thermocouple input #3

robcazzaro opened this issue Jan 7, 2022 · 44 comments

Comments

@robcazzaro
Copy link

Not sure if this is a real issue, but I thought that in any case you would be interested in more data.

I'm building a hot plate for SMD soldering with a k-type thermocouple connected to a MAX6675 and a plate heater controlled by a SSR. I use a relay-like control for the SSR, with 2 sec window and the SSR on for a fraction of that time. I built an Arduino program to auto tune my physical setup.

Unfortunately the MAX6675 output is very noisy, due to the thermocouple limitations. The output can easily change by a degree centigrade just due to noise. When looking at Brett's old autoune library, there was a specific note about noise http://brettbeauregard.com/blog/2012/01/arduino-pid-autotune-library/. The library had a setting for dealing with noise SetNoiseBand()

You are clearly using a different algorithm, and I'm not sure how impacted it is by noise. I also mentioned in another issue that it's very hard for a setup like mine to reach steady state. So even if I'm trying to use always the same conditions (5% output stepping up to 20%, but since I use a 2000 msec PWM window, the values you will see are 100 and 400), the actual values change quite a lot.

I modified your library to also print the thermocouple input value, not just the average, so you can see how noisy the system is. I'm attaching 4 different runs with identical settings but different starting conditions. The suggested K values are actually very consistent, considering the noise and starting conditions

//user settings
const uint32_t testTimeSec = 600;
float outputStart = 100; // 5% start
float outputStep = 400; // 20% step
const uint16_t samples = 600;
const uint32_t settleTimeSec = 300; //5 minutes to settle

pid2.log
pid3.log
pid4.log
pid5.log

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

Oh, I'm always interested in test results, the log files are very helpful. I see the resolution is ±0.25 and as I scroll down through the settling mode data, it jumps by 2 resolution-steps max, but follows a very gradual warming trend.

Yes, this test algorithm is quite tolerant to noise ... on the TCLab I was able to get results with a step change of 1, but had the board in a covered case so any moving air wouldn't cause a disturbance. In the next release, It shouldn't be a problem to add a reset function, option to print instantaneous values and a looser tolerance for settling so the next step test can begin sooner.

@robcazzaro
Copy link
Author

I'm afraid that the auto-tuned parameters did not work at all. I think it might be a scaling problem.

Let's say I use the values from the pid4.log file, Kp 0.0230 K 0.0013 Kd 0.0145

The output went from 100 to 400 (out of a possible 2000). But when I use those values in my program, the PWM out value is around 3 (out of 2000) even if the temperature is 30C away from the goal and dropping. after 100 seconds, PWM out is 5, and the temperature keeps dropping fast (it requires at least 300 to keep an higher than room temperature stable)

What am I doing wrong? Is there a scaling parameter I'm missing?

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

Not sure what board you're using ... is it 8-bit PWM?

Seems that the process gain is too low .. might need to scale the output range and/or min and max output limits.

This is sTune's internal calculation for process gain:

// calculate the process gain
_Ku = abs((pvMax - pvStart) / (_outputStep - _outputStart));

Also might need to use Arduino's map() function to scale the output.

I'll have a new revision ready shortly that addresses the issues (tested on my TCLab setup) ...

  • updated the Reset() function.
  • the Configure() function now runs reset()
  • serialMode printALL or printDEBUG now prints both pvInst and pvAvg values
  • the print test results now reports if the sample rate is adequate
  • updated dead time algorithm
  • can run consecutive step tests with a shortened settling period in between

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

I've pushed an update to the repository.
I'll create a new release if it checks out OK and after I get a chance to update the readme.

@robcazzaro
Copy link
Author

Sorry, I mentioned it elsewhere. My system is made of a 300w heating plate controlled by an SSR. Since you can't really PWM an SSR, I used what most people do: I selected a 2000msec window, and the PWM range is 0 to 2000. When the output is, say, 1000, the SSR is on for 1 second, off for one. With 1500, on for 1.5 seconds, off for 0.5 seconds. I read the temperature every second.

It's a very slow system. If the temperature is dropping, say from 100C and I apply 100% power, the temperature keeps dropping another 5-10 seconds (2-3C drop) before it starts stabilizing, and it takes at least another 5-6 seconds to cross 100C again

So I need to understand what range your code uses (i.e. what is 0% and 100%), and map it to my values. That is for both the range and the pvStart/pvStop values. And also select the right values for pvStart and pvEnd

It would be very easy for users if your code always assumed, say, 0 to 100 range, so that I could map 0 to 0 and 100 to 2000. And, once the parameters are calculated, then I could use the same scaling in the code (basically, in my case, multiply everything by 20). It might be already the case, btw, I'm just a bit lost at this point, I'm afraid

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

Ah, sorry ... I was mistakenly thinking of hardware PWM and PID output range and limits.

sTune only directly sets an output value as entered. In this case, its just a slowly timed output with an on time of 0-2000ms. sTune works with seconds for time units, so this might explain the low tuning values. After manually recalculating results from pid4.log, I get:

Output Control Range is 0.000 to 2.000 seconds

Output Start:    0.10
Output Step:     0.40
Process Gain:    38.33

Kp:              23.0
Ki:              1.3
Kd:              14.5

No mapping required as the process gain is just ... input difference / output difference
or more specifically ... _Ku = abs((pvMax - pvStart) / (_outputStep - _outputStart));

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

The part I'm still not sure about, is if the above are values are out by a factor of 60.

The tuning rule constants (I'm quite sure) are based on minutes. In sTune.cpp lines 193-194, I've already divided by 60 to convert to seconds which works well with true PWM based control.

However, controlling a digital output based on time might require a different conversion ... I'll need to do some research on this as I'm not a PID guru.

@robcazzaro
Copy link
Author

One thing I'm noticing, using Kp 23, Ki 1.3, and Kd 14.5 is that the system overshoots (expected), then slowly turns the output lower until 0% output.

Let's say my set temperature is 100C. The PID algorithm starts from output 20%, say, al the way to 100%. at around 90C, the output starts dropping, but not fast enough, and it overshoots by 15-20C. Then the output goes to 0 and the temperature drops. I would expect that around 105C or so the PID output started rising, to slow the undershoot. But it stays at 0 until the measure temperature drops below 100C, and only at that point it slowly starts raising the output, and it undershoots by at least 10C. My system is such that to maintain a 100C temperature, it needs a constant 20-25% output. So now it starts again to increase the output until it overshoots to, say 110C. But from this point on it never converges, as it always waits for the temperature to drop below 100C to start heating again

I have an espresso coffee machine with a PID, which in many ways it's similar to my plate heater: very slow to heat, and requires constant output to maintain the temperature once reached. That PID was auto tuned, and it works exceptionally well. IT overshoots target by a bit on the first rise, starts slowing down the drop before the setpoint, undershoots and overshoots once more by 2-3C, then it's rock solid. The key is that I can see its starting to increase the output while the temperature drops, well before it drops under the setpoint. That is a commercial PID, and it uses an SSR as well

I have an encyclopedic ignorance when it comes to PIDs (i.e. what I don't know can fill a volume :), so I have no idea what changes I should make to help "brake" the descent below the setpoint.

@robcazzaro
Copy link
Author

To add: the PID for the espresso machine is a Fuji PXR3 (https://americas.fujielectric.com/files/prod_selector_v/Micro%20Controller%20X%20Operation%20Manual%20Model%20PXR3%20(ECNO%20409d).pdf?r=false) and according to the manual the ranges for P, I and D are

P 0.0 to 999.9%
I 0 to 3200 seconds
D 0.0 to 999.9 seconds

and my autotuned values for that are P=4.3, I=95 and D=18.3

But P doesn't seem to be the same as in your library, since the manual says "When is too small, control will be unstable, and when is too large, the response will be delayed.", and from what I can see this behaves differently in my code

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

One thing I'm noticing, using Kp 23, Ki 1.3, and Kd 14.5 is that the system overshoots (expected), then slowly turns the output lower until 0% output.

This sounds good. Should expect up to 25% overshoot using zieglerNicholsPID
There is a no-overshoot tuning rule available.
Some progressively less aggressive rules are tyreusLuybenPID, someOvershootPID and noOvershootPID

@robcazzaro
Copy link
Author

The overshoot is not the problem, sorry if I was not clear. That will get smaller over time, if the undershoot also improves.

The current problem is that it always undershoots by 10C, no matter what the starting point is. Since it always undershoots the same amount, it never converges. And the problem is that the output doesn't start increasing until after the input temperature goes below the setpoint. To avoid undershoot, the system should start increasing the output once the temperature is dropping and it's 2-3C above setpoint. Instead it only starts after the drop. And the system inertia means that it wll always undershoot the same amount unless the PID output starts increasing sooner

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

To add: the PID for the espresso machine is a Fuji PXR3 (https://americas.fujielectric.com/files/prod_selector_v/Micro%20Controller%20X%20Operation%20Manual%20Model%20PXR3%20(ECNO%20409d).pdf?r=false) and according to the manual the ranges for P, I and D are

P 0.0 to 999.9%
I 0 to 3200 seconds
D 0.0 to 999.9 seconds

and my autotuned values for that are P=4.3, I=95 and D=18.3

But P doesn't seem to be the same as in your library, since the manual says "When is too small, control will be unstable, and when is too large, the response will be delayed.", and from what I can see this behaves differently in my code

In this case, P = 4.3 x 100 = 430%
The "I" value (I think) refers to time integral. So the autotuned I needs to be inverted (1/I) = 0.0105
The "D" value (I think) refers to time derivative. So the autotuned D needs to be inverted (1/D) = 0.0546

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 7, 2022

The overshoot is not the problem, sorry if I was not clear. That will get smaller over time, if the undershoot also improves.

The current problem is that it always undershoots by 10C, no matter what the starting point is. Since it always undershoots the same amount, it never converges. And the problem is that the output doesn't start increasing until after the input temperature goes below the setpoint. To avoid undershoot, the system should start increasing the output once the temperature is dropping and it's 2-3C above setpoint. Instead it only starts after the drop. And the system inertia means that it wll always undershoot the same amount unless the PID output starts increasing sooner

I'll take a closer look at the code when I get a chance later ... perhaps the output temporarily reverts to outputStart value in between step tests or from step test completion to initializing and starting the PID. Or perhaps the PID needs to start with the outputStep value so it doesn't try to climb from 0 output.

@robcazzaro
Copy link
Author

I ran a few tests using the old Autotune library https://github.com/br3ttb/Arduino-PID-AutoTune-Library.

The average of a few runs (even when using wildly different tuning ranges) was Kp=33, Ki=0.17 and Kd=0. Using those parameters, I get really good performance: minimal overshot, stabilizes without oscillations (even if the output stays at 0 when dropping). I'm enclosing the data from my program (not the autotune)

I first set a holding temperature of 100C from room temperature. Once it stabilizes at 100C, I drop the setpoint to 70C and wait for stable output again. Considering how slow my system is to cool (and can heat much faster than cooling), a great performance. Not sure why the original algorithm found much better values. Happy to run any test for you if you like

pidQuick.log

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 8, 2022

Interesting. I haven't used the PID-AutoTune-Library before but I have seen it. Do you have an example of how you're using it in code? Also curious how long the AutoTune test run takes.

If you're using QuickPID, note that it defaults to a more advanced anti-windup mode that reduces overshoot, so this could partially be the cause of the minimal overshoot and no oscillations.

Not sure why the original algorithm found much better values. Happy to run any test for you if you like

Thanks ... I still need to catch up a bit with more testing with my setup. Ive just finished comparing several settle time values here.

It looks looks like you have the PID-AutoTune-Library controller mode set to PI, which uses different constants than PID mode. I think its using Ziegler Nichols rules for a relay step test ... I'll look more closely later and compare with the reaction curve method and constants that sTune uses.

So far, sTune seems to be working great with others and on my setup with PWM control, but has some issue with the constants when using "bang-bang" output control for a relay.

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 8, 2022

I think I've found the main issue with the tuning constants. Currently sTune is using tuning constants based on ZN closed-loop, but sTune is open loop. I've found some enlightening information here They have a downloadable PID Tuner that provides a wealth of information and capabilities.

This will require a new sTune 1.2.0 release and will take some time (a few days to a week) to have it ready and tested.
Thanks for all your testing and reporting ... now I need to get busy on this!

@Dlloydev
Copy link
Owner

sTune 2.0.0 is published ... new open loop tuning methods and updated examples.

@Dlloydev
Copy link
Owner

Just released a new version that should work with relay, MOSFET or transistor PID control.

@robcazzaro
Copy link
Author

I just tried the latest release (using the latest sTune 2.1.1 and QuickPID 3.1.0)

My setup uses a MAX6675 k type thermocouple to control a 300W heating plate (https://www.amazon.com/gp/product/B07W1ZZH8T/) using a SSR.

I had to slightly modify your example code because you cannot read the MAX6675 more than every 500msec or so (I read it every second). And the SSR uses, like in your example, a 200msec window

Depending on the plate current temperature, the PWM output to keep the temperature stable changes. For example, at just above room temperature, a value of 200 (i.e. 10%) is enough. At 100C, it might need 700 (35%).

Brett's autotune is working based on temperature intervals: set pwm output to step value, once it reaches the upper temperature band (usually a degree or two over the thermocouple noise), turn off the PWM, and see how much it overshoots. That works well with slow systems like mine with a lot of thermal inertia

In your case, you set a PWM value and wait until an inflection is noticed. The problem is that the PWM value cannot be too high, or the system will go into thermal runout. For example, in my case, values above 700 (35%) will result in an ever increasing temperature with no limit (incidentally, might be worth having an "emergency shutoff" in your code, when the instant temperature is much above a limit, to turn off the PWM and abort)

If I try your code with a 700 step, it completes the run (see below), then goes into normal PID mode. With the values it calculated, it started from a 45C temperature at the end of the auto tuning steps, trying to keep a 100C temperature. As you can see, it overshoot by more than 100C (I pulled the plug above 200C instant temperature, the averaged reading only show 195C). While at 190C, the PWM was still well above 800 (40%), i.e. still increasing the temperature. The auto-tuned parameters clearly were very wrong (I tried a few other runs before, with a range of parameters very close and also wrong, added at the end)

If you have a clothes iron and get a MAX6675 module plus a k type thermocouple, you could simulate a system like mine. Set the iron thermostat to Linen (usually above 180C) and try to maintain a 100C temperature using a thermocouple connected to the bottom plate. Most PIDs used for temperature control operate with parameters similar to mine: a very powerful heater heating water (e.g. sous vide) or a system with lot of thermal inertia (like a reflow oven for PCBs). So I think that having your code work in this type of system would benefit many people

Here's my code (temperatures in C)

/****************************************************************************
   Autotune QuickPID Digital Output Example
   https://github.com/Dlloydev/sTune/wiki/Autotune_PID_Digital_Out_Reference
 ****************************************************************************/

#include <Arduino.h>
#include <sTune.h>
#include <QuickPID.h>
#include "Wire.h"
#include <LiquidCrystal.h>
#include <max6675.h>

// ***** PIN ASSIGNMENT *****
int ssrPin = 11;
int thermocoupleSOPin = A3;
int thermocoupleCSPin = A4;
int thermocoupleCLKPin = A5;

int lcdRsPin = 8;
int lcdEPin = 9;
int lcdD4Pin = 4;
int lcdD5Pin = 5;
int lcdD6Pin = 6;
int lcdD7Pin = 7;

// pins
const uint8_t inputPin = 0;
const uint8_t outputPin = ssrPin;
const uint8_t ledPin = LED_BUILTIN;

// test setup
uint32_t testTimeSec = 600; // testTimeSec / samples = sample interval
const uint16_t samples = 300;

uint32_t settleTimeSec = 15;
const float inputSpan = 240;
const float outputSpan = 2000; // window size for sTune and PID
const float minSpan = 50;
float outputStart = 0;
float outputStep = 700;
bool clearPidOutput = false; // false: "on the fly" testing, true: PID starts at 0 output

// temperature
const float mvResolution = 3300 / 1024.0f;
const float bias = 50;

unsigned long nextRead;

// variables
float Input, Output, Setpoint = 100, Kp, Ki, Kd;

QuickPID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd,
               myPID.pMode::pOnError,
               myPID.dMode::dOnMeas,
               myPID.iAwMode::iAwClamp,
               myPID.Action::direct);

sTune tuner = sTune(&Input, &Output, tuner.ZN_PID, tuner.directIP, tuner.printALL);
/*                                         ZN_PID           directIP        printOFF
                                           DampedOsc_PID    direct5T        printALL
                                           NoOvershoot_PID  reverseIP       printSUMMARY
                                           CohenCoon_PID    reverse5T       printDEBUG
                                           Mixed_PID
                                           ZN_PI
                                           DampedOsc_PI
                                           NoOvershoot_PI
                                           CohenCoon_PI
                                           Mixed_PI
*/

LiquidCrystal lcd(lcdRsPin, lcdEPin, lcdD4Pin, lcdD5Pin, lcdD6Pin, lcdD7Pin);
// Specify MAX6675 thermocouple interface
MAX6675 thermocouple(thermocoupleCLKPin, thermocoupleCSPin,
                     thermocoupleSOPin);

void softPwmOutput();

void setup()
{
  pinMode(outputPin, OUTPUT);
  pinMode(ledPin, OUTPUT);
  digitalWrite(outputPin, LOW);
  //analogReference(EXTERNAL); // used by TCLab
  Serial.begin(57600);
  tuner.Configure(inputSpan, outputSpan, outputStart, outputStep, testTimeSec, settleTimeSec, samples);
  // Start-up splash
  lcd.begin(16, 2);
  lcd.clear();
  nextRead = millis();
}

void loop()
{
  // Current time
  unsigned long now;

  softPwmOutput();
  switch (tuner.Run())
  {
  case tuner.inOut: // active while sTune is testing
    if (millis() > nextRead)
    {
      // Read thermocouple next sampling period
      nextRead += 1000;
      Input = thermocouple.readCelsius();
      
      lcd.setCursor(0, 1);
      lcd.print(Input);
      lcd.print("C pwm=");
      lcd.print((int)Output);
      lcd.print(" ");
      //Serial.println(Input);
    }
    break;
  case tuner.tunings:                         // active just once when sTune is done
    tuner.GetAutoTunings(&Kp, &Ki, &Kd);      // sketch variables updated by sTune
    myPID.SetOutputLimits(0, outputSpan);     // PID output spans from 0 to the full window size
    myPID.SetSampleTimeUs(outputSpan * 1000); // PID sample rate matches sTune
    if (clearPidOutput)
      Output = 0;
    myPID.SetMode(myPID.Control::automatic); // the PID is turned on (automatic)
    myPID.SetTunings(Kp, Ki, Kd);            // update PID with the new tunings
    break;
  case tuner.runPid: // active once per sample period after case "tunings"
    if (millis() > nextRead)
    {
      // Read thermocouple next sampling period
      nextRead += 1000;
      Input = thermocouple.readCelsius();

      lcd.setCursor(0, 1);
      lcd.print(Input);
      lcd.print("C pwm=");
      lcd.print((int)Output);
      lcd.print(" ");
      //Serial.println(Input);
    }
    myPID.Compute();
    tuner.plotter(Setpoint, 0.1, 5, 1); // scaled output, every 5th sample, averaged input
    break;
  }
  // put your main code here, to run repeatedly
}

void softPwmOutput()
{
  static uint32_t tPrev;
  uint32_t tNow = millis();
  uint32_t tElapsed = (tNow - tPrev);
  if (tElapsed >= outputSpan)
    tPrev = tNow;
  if (tElapsed > minSpan && tElapsed < outputSpan - minSpan)
  { // in range?
    if (tElapsed <= Output)
    {
      digitalWrite(outputPin, HIGH);
      digitalWrite(ledPin, HIGH);
    }
    else
    {
      digitalWrite(outputPin, LOW);
      digitalWrite(ledPin, LOW);
    }
  }
}

And here's the run

 Sec: 12.99996  pvInst: 25.2500  Settling  ⤳⤳⤳⤳
 Sec: 10.99992  pvInst: 25.2500  Settling  ⤳⤳⤳⤳
 Sec: 8.99991  pvInst: 25.5000  Settling  ⤳⤳⤳⤳
 Sec: 6.99987  pvInst: 25.0000  Settling  ⤳⤳⤳⤳
 Sec: 4.99984  pvInst: 25.2500  Settling  ⤳⤳⤳⤳
 Sec: 2.99980  pvInst: 25.5000  Settling  ⤳⤳⤳⤳
 Sec: 0.99978  pvInst: 25.2500  Settling  ⤳⤳⤳⤳
 Sec: 0.00000  pvInst: 25.00  pvAvg: 25.0000  Output: 0  pvTangent: 0.0000 →
 Sec: 2.00003  pvInst: 24.75  pvAvg: 24.9917  Output: 0  pvTangent: -0.0083 ↘
 Sec: 4.00007  pvInst: 25.00  pvAvg: 24.9917  Output: 0  pvTangent: -0.0083 →
 Sec: 6.00012  pvInst: 25.00  pvAvg: 24.9917  Output: 0  pvTangent: -0.0083 →
 Sec: 8.00016  pvInst: 25.00  pvAvg: 24.9917  Output: 0  pvTangent: -0.0083 →
 Sec: 10.00016  pvInst: 24.50  pvAvg: 24.9750  Output: 0  pvTangent: -0.0250 ↘
 Sec: 12.00019  pvInst: 25.00  pvAvg: 24.9750  Output: 0  pvTangent: -0.0250 →
 Sec: 14.00022  pvInst: 25.00  pvAvg: 24.9750  Output: 0  pvTangent: -0.0250 →
 Sec: 16.00024  pvInst: 24.50  pvAvg: 24.9583  Output: 700  pvTangent: -0.0417 ↘
 Sec: 18.00028  pvInst: 25.50  pvAvg: 24.9750  Output: 700  pvTangent: -0.0250 ↗
 Sec: 20.00028  pvInst: 25.25  pvAvg: 24.9833  Output: 700  pvTangent: -0.0167 ↗
 Sec: 22.00032  pvInst: 25.75  pvAvg: 25.0083  Output: 700  pvTangent: 0.0083 ↗
 Sec: 24.00032  pvInst: 26.25  pvAvg: 25.0500  Output: 700  pvTangent: 0.0500 ↗
 Sec: 26.00038  pvInst: 26.00  pvAvg: 25.0833  Output: 700  pvTangent: 0.0833 ↗
 Sec: 28.00039  pvInst: 26.50  pvAvg: 25.1333  Output: 700  pvTangent: 0.1333 ↗
 Sec: 30.00045  pvInst: 25.25  pvAvg: 25.1417  Output: 700  pvTangent: 0.1417 ↗
 Sec: 32.00046  pvInst: 26.25  pvAvg: 25.1833  Output: 700  pvTangent: 0.1833 ↗
 Sec: 34.00051  pvInst: 26.75  pvAvg: 25.2417  Output: 700  pvTangent: 0.2417 ↗
 Sec: 36.00051  pvInst: 27.25  pvAvg: 25.3167  Output: 700  pvTangent: 0.3167 ↗
 Sec: 38.00054  pvInst: 27.50  pvAvg: 25.4000  Output: 700  pvTangent: 0.4000 ↗
 Sec: 40.00058  pvInst: 27.25  pvAvg: 25.4750  Output: 700  pvTangent: 0.4750 ↗
 Sec: 42.00062  pvInst: 26.50  pvAvg: 25.5250  Output: 700  pvTangent: 0.5250 ↗
 Sec: 44.00063  pvInst: 28.00  pvAvg: 25.6250  Output: 700  pvTangent: 0.6250 ↗
 Sec: 46.00068  pvInst: 27.00  pvAvg: 25.6917  Output: 700  pvTangent: 0.6917 ↗
 Sec: 48.00068  pvInst: 27.50  pvAvg: 25.7750  Output: 700  pvTangent: 0.7750 ↗
 Sec: 50.00074  pvInst: 27.75  pvAvg: 25.8667  Output: 700  pvTangent: 0.8667 ↗
 Sec: 52.00080  pvInst: 28.25  pvAvg: 25.9750  Output: 700  pvTangent: 0.9750 ↗
 Sec: 54.00082  pvInst: 29.25  pvAvg: 26.1167  Output: 700  pvTangent: 1.1167 ↗
 Sec: 56.00083  pvInst: 29.75  pvAvg: 26.2750  Output: 700  pvTangent: 1.2750 ↗
 Sec: 58.00088  pvInst: 28.50  pvAvg: 26.3917  Output: 700  pvTangent: 1.3917 ↗
 Sec: 60.00091  pvInst: 29.00  pvAvg: 26.5250  Output: 700  pvTangent: 1.7750 ↗
 Sec: 62.00091  pvInst: 30.00  pvAvg: 26.7000  Output: 700  pvTangent: 1.7000 ↘
 Sec: 64.00096  pvInst: 29.75  pvAvg: 26.8583  Output: 700  pvTangent: 1.8583 ↗
 Sec: 66.00101  pvInst: 30.50  pvAvg: 27.0417  Output: 700  pvTangent: 2.0417 ↗
 Sec: 68.00102  pvInst: 31.00  pvAvg: 27.2417  Output: 700  pvTangent: 2.7417 ↗
 Sec: 70.00102  pvInst: 30.25  pvAvg: 27.4333  Output: 700  pvTangent: 2.4333 ↘
 Sec: 72.00106  pvInst: 31.25  pvAvg: 27.6417  Output: 700  pvTangent: 2.6417 ↗
 Sec: 74.00109  pvInst: 32.25  pvAvg: 27.8833  Output: 700  pvTangent: 3.3833 ↗
 Sec: 76.00109  pvInst: 32.50  pvAvg: 28.1500  Output: 700  pvTangent: 2.6500 ↘
 Sec: 78.00113  pvInst: 33.25  pvAvg: 28.4083  Output: 700  pvTangent: 3.1583 ↗
 Sec: 80.00116  pvInst: 33.25  pvAvg: 28.6750  Output: 700  pvTangent: 2.9250 ↘
 Sec: 82.00119  pvInst: 33.25  pvAvg: 28.9250  Output: 700  pvTangent: 2.6750 ↘
 Sec: 84.00122  pvInst: 33.25  pvAvg: 29.1583  Output: 700  pvTangent: 3.1583 ↗
 Sec: 86.00128  pvInst: 33.50  pvAvg: 29.4083  Output: 700  pvTangent: 2.9083 ↘
 Sec: 88.00131  pvInst: 33.75  pvAvg: 29.6500  Output: 700  pvTangent: 4.4000 ↗
 Sec: 90.00131  pvInst: 34.50  pvAvg: 29.9583  Output: 700  pvTangent: 3.7083 ↘
 Sec: 92.00136  pvInst: 35.00  pvAvg: 30.2500  Output: 700  pvTangent: 3.5000 ↘
 Sec: 94.00136  pvInst: 34.50  pvAvg: 30.5083  Output: 700  pvTangent: 3.2583 ↘
 Sec: 96.00137  pvInst: 35.25  pvAvg: 30.7750  Output: 700  pvTangent: 3.2750 ↗
 Sec: 98.00140  pvInst: 36.00  pvAvg: 31.0583  Output: 700  pvTangent: 3.8083 ↗
 Sec: 100.00144  pvInst: 35.50  pvAvg: 31.3333  Output: 700  pvTangent: 4.8333 ↗
 Sec: 102.00148  pvInst: 35.50  pvAvg: 31.6333  Output: 700  pvTangent: 3.6333 ↘
 Sec: 104.00151  pvInst: 36.75  pvAvg: 31.9250  Output: 700  pvTangent: 4.9250 ↗
 Sec: 106.00152  pvInst: 36.50  pvAvg: 32.2417  Output: 700  pvTangent: 4.7417 ↘
 Sec: 108.00156  pvInst: 37.25  pvAvg: 32.5667  Output: 700  pvTangent: 4.8167 ↗
 Sec: 110.00160  pvInst: 37.75  pvAvg: 32.9000  Output: 700  pvTangent: 4.6500 ↘
 Sec: 112.00166  pvInst: 36.75  pvAvg: 33.1833  Output: 700  pvTangent: 3.9333 ↘
 Sec: 114.00167  pvInst: 38.00  pvAvg: 33.4750  Output: 700  pvTangent: 3.7250 ↘
 Sec: 116.00170  pvInst: 38.25  pvAvg: 33.7583  Output: 700  pvTangent: 5.2583 ↗
 Sec: 118.00170  pvInst: 38.75  pvAvg: 34.1000  Output: 700  pvTangent: 5.1000 ↘
 Sec: 120.00176  pvInst: 38.75  pvAvg: 34.4250  Output: 700  pvTangent: 4.4250 ↘
 Sec: 122.00177  pvInst: 39.25  pvAvg: 34.7333  Output: 700  pvTangent: 4.9833 ↗
 Sec: 124.00181  pvInst: 39.75  pvAvg: 35.0667  Output: 700  pvTangent: 4.5667 ↘
 Sec: 126.00183  pvInst: 40.25  pvAvg: 35.3917  Output: 700  pvTangent: 4.3917 ↘
 Sec: 128.00184  pvInst: 40.00  pvAvg: 35.6917  Output: 700  pvTangent: 5.4417 ↗
 Sec: 130.00186  pvInst: 41.00  pvAvg: 36.0500  Output: 700  pvTangent: 4.8000 ↘
 Sec: 132.00190  pvInst: 40.75  pvAvg: 36.3667  Output: 700  pvTangent: 4.1167 ↘
 Sec: 134.00193  pvInst: 40.75  pvAvg: 36.6500  Output: 700  pvTangent: 4.1500 ↗
 Sec: 136.00195  pvInst: 41.50  pvAvg: 36.9500  Output: 700  pvTangent: 3.7000 ↘
 Sec: 138.00195  pvInst: 42.25  pvAvg: 37.2500  Output: 700  pvTangent: 4.0000 ↗
 Sec: 140.00199  pvInst: 42.00  pvAvg: 37.5417  Output: 700  pvTangent: 4.2917 ↗
 Sec: 142.00204  pvInst: 42.25  pvAvg: 37.8417  Output: 700  pvTangent: 4.5917 ↗
 Sec: 144.00207  pvInst: 42.75  pvAvg: 38.1583  Output: 700  pvTangent: 4.6583 ↗
 Sec: 146.00213  pvInst: 42.25  pvAvg: 38.4500  Output: 700  pvTangent: 4.7000 ↗
 Sec: 148.00213  pvInst: 42.75  pvAvg: 38.7500  Output: 700  pvTangent: 4.2500 ↘
 Sec: 150.00218  pvInst: 42.75  pvAvg: 39.0250  Output: 700  pvTangent: 4.0250 ↘
 Sec: 152.00218  pvInst: 43.50  pvAvg: 39.3083  Output: 700  pvTangent: 4.8083 ↗
 Sec: 154.00221  pvInst: 43.75  pvAvg: 39.6167  Output: 700  pvTangent: 4.3667 ↘
 Sec: 156.00221  pvInst: 44.00  pvAvg: 39.9083  Output: 700  pvTangent: 3.9083 ↘
 Sec: 158.00224  pvInst: 44.25  pvAvg: 40.1833  Output: 700  pvTangent: 4.6833 ↗
 Sec: 160.00230  pvInst: 44.50  pvAvg: 40.4833  Output: 700  pvTangent: 4.9833 ↗

 Controller Action: directIP
 Tuning Method:     ZN_PID

 Output Start:      0.00
 Output Step:       700.00
 Sample Sec:        2.00000

 Pv Start:          25.0000
 Pv Max:            59.0930
 Pv Diff:           34.0930

 Process Gain:      0.4059
 Dead Time Sec:     24.00032
 Tau Sec:           339.37997

 Tau/Dead Time:     14.1 (easy to control)
 Tau/Sample Period: 169.7 (good sample rate)

  Kp: 0.05
  Ki: 0.06  Ti: 16.00
  Kd: 0.25  Td: 4.00

Setpoint:100.00, Input:42.56, Output:73.64,
Setpoint:100.00, Input:44.03, Output:76.23,
Setpoint:100.00, Input:45.48, Output:78.77,
Setpoint:100.00, Input:47.04, Output:81.18,
Setpoint:100.00, Input:48.71, Output:83.53,
Setpoint:100.00, Input:50.49, Output:86.33,
Setpoint:100.00, Input:52.22, Output:88.50,
Setpoint:100.00, Input:54.00, Output:90.55,
Setpoint:100.00, Input:55.88, Output:92.99,
Setpoint:100.00, Input:57.88, Output:95.25,
Setpoint:100.00, Input:60.06, Output:96.53,
Setpoint:100.00, Input:62.28, Output:98.48,
Setpoint:100.00, Input:64.92, Output:100.18,
Setpoint:100.00, Input:67.73, Output:101.73,
Setpoint:100.00, Input:70.65, Output:103.07,
Setpoint:100.00, Input:73.66, Output:103.96,
Setpoint:100.00, Input:76.90, Output:104.64,
Setpoint:100.00, Input:80.43, Output:105.11,
Setpoint:100.00, Input:84.07, Output:105.40,
Setpoint:100.00, Input:88.05, Output:105.44,
Setpoint:100.00, Input:92.46, Output:105.13,
Setpoint:100.00, Input:97.18, Output:104.60,
Setpoint:100.00, Input:102.17, Output:103.83,
Setpoint:100.00, Input:107.53, Output:102.44,
Setpoint:100.00, Input:113.18, Output:100.99,
Setpoint:100.00, Input:119.09, Output:99.62,
Setpoint:100.00, Input:125.31, Output:97.49,
Setpoint:100.00, Input:131.80, Output:94.99,
Setpoint:100.00, Input:138.63, Output:92.13,
Setpoint:100.00, Input:145.63, Output:88.89,
Setpoint:100.00, Input:152.83, Output:84.40,
Setpoint:100.00, Input:160.23, Output:80.41,
Setpoint:100.00, Input:167.66, Output:75.00,
Setpoint:100.00, Input:174.92, Output:69.20,
Setpoint:100.00, Input:181.83, Output:63.04,
Setpoint:100.00, Input:188.11, Output:59.24,
Setpoint:100.00, Input:192.73, Output:53.11,
Setpoint:100.00, Input:195.04, Output:48.48,
Setpoint:100.00, Input:195.08, Output:44.19,
Setpoint:100.00, Input:192.98, Output:39.22,

Here are all the other values from previous runs

 Controller Action: directIP
 Tuning Method:     ZN_PID

 Output Start:      0.00
 Output Step:       500.00
 Sample Sec:        2.00000

 Pv Start:          19.5000
 Pv Max:            46.9949
 Pv Diff:           27.4949

 Process Gain:      0.4582
 Dead Time Sec:     12.00022
 Tau Sec:           400.42297

 Tau/Dead Time:     33.4 (easy to control)
 Tau/Sample Period: 200.2 (good sample rate)

  Kp: 0.06
  Ki: 0.12  Ti: 8.00
  Kd: 0.50  Td: 2.00
  
   Sec: 166.00251  pvInst: 45.00  pvAvg: 42.4750  Output: 500  pvTangent: 2.4750 ↗

 Controller Action: directIP
 Tuning Method:     ZN_PID

 Output Start:      0.00
 Output Step:       500.00
 Sample Sec:        2.00000

 Pv Start:          35.2500
 Pv Max:            53.6671
 Pv Diff:           18.4171

 Process Gain:      0.3070
 Dead Time Sec:     54.00101
 Tau Sec:           280.68066

 Tau/Dead Time:     5.2 (easy to control)
 Tau/Sample Period: 140.3 (good sample rate)

  Kp: 0.06
  Ki: 0.03  Ti: 36.01
  Kd: 0.11  Td: 9.00
  
 Sec: 128.00184  pvInst: 55.25  pvAvg: 52.5667  Output: 500  pvTangent: 1.0667 ↘

 Controller Action: directIP
 Tuning Method:     ZN_PID

 Output Start:      0.00
 Output Step:       500.00
 Sample Sec:        2.00000

 Pv Start:          50.5000
 Pv Max:            60.5734
 Pv Diff:           10.0734

 Process Gain:      0.1679
 Dead Time Sec:     66.00090
 Tau Sec:           185.29325

 Tau/Dead Time:     2.8 (easy to control)
 Tau/Sample Period: 92.6 (good sample rate)

  Kp: 0.07
  Ki: 0.02  Ti: 44.01
  Kd: 0.09  Td: 11.00

@Dlloydev
Copy link
Owner

Thank you for your suggestions and detailed test data!

I should easily be able to closely replicate your setup after getting a few missing parts. I have a MAX31856 thermocouple amplifier board and some type-K thermocouples on hand that I haven't used yet, so all I'll need to get is a zero-crossing type SSR and the wife's iron. I like (and could use) the 300W heating plate you've referenced, so I'll get one of those (or similar) on order.

Most PIDs used for temperature control operate with parameters similar to mine: a very powerful heater heating water (e.g. sous vide) or a system with lot of thermal inertia (like a reflow oven for PCBs). So I think that having your code work in this type of system would benefit many people

Yes, I agree ... I'll report back after testing a similar setup.

@robcazzaro
Copy link
Author

Cool!

The MAX31856 is even better than the MAX6675. Cheap thermocouples are not always calibrated and are noisy in the range I use them. A K type thermocouple has a -200 to 1260C range, so even a low 0.5% noise can easily be more than 1C, even if used with a better amplifier than the MAX6675

If you get one of those heating plates, please keep in mind that above ~200C they become non linear. The heating element is a PTC, and the resistance increases with the temperature. So they never get above 240-250C even with a 100% PWM signal. I kinda like this feature for my project, because no matter what, I won't set anything on fire :). As such it's basically impossible to have a PID controller that can use the same parameters below 200C and above 200C. Most of my use is below 165C (for low temp solder rework), so those plates are still mostly linear in the range of my interest

Those plates are very cheap from Aliexpress and with a lot of sizes and wattages. And can get hot FAST. I chose that specific one because it had silicone feet, which protect the surface. The metal feet get hot, so if you get one without the silicone, make sure to have something to protect the surface they are on

@geleos27
Copy link

I'm sorry that wedging to your conversation.
We have community who construct BGA reflow stations. It's non-commercial project, but members definetly uses their stations for earnings =)
You can found links to forum in ArduinoSolderingStation repo (conversations mostly in Russian but we have ENG version of software (both: sketch for arduino and PC software)).

I'm trying to use sTune for solving similar to robcazzaro problem. (even with tottaly same hardware: MAX6675, K-Thermocouples and SSR. Heaters slightly slower but with wider temp range (Flat Ceramic heaters)

Right now we are trying to implement autotune function in software, but faicing with difficulties - due to noizy thermocouples, 10 to 30 seconds dead time(caused by thermocouple location), and inertial heaters.

So there are at least 20 more persons who interested in your project =)

BTW, during manual tuning I'ts pretty easy to determine wich part of PID cause overshoot: just print calculated pTerm, iTerm and dTerm separetely in addition to total Output.

@Dlloydev
Copy link
Owner

Hello and thank you for your interesting post. I assume the flat ceramic heaters are PTC type, which at this point, I've had no experience working with. However, now I've just received one of these PTC heating plates (140W/110V) and a suitable SSR as I was unwilling to wait until March for delivery of the larger PTC heater. So now I have the necessary components ... just need to find some time to get some testing done.

@robcazzaro
Copy link
Author

@Dlloydev, PTC heaters behave pretty much like any other heater until almost at the cutoff temperature, at which point the internal resistance increases exponentially (literally). Actually a PTC resistance gets slightly lower with the increase of the temperature, only to increase dramatically close to the self-stable point (see attached curve, for a unit similar to yours). For all practical purposes, a PTC heater behaves like a normal heater until T-10C or so

The critical points of our type of devices are the high cooling inertia compared to the heating one, and the SSR control, slower in general then a pwm control (1-2 seconds cycle). A heating plate or a PCB oven can increase temperature by tens of degrees in a few seconds when at max power, but only cool a fraction of a degree in the same time. That makes the PID control more challenging and requires aggressively slowing (and less so increasing) the output the closer one gets to the target set point

PTC Curve

@geleos27 thanks for the tip to print the calculated pTerms during the loop. I did not think about it before, and I'm loudly slapping my head now :)

@Dlloydev
Copy link
Owner

@robcazzaro, yes, I can see how the critical points you've mentioned make PID control more challenging - thanks for explaining them in detail as this is very helpful. I have some ideas I'd like to try to help equalize the temperature rise and fall times.

Offhand, regarding the 1-2 sec PID cycle, I don't see why the PID windowSize couldn't be shortened to say 500ms for a 2Hz sample rate. I have a zero cross type SSR, so with a 60Hz system, the output control would be in steps of 8.33ms, giving 60 steps of power control within the 500ms window or maybe there would be increased benefit by using a 4Hz sample rate with only 30 steps of power control.

I hope to get some testing done in the next few days.

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 26, 2022

I've just tested my 140W PTC / SSR setup to see what output value is needed to bring the temp up to max. Also, I've found another link to the 300W PTC heater where they specify the constant power used to reach the max temp. Here's a comparison of the TCLab (which works with sTune and PID) vs PTC Heaters:

  TCLab 60Hz/110VAC/140W PTC 220℃ 50Hz/220VAC/300W PTC 260℃
Constant Power Output 100% to reach 95℃ 14W (10%) to reach 220℃ < 60W (20%) to reach 260℃
Useable Control Range 0-100% 0-10% 0-20%
Minimum step size 1 ms (TIP31C) 8.33 ms (60Hz AC, SSR) 10 ms (50Hz AC, SSR)
Useable Control Steps (windowSize=2000ms) 2000 24 0.1*2000/8.33 40 0.2*2000/10
If adding heatsink can boost PTC control range to 0-50%, then the useable control steps = 2000 120 100
With windowSize=5000ms, useable control steps = 5000 300 250

The output control range and useable steps for PTC heaters is significantly limited. I think this is the main reason for tuning and PID control difficulties.

I haven't tried tuning or PID yet (just timed digitalWrite on/off). Since the control resolution is so poor, I'll wait for a heatsink I've ordered to arrive and see what improvements that will make. May also need to go with a larger windowSize rather than smaller.

I've tested with nothing added to the aluminum shell, but if it were heating something up, I can see how this should only help to boost the useable control range.

If the cost of a suitable DC power supply isn't an issue I'm thinking that something like this might work well (no heatsink required) and perhaps operate it with a MOSFET switching only 12VDC. Interesting that the negative comment offers a clue as to the natural heat dissipation (about 150W), so this should be easy to control at much higher speeds and resolution.

@geleos27
Copy link

geleos27 commented Jan 26, 2022

Thank you for engagement. Actually there are lot's of projects and chineese BGA stations use heaters this of this type:
https://www.ebay.com/itm/274667875972

They much slower and more inertial than PTC's

@robcazzaro
Copy link
Author

Good info @Dlloydev

I'm not entirely sure why you say that you can only use 10% of the power for control and that control resolution is poor. Control resolution is still 0-100%, and with a 2000 msec window you have 2000 possible values to play with. In my case (where the PTC cartridge is connected to an aluminum plate), if I connect the plate to 110V with no PID, it takes a good 40-50 seconds to reach 240C, so there's plenty of time to start from 100% and dial down to whatever is required for a stable temperature

I used one of those REX C100 PIDs to control my PTC heater to 100C, and with the default settings it started from 100% on, after 60-70C started dialing down, overshot by 10C or so, then stabilized very quickly at 100C. It's just a matter of finding the right parameters. Currently your sTune is not capable of handling this, while the older Brett's Autotune does a much better job (no disrespect meant, just trying to provide data). With Brett's Autotune, I can get much closer to optimize the system

I also gave you before the example of my espresso machine, with a 1500W heater in a small boiler, working the same way: 100% output when cold, progressively lower the closer it gets to the target. I estimate that when my espresso machine is at a stable temperature, the heater is on around 5% of the cycle.

All the systems based on a heater have a heater that is an order of magnitude more powerful than what's needed to simply maintain temperature. The PID controls on/off so that it gets stable. It works very differently than a PID for a motor or other similar processes

As such, the PID must use 100% of the potential control range, simply increasing or decreasing as needed

@Dlloydev
Copy link
Owner

Thanks @robcazzaro, that clarifies things really well - that auto-tuning and PID control with the PTC heaters (unloaded?) can work OK. Poor choice of words "useable" for control range ... just meant to compare the % output required for max temp.

The 10% of the power for control for the 140W PTC was just from my tests with timed output (on 200ms, off 1800ms) where this would bring the temperature up to about 209 deg C. If I were to rely only on natural heat dissipation, then there's only 10% range of control to work with, because in this case 11-100% output has no effect on temperature.

I think another benefit to adding heat-sink might be helping to balance (equalize) the temp rise and fall response. With heat-sink, the PID would have to work harder (increased output value) to get to the same setpoint. Then when it needs to lower the temp, heat would dissipate much quicker and the fall time reduced and more responsive.

Thinking ahead, after getting the PTCs to work with tuning and PID control, I'd like to try some sort of dual tuning setup, similar to the PID_AdaptiveTunings example, except that it will use separate "heating" and "cooling" tuning parameters depending on if the input is below or above setpoint.

@robcazzaro
Copy link
Author

Yes, in pretty much all this category of systems, 10% power output is more than enough to reach max temperature (and beyond for systems based on a traditional heater instead of a PTC). Think about a clothes iron: if you listen to the mechanical thermostat, you will notice that after reaching stable temperature, it clicks on and off very rapidly, spending most of the time in the off state.

Even in my system, around 10% power is more than enough to keep increasing temperature until the PTC cutoff. No matter how big the heat sink, these systems will always have an order of magnitude more power than needed. It's the best way to quickly reach the target temperature and to be able to handle environmental changes

Pretty much every sous vide in use and every PCB oven in the world (plus a huge percent of good espresso machines) operate this way. And I'm sure pretty much every industrial oven or heated tank as well

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 27, 2022

I ran the Get_All_Tunings.ino example to get the ZN_PID tuning constants and then used these in a basic QuickPID sketch set up for this system. I found that I needed to significantly add to the integral time (reduce Ki) to prevent oscillations and improve regulation.

With the setpoint temperature set to 150°C and the input still reading more than 150°C due to a previous test, this is the plot:

image

You can see that the PID brings the temperature into regulation, but it wanders for a period of time, usually opposite to the direction of the output.

I've marked a few points on the plot to illustrate:

① This is when the input temperature crosses the setpoint (the error goes from positive to negative). The PID takes corrective action by increasing the output.

① to ② The temperature keeps drifting lower while the output keeps increasing. This continues significantly beyond the deadtime of the process, but it shouldn't. The deadtime is about 20 seconds and ① to ② represents about 40 samples at 5 sec/sample, so about 200 seconds.

② This is where the input stops decreasing, so it finally begins responding to the increasing output.

⬍ This shows the difference in output required to get a response on the input. It appears to be around 8-10 ms. Note that the output value is in milliseconds and the windowSize (samplePeriod) is 5000ms. It will require a change in output of 8.33 ms (any direction) before the SSR will be able to add or subtract ½ AC cycle of output power.

This is the plot with setpoint at 80°C and the same tuning constants:

image

Same effect where the input drifts in opposite direction to the direction of output control. If it's due to the ½ AC cycle of power resolution, then I don't think its possible to tune this variation away, although using a smaller window size might help.

In the next plot, I tried oscillating an offset to the output in order to coerce a response from the input. I used a new variable (outputCorr) for the softPWM output control function, so as not to interfere with the Output variable seen by the PID.

It works using logic by offsetting outputCorr by -9 while the error is positive and +9 while the error is negative and using offset of 0 when there is no error.

Using the same tuning constants, here's the results:

image

Now there's just a small amount of ripple left in the input temperature due to the slow PID rate, but this should improve by reducing the window size.

@geleos27
Copy link

geleos27 commented Jan 27, 2022

"It works using logic by offsetting outputCorr by -9 while the error is positive and +9 while the error is negative and using offset of 0 when there is no error." - really interesting approach. How did you found offset value "9"? Aproximate output difference between points 1 and 2?

@Dlloydev
Copy link
Owner

Dlloydev commented Jan 27, 2022

Yes, but not measured, just the next integer higher than ±8.33ms which is the timing for ½ cycle @ 60Hz AC. For 50Hz AC, ½ cycle is 10ms, so I'll try a value of 12 which should work with SSRs connected to 50Hz or 60Hz AC plus an extra ±1ms for software timing variances. The idea is to force ±½ cycle power control while operating within the dead-band area between each AC ½ cycle. Basically it's to enhance power control resolution.

For the next test, I'll post a plot with setpoint at only 50°C using a 10x shorter window size (500ms) and using ±12 offset.

Edit: The best improvement was to use an offset just lower than the ½ cycle timing so here's the plot using ZN_PID, setpoint = 50°C, window size 500ms and ±8 offset:

image

The software PWM function for SSR relay control:

void softPwm() {
  if (Input > Setpoint) outputCorr = Output - 8;
  else if (Input < Setpoint) outputCorr = Output + 8;
  else  outputCorr = Output;
  if (outputCorr < 0) outputCorr = 0;
  if (!relayStatus && outputCorr > (msNow - windowStartTime)) {
    if (msNow > nextSwitchTime) {
      nextSwitchTime = msNow + debounce;
      relayStatus = true;
      digitalWrite(relayPin, HIGH);
    }
  } else if (relayStatus && outputCorr < (msNow - windowStartTime)) {
    if (msNow > nextSwitchTime) {
      nextSwitchTime = msNow + debounce;
      relayStatus = false;
      digitalWrite(relayPin, LOW);
    }
  }
}

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 1, 2022

Ive updated sTune and examples. The softPWM function is now part of the sTune class. The examples are now categorized by hardware setup. Here's the wiki page for Examples_MAX31856_PTC_SSR

Just today I've received a MAX6675 module, so I'll be able to eventually create examples and wiki page for this (time permitting).

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 3, 2022

sTune 2.3.1

New examples and reference is ready for the MAX6675 hardware setup.
Here's the wiki page: Examples_MAX6675_PTC_SSR

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 5, 2022

@robcazzaro

I've updated your previous example in case you're interested in giving it a run with the latest revision of sTune (2.3.2). It would be interesting to see the plot results.

/********************************************************************************
  sTune QuickPID Adaptive Control Example (robcazzaro)
  This sketch does on-the-fly tunning and PID SSR control of a PTC heater
  using the ZN_PID tuning method. When sTune completes, we need the input
  to approach setpoint for the first time. As the input gets closer, the gains
  (tunings) are gradually applied. This helps keep the integral term to a
  minimum to help reduce initial overshoot. Final overshoot correction is made
  at the first crossover from setpoint. Here, a full AC cyle is dropped from
  the output and 100% PID control resumes with ½ AC cycle optimized SSR control.

  Open the serial plotter to view the graphical results.
  Reference: https://github.com/Dlloydev/sTune/wiki/Examples_MAX6675_PTC_SSR
  *******************************************************************************/

#include <Arduino.h>
#include <sTune.h>
#include <QuickPID.h>
#include "Wire.h"
#include <LiquidCrystal.h>
#include <max6675.h>

// ***** PIN ASSIGNMENT *****
int ssrPin = 11;
int thermocoupleSOPin = A3;
int thermocoupleCSPin = A4;
int thermocoupleCLKPin = A5;

int lcdRsPin = 8;
int lcdEPin = 9;
int lcdD4Pin = 4;
int lcdD5Pin = 5;
int lcdD6Pin = 6;
int lcdD7Pin = 7;

// pins
const uint8_t inputPin = 0;
const uint8_t outputPin = ssrPin;
const uint8_t ledPin = LED_BUILTIN;

// user settings
uint32_t settleTimeSec = 15;
uint32_t testTimeSec = 1000;
const uint16_t samples = 500;
const float inputSpan = 240;
const float outputSpan = 2000;
float outputStart = 0;
float outputStep = 400;
float tempLimit = 150;
uint8_t debounce = 1;
uint8_t  startup = 0;

// variables
float Input, Output, Setpoint = 100, Kp, Ki, Kd;

sTune tuner = sTune(&Input, &Output, tuner.ZN_PID, tuner.direct5T, tuner.printALL);
QuickPID myPID(&Input, &Output, &Setpoint);
LiquidCrystal lcd(lcdRsPin, lcdEPin, lcdD4Pin, lcdD5Pin, lcdD6Pin, lcdD7Pin);
MAX6675 thermocouple(thermocoupleCLKPin, thermocoupleCSPin, thermocoupleSOPin);

void setup()
{
  pinMode(outputPin, OUTPUT);
  pinMode(ledPin, OUTPUT);
  digitalWrite(outputPin, LOW);
  Serial.begin(57600);
  tuner.Configure(inputSpan, outputSpan, outputStart, outputStep, testTimeSec, settleTimeSec, samples);
  tuner.SetEmergencyStop(tempLimit);
  // Start-up splash
  lcd.begin(16, 2);
  lcd.clear();
}

void loop()
{
  float optimumOutput = tuner.softPwm(outputPin, Input, Output, Setpoint, outputSpan, debounce);

  switch (tuner.Run())
  {
    case tuner.sample:                          // active once per sample during test
      Input = thermocouple.readCelsius();
      lcd.setCursor(0, 1);
      lcd.print(Input);
      lcd.print("C pwm=");
      lcd.print((int)Output);
      lcd.print(" ");
      //Serial.println(Input);
      tuner.plotter(Input, Output * 0.5, Setpoint, 1, 3);
      break;

    case tuner.tunings:                                 // active just once when sTune is done
      tuner.GetAutoTunings(&Kp, &Ki, &Kd);              // sketch variables updated by sTune
      myPID.SetOutputLimits(0, outputSpan * 0.5);
      myPID.SetSampleTimeUs(outputSpan * 1000 * 0.2);
      debounce = 0;                                     // switch to SSR optimum cycle mode
      myPID.SetMode(myPID.Control::automatic);          // the PID is turned on
      myPID.SetProportionalMode(myPID.pMode::pOnMeas);
      myPID.SetAntiWindupMode(myPID.iAwMode::iAwClamp);
      myPID.SetTunings(Kp, 0, 0);                       // update PID with the new Kp (P-controller)
      break;

    case tuner.runPid:                                  // active once per sample after case "tunings"
      Input = thermocouple.readCelsius();
      lcd.setCursor(0, 1);
      lcd.print(Input);
      lcd.print("C pwm=");
      lcd.print((int)Output);
      lcd.print(" ");
      //Serial.println(Input);

      if (Input > Setpoint && startup == 6) {
        Output -= 9; // drop half AC cycle
        myPID.SetMode(myPID.Control::manual);     // toggle PID control mode
        myPID.SetMode(myPID.Control::automatic);  // now PID uses the new output value
        startup++;
      } else if (Input > Setpoint - 3 && startup == 5) {
        myPID.SetTunings(Kp, Ki, Kd);  // 100% of gains
        startup++;
      } else if (Input > Setpoint - 6 && startup == 4) {
        myPID.SetTunings(Kp, Ki * 0.8, Kd * 0.8);  // 80% of gains
        startup++;
      } else if (Input > Setpoint - 9 && startup == 3) {
        myPID.SetTunings(Kp, Ki * 0.6, Kd * 0.6);  // 60% of gains
        startup++;
      } else if (Input > Setpoint - 12 && startup == 2) {
        myPID.SetTunings(Kp, Ki * 0.4, Kd * 0.4);  // 40% of gains
        startup++;
      } else if (Input > Setpoint - 15 && startup == 1) {
        myPID.SetTunings(Kp, Ki * 0.2, Kd * 0.2);  // 20% of gains
        startup++;
      } else if (Input > Setpoint - 18 && startup == 0) {
        myPID.SetTunings(Kp, Ki * 0.1, Kd * 0.1);  // 10% of gains
        startup++;
      }
      myPID.Compute();
      tuner.plotter(Input, optimumOutput * 0.5, Setpoint, 1, 3);
      break;
  }
}

@craig02445
Copy link

craig02445 commented Feb 5, 2022 via email

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 5, 2022

@craig02445
Sure,
I thought I'd try starting a new discussion (here) so this could have a separate topic. If you could provide the part# or link to the SSR, a simple circuit diagram and your test code, that would be great. I'd be happy to take a look at it and help with the code and suggestions.
-David

@robcazzaro
Copy link
Author

robcazzaro commented Feb 7, 2022

Apologies for the delay in replying, @Dlloydev , but I was working on another project and didn't have access to the hardware.

I cut&pasted your modified code and run it as is. The results are really puzzling, not just for how slow the auto tuning was (35 minutes!), but also when switching to normal mode, the output was never above 200 (out of 2000), so the temperature stays around 80C, never reaching the 100C set temperature. I'm including the whole log as a txt file (given how long it is)

The calculated parameters (Kp: 6.75, Ki: 0.08, Kd: 0.32) are roughly one order of magnitude too small. It looks as if there's a missing 0 somewhere in the example parameters (it's very slow to increase temperature, and still overshoots to above 130C)

sTune example.txt

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 8, 2022

Thanks,

The modified code did a direct5T test, so this actually completes in 3-4 time constants, so that's 3-4 x 9.4 minutes on your system. I had the test run with a lower step value so that it would do a complete tuning test below setpoint, then gradually apply the new PID gains, but yes, the gains did come out too small. I've still got some work to do with the software and lot's of testing.

I've reformatted your latest test results and pasted / saved it here in the online PID Tuner which is a great tool. Here, you can view the plot, view or change / re-scale its identified gains and more. It identified the time constant tau to be 848.6 sec (14 min). Using the "scale gains" slider, the overshoot would disappear with Kp = 25, Ki = 0.03, Kd = 0. If I manually entered the gains from sTune, the plot would reveal the oscillations.

Click the "back" button 3 times to see the data table and the plot in more detail.

@robcazzaro
Copy link
Author

Thanks for the additional info. I tried using Kp=25, Ki=0.03 and Kd=0, but the system is super slow to reach the 100C set temperature. It reaches 90C after roughly 180 seconds, but reaches 100C only after 1300 seconds (more than 20 minutes), which is way too long. No overshot, clearly :)

I got these values from running Brett's autotune, Kp=28, Ki=0.17 and Kd=15, and with those I get a 10C overshot after roughly 120 seconds, then it stabilizes at 100C after 500 seconds.

My ideal tuning would be for a system that reaches 100C in under 240 seconds, with no more than 5C overshot.

@Dlloydev
Copy link
Owner

Dlloydev commented Feb 9, 2022

I tried using Kp=25, Ki=0.03 and Kd=0, but the system is super slow to reach the 100C set temperature. It reaches 90C after roughly 180 seconds, but reaches 100C only after 1300 seconds (more than 20 minutes), which is way too long. No overshot, clearly :)

Oh, that was just playing with the settings to dial out the overshoot.

My ideal tuning would be for a system that reaches 100C in under 240 seconds, with no more than 5C overshot.

This specification helps ... here's some gains I've tried with each example getting more aggressive. Note the plot's time scale is zoomed in to max to reveal the response in just the first 342 seconds.

Kp = 50, Ki = 0.0625, Kd = 0

image

Kp = 100, Ki = 0.125, Kd = 0

image

Kp = 150, Ki = 0.1875, Kd = 0

image

I got these values from running Brett's autotune, Kp=28, Ki=0.17 and Kd=15, and with those I get a 10C overshot after roughly 120 seconds, then it stabilizes at 100C after 500 seconds.

image
Here, if you make Kd = 0, the overshoot might be a bit higher, but overall the output and possibly the input will be a bit smoother. If you leave Ki at 0.17, then try higher Kp values (i.e. 50, 100, 150), the overshoot will progressively decrease and the response will get faster.

I've made some fixes to sTune and will push an update after I get a chance to do more testing. My goal is to have sTune determine gains that are a closer match to the gains determined by the online PID Tuner.

@geleos27
Copy link

geleos27 commented Feb 9, 2022

I've made some fixes to sTune and will push an update after I get a chance to do more testing. My goal is to have sTune determine gains that are a closer match to the gains determined by the online PID Tuner.

pidtuner really helpfull, but please pay attention to bottom graph also. Sometimes to achieve faster response pidtuner offers both positive and negative CV, but for heaters it's impossible =(

power

and there is no way in pidtuner to consider any constrains (if you have any). Which may drammaticaly cause on results while using gains provided to pidtuner.

If robcazzaro have transition from room ambient ~25C degree to 100C we will get 75 degree transition.

and the PWM range is 0 to 2000

power2

looks like with provided gains CV will be limited and results may be confusing.

@Dlloydev
Copy link
Owner

sTune 2.4.0 is published. Includes various fixes, updated and new examples and documentation.

Repository owner locked and limited conversation to collaborators Jun 7, 2022
@Dlloydev Dlloydev converted this issue into discussion #14 Jun 7, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants