改進初學者的PID-測量的比例編碼

最近看到了Brett Beauregard發表的有關PID的系列文章,感覺對於理解PID算法很有幫助,於是將系列文章翻譯過來!在自我提高的過程中,也希望對同道中人有所幫助。作者Brett Beauregard的原文網址:http://brettbeauregard.com/blog/2017/06/proportional-on-measurement-the-code/

 

在上一篇文章中,我把所有的時間都花在解釋了比例測量的好處上。在這篇文章中,我將解釋代碼。人們似乎很欣賞我上次一步一步地解釋事情的方式,所以在此我也將採取這樣的方式。下面的3個步驟詳細介紹了我是如何將 PonM 添加到 PID 庫的。

第一階段–初始輸入和比例模式選擇

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double ITerm,lastInput;
double kp,ki,kd;
int SampleTime = 1000; //1 sec
double outMin,outMax;
bool inAuto = false;
  
#define MANUAL 0
#define AUTOMATIC 1
  
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
  
#define P_ON_M 0
#define P_ON_E 1
bool PonE = true;
double initInput;
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm > outMax) ITerm= outMax;
      else if(ITerm < outMin) ITerm= outMin;
      double dInput = (Input - lastInput);

      /*Compute P-Term*/
      if(PonE) Output = kp * error;
      else Output = -kp * (Input-initInput);
  
     /*Compute Rest of PID Output*/
      Output += ITerm - kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
  
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
  
