Expose integral for user-space anti-windup access
This code adds
double GetIntegral(void) ; // returns the internal state of the integrator
void SetIntegral(double) ; // Sets the internal state of the integrator
example/PID_SimulatedHeater/PID_SimulatedHeater.ino // a simulation of a heater block
See https://wokwi.com/projects/358122536159671297 for a live simulation on Wokwi
These two functions provide access to the integrator to enable user-space monitoring and anti-windup schemes.
In response to Issue https://github.com/br3ttb/Arduino-PID-Library/issues/125
Here's a sample user-space anti-windup scheme on that example:
// user-space anti-windup method:
if((myPID.Get(Kp) * (Setpoint-Input) + myPID.GetIntegral())>255){
myPID.SetIntegral(0); // PD only below proportional zone
}
And a simulation using it:
https://wokwi.com/projects/358125689261331457
I encourage you to use Serial.print to output the Setpoint,Input,Output, and GetIntegral() over time. what you see might surprise you. Because of the way that the integral is calculated http://brettbeauregard.com/blog/2011/04/improving-the-beginner%E2%80%99s-pid-tuning-changes/, outputSum isn't some abstract number as it might be in a different algorithm, where it's held and constantly multiplied by Ki. Instead, it's going to be on the same scale as the Output. if you have SetOutputLimits(0,255), not only is the output going to stay within 0-255, so is outputSum.
This difference in how integral is calculated makes many of the published windup limiting methods either unnecessary or not-applicable. Windup, in the traditional sense http://brettbeauregard.com/blog/2011/04/improving-the-beginner%e2%80%99s-pid-reset-windup/, means that the integral has continued to grow despite no longer having an impact on the output (which is limited.) This algorithm doesn't do that.
In the hobby space, perhaps because some poorly-written versions DO wind up, windup seems to have become shorthand for "the output isn't doing what I want it to do". That's a legitimate issue. Often-times it can be corrected by tuning, but tuning well can be really hard. (There's a reason the company I work for makes good money selling tuning software!) In a hobby situation, where you have access to the code, it's just easier (and relatively harmless) to just hack the output into submission.
When it comes to this library, however, I just can't rationalize rolling in changes like this. 1) There's already a way to "pick up and move" the output to where it needs to be (mode=manual,change output, mode=auto) 2) I worked hard to make this library easy to use/implement. Every additional setting/function makes it more complex, so the value-add needs to match.
That being said, I LOVE LOVE LOVE that the people that want to just hack in their change are free to do so. Just because I'm precious about this doesn't mean that the hackers of the world need to suffer!
On Thu, Mar 2, 2023 at 4:42 PM drf5n @.***> wrote:
Here's a sample user-space anti-windup scheme on that example:
// user-space anti-windup method: if((myPID.Get(Kp) * (Setpoint-Input) + myPID.GetIntegral())>255){ myPID.SetIntegral(0); // PD only below proportional zone }— Reply to this email directly, view it on GitHub https://github.com/br3ttb/Arduino-PID-Library/pull/132#issuecomment-1452587791, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACYX4VSIW4CCO4LWGJFJODW2EH5JANCNFSM6AAAAAAVN4PYSI . You are receiving this because you are subscribed to this thread.Message ID: @.***>
-- Brett
Thanks.
How about just adding a GetIntegral() ?
I love the way your code does the integration into the output units into outputSum. That simplifies so many things. Other code that doesn't maintain the integral state variable in output units makes it much harder to tune their workarounds with things like SetIntegralLimits (would one update integral limits if you change Ki.
What I really like about this change compared to many of the other suggestions is that it doesn't change or complicate the PID::Compute() loop.
Reading your note I see that my PID::SetIntegral(double value) can be done in user-space with something like
void myPidSetIntegral(double value){
myPid.SetMode(MANUAL);
Output = value;
myPid.SetMode(AUTO);
}
// or maybe
void mySetIntegral(PID * ptrPID,double value ){
ptrPID->SetMode(MANUAL);
Output = value;
ptrPID->SetMode(AUTO);
}
//with
mySetIntegral(&myPID,value)
But PID::GetIntegral() can't be done in userspace, since it is private inside the black box.
Example code using this user space void mySetIntegral(&myPID,value) scheme:
/********************************************************
PID Basic simulated heater Example with userspace Integral Override
Reading analog input 0 to control analog PWM output 3
********************************************************/
// This simulates a 20W heater block driven by the PID
// Vary the setpoint with the Pot, and watch the heater drive the temperature up
//
// Simulation at https://wokwi.com/projects/358122536159671297
//
// Based on
// Wokwi https://wokwi.com/projects/357374218559137793
// Wokwi https://wokwi.com/projects/356437164264235009
#include <PID_v1.h> // https://github.com/br3ttb/Arduino-PID-Library
//Define Variables we'll be connecting to
double Setpoint, Input, Output;
//Specify the links and initial tuning parameters
double Kp = 17, Ki = 0.3, Kd = 2; // works reasonably with sim heater block
//double Kp = 255, Ki = .0, Kd = 0; // works reasonably with sim heater block
//double Kp = 2, Ki = 5, Kd = 1; // commonly used defaults
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, P_ON_E, DIRECT);
const int PWM_PIN = 3; // UNO PWM pin
const int INPUT_PIN = -1; // Analog pin for Input (set <0 for simulation)
const int SETPOINT_PIN = A1; // Analog pin for Setpoint Potentiometer
const int SETPOINT_INDICATOR = 6; // PWM pin for indicating setpoint
const int INPUT_INDICATOR = 5; // PWM pin for indicating Input
const int OVERRIDE_PIN = 12; // Pin to reset the PID output
void setup()
{
Serial.begin(115200);
Serial.println(__FILE__);
myPID.SetOutputLimits(-4, 255);
pinMode(OVERRIDE_PIN, INPUT_PULLUP);
if (SETPOINT_INDICATOR >= 0) pinMode(SETPOINT_INDICATOR, OUTPUT);
if (INPUT_INDICATOR >= 0) pinMode(INPUT_INDICATOR, OUTPUT);
Setpoint = 0;
//turn the PID on
myPID.SetMode(AUTOMATIC);
if(INPUT_PIN>0){
Input = analogRead(INPUT_PIN);
}else{
Input = simPlant(0.0,1.0); // simulate heating
}
Serial.println("Setpoint Input Output Watts");
}
void loop()
{
// gather Input from INPUT_PIN or simulated block
float heaterWatts = (int)Output * 20.0 / 255; // 20W heater
if (INPUT_PIN > 0 ) {
Input = analogRead(INPUT_PIN);
} else {
float blockTemp = simPlant(heaterWatts,Output>0?1.0:1-Output); // simulate heating
Input = blockTemp; // read input from simulated heater block
}
if (myPID.Compute())
{
analogWrite(PWM_PIN, (int)Output);
Setpoint = analogRead(SETPOINT_PIN) / 4; // Read setpoint from potentiometer
if (INPUT_INDICATOR >= 0) analogWrite(INPUT_INDICATOR, Input);
if (SETPOINT_INDICATOR >= 0) analogWrite(SETPOINT_INDICATOR, Setpoint);
}
if(digitalRead(OVERRIDE_PIN)==LOW) mySetIntegral(&myPID,50);
report();
}
void report(void)
{
static uint32_t last = 0;
const int interval = 1000;
if (millis() - last > interval) {
last += interval;
// Serial.print(millis()/1000.0);
Serial.print(Setpoint);
Serial.print(' ');
Serial.print(Input);
Serial.print(' ');
Serial.print(Output);
Serial.print(' ');
//Serial.print(myPID.GetIntegral()); // can't be done
Serial.print(' ');
Serial.println();
}
}
float simPlant(float Q,float hfactor) { // heat input in W (or J/s)
// simulate a 1x1x2cm aluminum block with a heater and passive ambient cooling
// float C = 237; // W/mK thermal conduction coefficient for Al
float h = 5 *hfactor ; // W/m2K thermal convection coefficient for Al passive
float Cps = 0.89; // J/g°C
float area = 1e-4; // m2 area for convection
float mass = 10 ; // g
float Tamb = 25; // °C
static float T = Tamb; // °C
static uint32_t last = 0;
uint32_t interval = 100; // ms
if (millis() - last >= interval) {
last += interval;
// 0-dimensional heat transfer
T = T + Q * interval / 1000 / mass / Cps - (T - Tamb) * area * h;
}
return T;
}
void mySetIntegral(PID * ptrPID,double value ){
ptrPID->SetMode(MANUAL);
Output = value;
ptrPID->SetMode(AUTOMATIC);
}
I proposed a simpler change at https://github.com/br3ttb/Arduino-PID-Library/pull/133
It exposes already-existing private functions Initialize() and outputSum at zero cost, and would make this PR redundant.
I encourage you to use Serial.print to output the Setpoint,Input,Output, and GetIntegral() over time.
This simulation enables exactly that by adding GetIntegral() to its copy of the PID_v1 code (Otherwise, the integral is inaccessible.):
https://wokwi.com/projects/358190033668210689
There's a slide-pot to vary the setpoint, and the button kicks the integral with the user-space MANUAL-change-AUTOMATIC trick.
What has been surprising to me is how different PID_v1 behaves compared to simple commercial PIDs and how different the tuning might need to be. How does a cheap Amazon PID controller still differ from PID_v1?