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 to enable user-space hacking #133

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

drf5n
Copy link

@drf5n drf5n commented Mar 3, 2023

Based on the discussion in #132 (comment)
I realized that a low impact way of handling the issues would be to just move some features out of the black box.

Moving the PID::Initialize() function out to public makes the SetMode(MANUAL);Output=value;SetMode(MANUAL); trick easier to discuss and handle.

Moving the PID::outputSum variable from private to public makes it possible to examine (or hack on) the internal integrator state.

Since these functions already exist, it should be a no-cost change and enables easy understanding of what the algorithm does and easy adaptation to special uses.

A live example:

https://wokwi.com/projects/358190033668210689

image

This minimal-impact change would obviate #132

/********************************************************
   PID Basic simulated heater Example
   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
// local copy of .h and .cpp are tweaked to expose the integral per
// https://github.com/br3ttb/Arduino-PID-Library/pull/132#issuecomment-1453839425

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

//Specify the links and initial tuning parameters
double Kp = 17, Ki = .1, Kd = 2; // works reasonably with sim heater block 
//double Kp = 255, Ki = .0, Kd = 0; // works reasonably with sim heater block 
//double Kp = 255, Ki = 0.0, Kd = 0.0; // bang-bang
//double Kp = 2, Ki = 0.0, Kd = 0.0; // P-only
//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 AUTOMATIC_PIN = 8; // Pin for controlling manual/auto mode
const int OVERRIDE_PIN = 12; // Pin for integral override

void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  myPID.SetOutputLimits(-4, 255);
  pinMode(OVERRIDE_PIN, INPUT_PULLUP);
  pinMode(AUTOMATIC_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 Integral");
}

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,0); // integral override
  if(digitalRead(AUTOMATIC_PIN)==HIGH !=  myPID.GetMode()==AUTOMATIC){
    myPID.SetMode(digitalRead(AUTOMATIC_PIN)==HIGH ? AUTOMATIC :MANUAL);

  }

  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("SP:");Serial.print(Setpoint);
    Serial.print(" PV:");
    Serial.print(Input);
    Serial.print(" CV:");
    Serial.print(Output);
    Serial.print(" Int:");
    Serial.print(myPID.GetIntegral());
    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 13, 2023

Here's an easier-to-fiddle with simulation example:

https://wokwi.com/projects/359088752027305985

image

The Auto/Manual, Minus/Plus and "Manual/Output=0/Auto" trick controls used in this simulation are all available with the stock PID_v1 code. This sim only adds exposing the outputSum internal variable to the UI.

/********************************************************
   PID Basic simulated heater Example
   Reading simulated 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/359088752027305985
//
//  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
// local copy of .h and .cpp are tweaked to expose the integral per
// https://github.com/br3ttb/Arduino-PID-Library/pull/132#issuecomment-1453839425
#define USE_HACK   // access the PID.outputSum variable

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

//Specify the links and initial tuning parameters
//double Kp = 20, Ki = .01, Kd = 10; // works reasonably with sim heater block fo 220deg
double Kp = 25.5, Ki = 0.1, Kd = 0; // +/-10°proportional band 
//double Kp = 255, Ki = 0.05, Kd = 0; // works reasonably with sim heater block 
//double Kp = 255, Ki = .0, Kd = 0; // +/-1° proportional band works reasonably with sim heater block 
//double Kp = 10000, Ki = 0.0, Kd = 0.0; // bang-bang
//double Kp = 2, Ki = 0.0, Kd = 0.0; // P-only
//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 for Output
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 AUTOMATIC_PIN = 8; // Pin for controlling manual/auto mode, NO
const int OVERRIDE_PIN = 12; // Pin for integral override, NO
const int PLUS_PIN = 4; // Pin for integral override, NO
const int MINUS_PIN =7; // Pin for integral override, NO


#include <LiquidCrystal_I2C.h>

#define I2C_ADDR    0x27
#define LCD_COLUMNS 20
#define LCD_LINES   4

LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);

void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  myPID.SetOutputLimits(0, 255); // -4 for 
  pinMode(OVERRIDE_PIN, INPUT_PULLUP);
  pinMode(AUTOMATIC_PIN, INPUT_PULLUP);
  pinMode(MINUS_PIN, INPUT_PULLUP);
  pinMode(PLUS_PIN, INPUT_PULLUP);
  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
  }
  lcd.init();
  lcd.backlight();

  Serial.println("Setpoint Input Output Integral");
}

void loop()
{
  // gather Input from INPUT_PIN or simulated block
  float heaterWatts = 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())
  {
    //Output = (int)Output; // Recognize that the output as used is integer
    analogWrite(PWM_PIN, Output);

  }

  Setpoint = analogRead(SETPOINT_PIN) / 4; // Read setpoint from potentiometer
  if(digitalRead(OVERRIDE_PIN)==LOW) mySetIntegral(&myPID,0); // integral override
  if(digitalRead(AUTOMATIC_PIN)==HIGH !=  myPID.GetMode()==AUTOMATIC){
    myPID.SetMode(digitalRead(AUTOMATIC_PIN)==HIGH ? AUTOMATIC :MANUAL);
  }
  static uint32_t lastButton = 0;
  if(myPID.GetMode()==MANUAL && millis() - lastButton > 250){
    if(digitalRead(PLUS_PIN)==LOW){ 
      Output += 1;
      lastButton = millis();
    }
    if(digitalRead(MINUS_PIN)==LOW){
      Output -= 1;
      lastButton = millis();
    }
  }

  report();
  reportLCD();

}

void report(void)
{
  static uint32_t last = 0;
  const int interval = 250;
  if (millis() - last > interval) {
    last += interval;
    //    Serial.print(millis()/1000.0);
    Serial.print("SP:");Serial.print(Setpoint);
    Serial.print(" PV:");
    Serial.print(Input);
    Serial.print(" CV:");
    Serial.print(Output);
    Serial.print(" Int:");
#if defined(USE_HACK)
    Serial.print(myPID.outputSum);
#endif
    Serial.print(' ');
    Serial.println();
  }
}

void reportLCD(void)
{
  static uint32_t last = 0;
  const int interval = 250;
  if (millis() - last > interval) {
    last += interval;
    //    Serial.print(millis()/1000.0);
   // lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("PV:");
    lcd.print(Input,3);
    lcd.print(" CV:");
    lcd.print(Output,3);
    lcd.print("  ");
    lcd.setCursor(0,1);
    lcd.print("SP:");
    lcd.print(Setpoint,3);
    lcd.print(myPID.GetMode()==AUTOMATIC? " Automatic ":" Manual   ");
    lcd.print("  ");
    lcd.setCursor(0,3);
    lcd.print("Int:");
#if defined(USE_HACK)
    lcd.print(myPID.outputSum,4);
#endif
    lcd.print(' ');
    lcd.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 drf5n mentioned this pull request Apr 22, 2023
@drf5n
Copy link
Author

drf5n commented Apr 22, 2023

Here's an example of using user-space hacking to implement conditional integration:

https://wokwi.com/projects/362722038135838721

The key bit of code is:

  float outputSumSave = myPID.outputSum; /// save for conditional integration
  if (myPID.Compute())
  {
    if(Output == 255 || Output == -4){ // undo integral update at limits
       myPID.outputSum = outputSumSave;
    }
   ...

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

1 participant