void SetTunings(double Kp,double Ki,double Kd,int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
  
   PonE = pOn == P_ON_E;
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
   if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
  
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime/ (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
  
void SetOutputLimits(double Min,double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
  
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
  
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
  
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }

    inAuto = newAuto;
}
  
void Initialize()
{
   lastInput = Input;
   initInput = Input;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
  
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

隨着輸入的變化,測量的比例提供了越來越大的阻力,但如果沒有參照系,我們的表現會有些不穩定。如果我們第一次打開控制器時的PID輸入是10000,我們真的想從Kp*10000開始抵制嗎?不,我們希望使用我們的初始輸入作爲參考點 (第108行),從那裏開始隨着輸入的變化增加或減少阻力 (第38行)。

我們需要做的另一件事是允許用戶選擇是要在偏差上做比例或在測量上做比例。在最後一個帖子之後,它看起來像 PonE 是無用的,但重要的是要記住,對於許多回路,它工作的很好。因此,我們需要讓用戶選擇他們想要的模式 ,然後在計算中相應地操作。

第二階段–動態更改整定參數

雖然上面的代碼確實有效,但它有一個我們以前看到的問題。當整定參數在運行時發生更改,我們會得到一個不希望出現的信號。

爲什麼會這樣?

上次我們看到這一點時,是積分項被一個新的Ki 重新調整。而這一次,是比例項(輸入-初始輸入) 被新的Kp所更改。我選擇的解決方案類似於我們爲 Ki 所做的:我們不再是將(輸入-初始輸入)作爲一個整體乘以當前 Kp,而是我把它分解成單獨的步驟乘以當時的Kp:

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double ITerm,lastInput;
double kp,ki,kd;
int SampleTime = 1000; //1 sec
double outMin,outMax;
bool inAuto = false;
  
#define MANUAL 0
#define AUTOMATIC 1
  
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
  
#define P_ON_M 0
#define P_ON_E 1bool PonE = true;
double PTerm;
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
       double error = Setpoint - Input;
       ITerm+= (ki * error);
       if(ITerm > outMax) ITerm= outMax;
       else if(ITerm < outMin) ITerm= outMin;
       double dInput = (Input - lastInput);
      /*Compute P-Term*/
      if(PonE) Output = kp * error;
      else
      {
         PTerm -= kp * dInput;
         Output = PTerm;
      }
     
      /*Compute Rest of PID Output*/
      Output += ITerm - kd * dInput;
     
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
  
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
  
void SetTunings(double Kp,double Ki,double Kd,int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
   PonE = pOn == P_ON_E;
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;

   if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
  
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
  
void SetOutputLimits(double Min,double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
  
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
  
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
  
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
  
void Initialize()
{
   lastInput = Input;
   PTerm = 0;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
  
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

我們現在保留一個有效的和組成的P項,而不是將輸入-初始輸入作爲一個整體乘以Kp。在每一步中,我們只需將當前輸入變化乘以當時的Kp,並從P項(第41行) 中減去它。在這裏,我們可以看到變化的影響:

因爲舊的Kp是已經存儲,調整參數的變化只會影響我們後續的過程。

最終階段–求和問題。

我不會進入完整的細節 (花哨的趨勢等) 以及上述代碼有什麼問題。這相當不錯,但仍有重大問題。例如:

類式積分飽和:雖然最終的輸出限制在OUTmin和OUTmax之間。當PTerm不應該增長時,它有可能增長。它不會像積分飽和那樣糟糕,但仍然是不可接受的。

動態更改:在運行時,如果用戶想從P _ On _ M 更改爲P_ ON _E,並在一段時間後返回,那麼P項將不會被初始化,這會導致輸出振盪。

還有更多,但僅僅這些就足以讓人看到真正的問題是什麼。早在我們創建I項的時候,我們已經處理過所有這些問題。我沒有對P項重新執行相同的解決方案,而是選擇了一個更美觀的解決方案。

通過將P項和I項合併到一個名爲“outputSum”的變量中,P _ ON _ M 代碼將受益於已存在的所有上下文修補程序,並且由於代碼中沒有兩個總和,因此不會出現不必要的冗餘。

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double outputSum,lastInput;
double kp,ki,kd;
int SampleTime = 1000; //1 sec
double outMin,outMax;
bool inAuto = false;
  
#define MANUAL 0
#define AUTOMATIC 1
  
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
  
#define P_ON_M 0
#define P_ON_E 1
bool PonE = true;
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
  
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      double dInput = (Input - lastInput);
      outputSum+= (ki * error);
         
      /*Add Proportional on Measurement,if P_ON_M is specified*/
      if(!PonE) outputSum-= kp * dInput;
       
      if(outputSum > outMax) outputSum= outMax;
      
      else if(outputSum < outMin) outputSum= outMin;
       
      /*Add Proportional on Error,if P_ON_E is specified*/
      if(PonE) Output = kp * error;
      else Output = 0;
       
      /*Compute Rest of PID Output*/
      Output += outputSum - kd * dInput;
     
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
  
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
  
void SetTunings(double Kp,double Ki,double Kd,int pOn)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
  
   PonE = pOn == P_ON_E;
  
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
  
   if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
  
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
  
void SetOutputLimits(double Min,double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
  
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
  
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
  
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
       Initialize();
    }
    inAuto = newAuto;
}
  
void Initialize()
{
   lastInput = Input;
    
   outputSum = Output;
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
  
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

現在你可以獲得它。因爲上述功能現在已存在於 V1.2.0版的Arduino PID庫中。

但等等,還有更多:設定點加權。

我沒有將下面的代碼添加到Arduino庫代碼中,但是如果您想滾動自己的代碼,這個特性可能會很有趣。設置點權重的核心是同時擁有PonE和PonM。通過指定0和1之間的比值,可以得到100% PonM、100% PonE(分別)或介於兩者之間的某個比值。如果您的流程不是完全集成的(比如迴流爐),並且希望解釋這一點,那麼這將非常有用。

最終,我決定不在這個時候將它添加到庫中,因爲它最終會成爲另一個需要調整/解釋的參數,而且我不認爲這樣做的好處是值得的。無論如何,如果你想修改代碼,使其具有設定值權重,而不僅僅是純PonM/PonE選擇,下面是代碼:

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double outputSum,lastInput;
double kp,ki,kd;
int SampleTime = 1000; //1 sec
double outMin,outMax;
bool inAuto = false;
  
#define MANUAL 0
#define AUTOMATIC 1
  
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
  
#define P_ON_M 0
#define P_ON_E 1
bool PonE = true,pOnM = false;
double PonEKp,pOnMKp;
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      double dInput = (Input - lastInput);
      outputSum+= (ki * error);
         
      /*Add Proportional on Measurement,if P_ON_M is specified*/
      if(pOnM) outputSum-= pOnMKp * dInput;
       
      if(outputSum > outMax) outputSum= outMax;
      
      else if(outputSum < outMin) outputSum= outMin;
       
      /*Add Proportional on Error,if P_ON_E is specified*/
      if(PonE) Output = PonEKp * error;
      else Output = 0;
       
      /*Compute Rest of PID Output*/
      Output += outputSum - kd * dInput;
     
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
  
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
  
void SetTunings(double Kp,double Ki,double Kd,double pOn)
{
   if (Kp<0 || Ki<0|| Kd<0 || pOn<0 || pOn>1) return;
  
   PonE = pOn>0; //some p on error is desired;
   pOnM = pOn<1; //some p on measurement is desired;
     
   double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
   if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
  
   PonEKp = pOn * kp;
   pOnMKp = (1 - pOn) * kp;
}
  
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
  
void SetOutputLimits(double Min,double Max)
{
   if(Min > Max) return;
   outMin = Min;   outMax = Max;
  
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
  
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
  
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
       Initialize();
    }
    inAuto = newAuto;
}
  
void Initialize()
{
   lastInput = Input;
   outputSum = Output;
   if(outputSum > outMax) outputSum= outMax;
   else if(outputSum < outMin) outputSum= outMin;
}
  
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

沒有將pOn設置爲整數,而是以雙精度輸入,允許使用一個比率(第58行)。除了一些標記(第62行和第63行)外,第77-78行計算了加權Kp項。然後在第37行和第43行,將加權後的PonM和PonE貢獻添加到整個PID輸出中。

歡迎關注:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章