Go Down

Topic: Writing a P.I.D. algorithm to controll a pump process (Read 190 times) previous topic - next topic

everone

Hello,

     Anyone familiar with P.I.D Process, Integral, Derivative form to continually process the best algorithm to bring a process value into its set point range.

I have experience using P.I.D. controllers but I don't have enough experience to write the C++ code.

any help would be extremely great.

I am using a Mega r3 to read an analog input so I am receiving a value 0-1024 and that value needs to be continually processed into a voltage output 0-5v when the process is outside the Set Point value.

So ill have my

Set Point Value = in this case a pH value
Process Value = the actual current pH Value
Algorithm to control the process


excuse my ignorance.

jremington

No need to write the algorithm, that has been done quite well here.

The main problems for you will be measuring the pH and controlling the pump.

When you can do both of those, then you will need to tune the PID parameters, Ki, Kd and Kp. There are many on line guides to general approaches.

1bit

struct PIDloop {
   double pvh;
   double pvl;
   double pv;
   double sp;
   double lpv;
   double lsp;
   double out;
   double kc;
   double td;
   double ti;
   double ddcout;
   double yn;
   double mx;
   double kd;
   char *mode;

};

void pid (struct PIDloop *lptr, double ts)  /* function doing the pid loop  JFL*/
{
   double yn1,err;

   lptr->lpv=(lptr->pv-lptr->pvl)/(lptr->pvh-lptr->pvl);
   lptr->lsp=(lptr->sp-lptr->pvl)/(lptr->pvh-lptr->pvl);
   err=lptr->lsp-lptr->lpv;

   yn1=lptr->yn+ts/(ts+lptr->td/lptr->kd)*(lptr->lpv-lptr->yn);
   lptr->mx+=err*lptr->kc*ts/lptr->ti;

   lptr->out=lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts+lptr->mx;
   if (lptr->out >1.0){
      lptr->out=1.0;
      lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
      }
   if (lptr->out <0.0){
      lptr->out=0.0;
      lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
      }

   lptr->yn=yn1;
}

MarkT

A great example of how not to code!

The only comment explains the only self-obvious feature!
[ I will NOT respond to personal messages, I WILL delete them, use the forum please ]

1bit

You could use gain scheduling.

But output scheduling would be a good choice for pH as it is nonlinear.

Also you could use model predictive control if you have a titration curve for the fluid.

here is a really raw output scheduler. you need to adjust the sheduling tables based upon the fluids and pumps and output types.

////

struct PIDloop {
  double pvh;
  double pvl;
  double pv;
  double sp;
  double lpv;
  double lsp;
  double out;
  double kc;
  double td;
  double ti;
  double ddcout;
  double yn;
  double mx;
  double kd;
  char *mode;
};

//output scheduling tables six segments
float fgenxf[3][7][2] = {
  //first set xy
  0.01,0.1,
  0.22,0.3,
  0.4,0.5,
  0.6,0.7,
  0.8,0.9,
  0.90,0.11,
  0.9912,0.13,
  //second set xy
  0.02,0.15,
  0.16,0.17,
  0.18,0.19,
  0.20,0.21,
  0.922,0.23,
  0.924,0.25,
  0.99926,0.27,
  //third set xy
  0.0003,0.29,
  0.30,0.31,
  0.532,0.33,
  0.634,0.35,
  0.836,0.37,
  0.838,0.39,
  0.9999940,0.41};

float x[3];
float y[3];
float pidout;
float output;
int set;
long int millisnext;
struct PIDloop aic02; /*ph loop*/
const int AI02A = A2;    // name the A2 pin
const int FY01 = 7;    // name the base pin
const int FY02= 9;    // name the A2 pin
const int l_ts=200;//scan interval


void setup() {

  delay(5000);
  aic02.pvh = 8.0;
  aic02.pvl = 6.0;
  aic02.pv = 7.0;
  aic02.sp = 7.1;
  aic02.lpv = 1.0/2.0;
  aic02.lsp = 1.1/2.0;
  aic02.out = 0.5;
  aic02.kc = 1.1;
  aic02.td = 0.0001;
  aic02.ti = 60.0*60.0;
  aic02.ddcout = 0.0;
  aic02.yn = 0.44;
  aic02.mx = 0.5;
  aic02.kd = 1000.0;
  aic02.mode = "auto";
  Serial.begin(9600);

  int set = 0;
  millisnext= millis()+l_ts;
}

void loop() {
  aic02.pv = scalar(double (analog_measure(AI02A)), 0, 1023, aic02.pvl, aic02.pvh);
  pid(&aic02, l_ts);
  for (set=0;set<3;set++)
  {
    int index = fgn(aic02.out,set);
    if (7>index && index>-1){
      y[set] = fscalar(aic02.out, fgenxf [set] [index-1]
  • , fgenxf [set] [index]
  • , fgenxf [set] [index-1] [1], fgenxf [set][index][1]);

    }
    else
    {
      y[set] = aic02.out;
    }
  }
  analogWrite(FY01, int (scalar (aic02.out, 0.0, 0.49, 0.0, 255.0)));
  analogWrite(FY02, int (scalar (y[1], 0.51, 1.00, 0.0, 255.0)));
  while( millisnext> millis())
  {
  }
  if (millis()<l_ts){

    millisnext = millis();
  }
  else{
    millisnext+=l_ts;
  }

}

void pid (struct PIDloop *lptr, double ts)  /* function doing the pid loop  JFL*/
{
  double yn1,err;

  lptr->lpv=(lptr->pv-lptr->pvl)/(lptr->pvh-lptr->pvl);
  lptr->lsp=(lptr->sp-lptr->pvl)/(lptr->pvh-lptr->pvl);
  err=lptr->lsp-lptr->lpv;

  yn1=lptr->yn+ts/(ts+lptr->td/lptr->kd)*(lptr->lpv-lptr->yn);
  lptr->mx+=err*lptr->kc*ts/lptr->ti;

  lptr->out=lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts+lptr->mx;
  if (lptr->out >1.0){
    lptr->out=1.0;
    lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
  }
  if (lptr->out <0.0){
    lptr->out=0.0;
    lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
  }

  lptr->yn=yn1;
}
//interpolate float schedule table
float fscalar(float x, float in_min,  float in_max, float out_min, float out_max)
{
  return ( (x) - (in_min)) * (out_max - out_min) / ( (in_max) - (in_min)) + out_min;
}
//scan the output schedule table
int fgn(float x, int i){
  int count = 0;
  int scount = -1;
  boolean found = false;
  for (count = 0; count<7;count++){
    if (fgenxf[count][0]< x) scount = count;
  }
  return (scount);
}
//scale the integer to the real value
double scalar(double x, double in_min,  double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
double analog_measure(int analogpin)
{
  analogRead(analogpin);
}


everone

A great example of how not to code!

The only comment explains the only self-obvious feature!
How would you do it then?

Please explain the code so I can understand what parts control what processes?

Thank you,

1bit

Use output scheduling in lookup tables to do this.

like they show here...
http://www.slideshare.net/EmersonExchange/fieldbus-tutorial-part-3-example-applications


Quote
struct PIDloop {
  double pvh;
  double pvl;
  double pv;
  double sp;
  double lpv;
  double lsp;
  double out;
  double kc;
  double td;
  double ti;
  double ddcout;
  double yn;
  double mx;
  double kd;
  char *mode;
};

//output scheduling tables six segments
float fgenxf[3][7][2] = {
  //first set xy
  0.01,0.1,
  0.22,0.3,
  0.4,0.5,
  0.6,0.7,
  0.8,0.9,
  0.90,0.11,
  0.9912,0.13,
  //second set xy
  0.02,0.15,
  0.16,0.17,
  0.18,0.19,
  0.20,0.21,
  0.922,0.23,
  0.924,0.25,
  0.99926,0.27,
  //third set xy
  0.0003,0.29,
  0.30,0.31,
  0.532,0.33,
  0.634,0.35,
  0.836,0.37,
  0.838,0.39,
  0.9999940,0.41};

float x[3];
float y[3];
float pidout;
float output;
int set;
long int millisnext;
struct PIDloop aic02; /*ph loop*/
const int AI02A = A2;    // name the A2 pin measurement
const int FY01 = 2;    // name the BASE pin
const int FY02= 9;    // name the ACID pin
const int l_ts=200;//scan interval  ms


void setup() {

  delay(5000);
  aic02.pvh = 8.0;
  aic02.pvl = 6.0;
  aic02.pv = 7.0;
  aic02.sp = 7.1;
  aic02.lpv = 1.0/2.0;
  aic02.lsp = 1.1/2.0;
  aic02.out = 0.5;
  aic02.kc = 1.1;
  aic02.td = 0.0001;
  aic02.ti = 60.0*60.0;
  aic02.ddcout = 0.0;
  aic02.yn = 0.44;
  aic02.mx = 0.5;
  aic02.kd = 1000.0;
  aic02.mode = "auto";
  Serial.begin(9600);

  int set = 0;
  millisnext= millis()+l_ts;
}

void loop() {
  aic02.pv = scalar(double (analog_measure(AI02A)), 0, 1023, aic02.pvl, aic02.pvh);
  pid(&aic02, l_ts);
  for (set=0;set<3;set++)
  { 
    int index = fgn(aic02.out,set);
    if (7>index && index>0){
     // Serial.println("intable");
      y[set] = fscalar(aic02.out, fgenxf [set][index-1][0], fgenxf [set][index][0], fgenxf [set][index-1][1], fgenxf [set][index][1]);
    }
    else
    {
      //Serial.println("NOPE");
      y[set] = aic02.out;
    }
  }

  analogWrite(FY01,constrain(scalar (y[1], 0.0, 0.49, 0.0, 255.0),0,255));
  analogWrite(FY02, constrain(scalar (y[2], 0.51, 1.00, 0.0, 255.0),0,255));
  while( millisnext> millis())
  {
   //nothin
  }
  if (millis()<l_ts){

    millisnext = millis();
  }
  else{
    millisnext+=l_ts;
  }

}
// function doing the pid loop
void pid (struct PIDloop *lptr, double ts) 
{
  double yn1,err;

  lptr->lpv=(lptr->pv-lptr->pvl)/(lptr->pvh-lptr->pvl);
  lptr->lsp=(lptr->sp-lptr->pvl)/(lptr->pvh-lptr->pvl);
  err=lptr->lsp-lptr->lpv;

  yn1=lptr->yn+ts/(ts+lptr->td/lptr->kd)*(lptr->lpv-lptr->yn);
  lptr->mx+=err*lptr->kc*ts/lptr->ti;

  lptr->out=lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts+lptr->mx;
  if (lptr->out >1.0){
    lptr->out=1.0;
    lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
  }
  if (lptr->out <0.0){
    lptr->out=0.0;
    lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
  }

  lptr->yn=yn1;
}
//interpolate float schedule table
float fscalar(float x, float in_min,  float in_max, float out_min, float out_max)
{
  return ( (x) - (in_min)) * (out_max - out_min) / ( (in_max) - (in_min)) + out_min;
}
//scan the output schedule table
int fgn(float x, int i){
  int count = 0;
  int scount = -1;
  boolean found = false;
  for (count = 0; count<7;count++){
    if (fgenxf[count][0]< x) scount = count;
  }
  return (scount);
}
//scale the integer to the real value
double scalar(double x, double in_min,  double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
double analog_measure(int analogpin)
{
  analogRead(analogpin);
}







everone

Thank you ibit,
ill try and decipher this into layman's terms.

1bit

Really, the look up tables let you compensate for the fluid abruptly changing pH.  you want the integral action to have some time when you are close to target.  Let the solution mix so to speak.  The other thing output scheduling does for you is let you compensate for the reagent strengths and the pump characteristics independently.  Some pumps are linear others not so.  I would also leave a very small gap with no action between the two pump outputs to lower reagent possibility of counter titration/ neutralization.


Just one opinion.... you are using  PWM, but you might also switch to a duty cycle arrangement if you have on off pumps,,,jut be sure to run the cycle time longer than your mixing time to avoid over adding reagents.  in this way you could vary the duty cycle length too as you approach SP. so as to give the solution a chance to ge whatever you are trying to neutralize.....the idea of feed forward is also good if you can measure what is coming in to your tank.

It is important to make the inlet and the addition point close together and as close as you can to the mixer if you have one.


Regards

IF you look you may get in touch.






Go Up
 


Please enter a valid email to subscribe

Confirm your email address

We need to confirm your email address.
To complete the subscription, please click the link in the email we just sent you.

Thank you for subscribing!

Arduino
via Egeo 16
Torino, 10131
Italy