Skip to content
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

Expose integral for user-space anti-windup access #132

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

drf5n
Copy link

@drf5n drf5n commented Mar 2, 2023

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 #125

@drf5n
Copy link
Author

drf5n commented Mar 2, 2023

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

@br3ttb
Copy link
Owner

br3ttb commented Mar 3, 2023 via email

@drf5n
Copy link
Author

drf5n commented Mar 3, 2023

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);
}

@drf5n
Copy link
Author

drf5n commented Mar 3, 2023

I proposed a simpler change at #133

It exposes already-existing private functions Initialize() and outputSum at zero cost, and would make this PR redundant.

@drf5n
Copy link
Author

drf5n commented Mar 3, 2023

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants