改善初學者的PID

一、簡介

結合新的Arduino PID庫的發佈,我決定發佈此係列文章。 最後一個庫雖然可靠,但實際上並沒有任何代碼說明。 這次圍繞該計劃詳細解釋代碼爲何如此。 我希望這對兩類人有用:

  • 直接對Arduino PID庫中發生的事情感興趣的人將獲得詳細說明。

  • 任何編寫自己的PID算法的人都可以看看我的工作方式,並借用他們喜歡的任何東西。

這將是一個艱難的口號,但我認爲我發現了一種不太痛苦的方式來解釋我的代碼。 我將從所謂的“初學者的PID”開始。然後逐步進行改進,直到獲得具有高效,魯棒性的pid算法。

初學者的PID

這是每個人首先學習的PID公式:

在這裏插入圖片描述

這使得幾乎每個人都編寫以下PID控制器:

在這裏插入圖片描述

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
void Compute()
{
   /*How long since we last calculated*/
   unsigned long now = millis();
   double timeChange = (double)(now - lastTime);
  
   /*Compute all the working error variables*/
   double error = Setpoint - Input;
   errSum += (error * timeChange);
   double dErr = (error - lastErr) / timeChange;
  
   /*Compute PID Output*/
   Output = kp * error + ki * errSum + kd * dErr;
  
   /*Remember some variables for next time*/
   lastErr = error;
   lastTime = now;
}
  
void SetTunings(double Kp, double Ki, double Kd)
{
   kp = Kp;
   ki = Ki;
   kd = Kd;
}

Compute()被定期或不定期調用,並且效果很好。不過,本系列不是關於“效果很好”的內容。如果要將該代碼轉換爲與工業PID控制器類似的代碼,則必須解決一些問題:

  1. 採樣時間 - 如果定期對PID算法進行評估,則其效果最佳。如果算法知道此間隔,我們還可以簡化一些內部數學運算。
  2. 微分衝擊 - 這不是最大的問題,但是很容易解決,因此我們將這樣做。
  3. 動態調整參數 - 一種好的PID算法可以在不影響內部工作的情況下更改調整參數。
  4. 緩解積分飽和 - 我們將介紹什麼是緩解積分飽和,並實施具有附帶好處的解決方案。
  5. 開/關(自動/手動)- 在大多數應用中,有時需要關閉PID控制器並手動調節輸出,而不會干擾控制器。
  6. 初始化 - 控制器首次開啓時,我們希望進行”無擾動的傳輸“,也就是說,我們不希望輸出突然變爲某個新值。
  7. 控制方向 - 最後一個不是魯棒性本身名稱的更改,它旨在確保用戶輸入具有正確符號的調優參數。
  8. 新增 測量比例(Proportional on Measurement) - 添加這個特性使得它更加容易控制特定類型的過程。

解決所有問題後,我們將獲得一個可靠的PID算法。我們也會(並非偶然)擁有最新版本的Arduino PID 庫中使用的代碼。因此,無論你在嘗試編寫自己的算法,還是試圖瞭解PID庫正在發生的事情,希望對你有所幫助。讓我們開始吧。

二、採樣時間

問題

初學者的PID被設置爲不定期調用。這導致了兩個問題:

  1. 你無法從PID中獲得一致的表現,因爲它有時會頻繁調用,有時不會。
  2. 你需要做額外的數學運算來計算微分和積分,因爲他們都取決於時間的變化。

解決方案

確保定期調用PID。我決定執行此操作的方法是指定每個週期調用一次計算函數。根據預定的採樣時間,PID計算函數決定是應該計算還是返回。

一旦知道以恆定的間隔對PID進行評估,就可以簡化微分和積分計算。

代碼

在這裏插入圖片描述

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      errSum += error;
      double dErr = (error - lastErr);
 
      /*Compute PID Output*/
      Output = kp * error + ki * errSum + kd * dErr;
 
      /*Remember some variables for next time*/
      lastErr = error;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

現在,在第10和11行,該算法自行決定是否需要計算。 另外,由於我們現在知道採樣之間的時間是相同的,因此我們不需要不斷地乘以時間變化。 我們只需適當地調整Ki和Kd(第31和32行),結果在數學上是等效的,但效率更高。
雖然這樣做有點瑕疵。 如果用戶決定在操作過程中更改採樣時間,則需要重新調整Ki和Kd以反映此新更改。 這就是第39-42行的全部內容。
另請注意,我在第29行將採樣時間轉換爲秒。嚴格來說,這不是必需的,但是這樣允許用戶以1 / sec和s的單位輸入Ki和Kd,而不是1 / mS和mS。

結果

上面的更改爲我們做了三件事:

  1. 不管調用Compute()的頻率如何,PID算法都會以固定的時間間隔進行計算【第11行】
  2. 由於時間相減【第10行】當millis()重新變爲0時將不會有問題。那隻會每55天發生一次,但我們需要防止嗎?
  3. 我們不需要再乘除時間變化。由於他是一個常數,因此我們可以將其從計算代碼中移出【第15、16行】,然後將其於調整常數一起輸入【第31、32行】。雖然從數學意義上講,他們的計算結果相同,但是這樣做省去了每次PID計算中的一次乘法和一次除法運算。

關於中斷的旁註

如果此PID應用在微控制器上,則可以作爲使用中斷的一個很好的依據。SetSampleTime是位置爲中斷頻率,時間到時Compute()將被調用。在這種情況下,則不需要9-12、23、24行。如果你的PID實現計劃這麼做,那就去吧!不過請繼續閱讀本系列的文章。你一定會從後續的修改中受益。

我沒有使用中斷有三點原因:

  1. 就本系列的文章而言,並不是每個人都能夠使用中斷。
  2. 如果你想同時實現多個PID控制器,事情將會變得棘手。
  3. 老實說,這不是我想做的。吉米·羅傑斯(Jimmie Rodgers)在爲我校對該系列作品時提出了建議。我可能決定在PID庫的將來版本中使用中斷。

三、微分衝擊

問題

這次修改將會微調微分項,目的是消除被稱爲“微分衝擊”的現象。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-E8DUBUoP-1585614286962)(/images/DonE.png)]

上圖顯示了這個問題。由於error = SetPoint - Input,對Setpoint的任何更改都會導致error瞬間改變。這種變化的導數是無窮大(實際上,由於dt不爲0,所以它實際上是一個非常大的數字)該數字被帶入PID的方程,這會導致輸出出現不希望的峯值。幸運的是,有一種簡單的方法可以擺脫這種情況。

解決方案

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BGczSKZW-1585614286967)(/images/DonMExplain.png)]

結果表明,除設定值(Setpoint)值更改的情況外,偏差(Error)的導數等於輸入(Input)的負導數。這將是一個完美的解決方法。我們減去(Kd * Input的導數),而不是添加(Kd * Error的導數)。這被稱爲使用”測量導數(Derivative on Measurement)“。

代碼

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hWgcLqGp-1585614286969)(/images/DerivativeKick.png)]

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      errSum += error;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ki * errSum - kd * dInput;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

這裏的修改相當容易。我們將+dError替代爲-dInput。我們現在記錄lastInput而非lastError。

結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-f9ve32X3-1585614286976)(/images/DonM.png)]

這就是哪些修改給我們帶來的。注意輸入值仍然看以來一樣,因此我們得到了相同的性能,但是每次設定值更改時,我們都不會產生巨大的輸出峯值。

這可能重要也可能不重要,這完全取決於你的應用程序對輸出峯值的敏感程度。在我看來,並不需要做太多的工作就可以避免尖峯,爲什麼不這樣做呢?

四、動態調整參數

問題

對任何可靠的PID算法而言,必須具備在系統運行時更改調節參數的能力。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8Qzkubxv-1585614286977)(/images/BadIntegral.png)]

如果你在系統運行時嘗試更改調節參數,初學者PID表現的有點瘋狂。讓我們探究下原因,這是初學者的PID的參數做了上述變化前後的情況。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-S3dPonGy-1585614286979)(/images/BadIntegralCode.png)]

因此我們可以立刻將問題歸咎於積分項。當參數改變時,這是唯一發生劇烈變化的東西。爲什麼會這樣呢?這於初學者對積分的解釋有關:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LMQi42Q2-1585614286981)(/images/BadIntegralEqn1.png)]

在Ki值被更改之前,這種解釋都非常有效。然後突然將新的Ki值乘以全部誤差的累積之和。這並不是我們想要的。我們只想影響事情往好的方向發展。

解決方案

我知道有幾種方法可以解決這個問題。在過去的庫中我使用的是縮放errSum的方法。將Ki值加倍,將errSum減半。這樣可以防止積分項發生突變,並且可以正常工作。這樣做有點笨拙,我又想出了一個更優雅的方法(我不可能是第一個想到這個方法的人,但我的確是一個人想到了。)

這個解決方法需要一點基本的代數知識(或者是微積分?)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-afDGoizF-1585614286983)(/images/GoodIntegralEqn.png)]

我們沒有將Ki置於積分之外,而是將其放入了積分之內。看起來似乎我們什麼都沒做,但我們會發現實際上這有很大的不同。

現在,我們採用誤差並將其乘以當時的Ki。 然後,我們存儲哪些的總和。 當Ki發生變化時,就不會有突變了,因爲可以說所有舊Ki都已經在“銀行”中了。 我們無需額外的數學運算即可平滑傳遞。 它可能會讓我成爲一個極客,但我認爲這很性感。

代碼

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IReo2ynp-1585614286985)(/images/TuringChanges.png)]

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm += (ki * error);
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm - kd * dInput;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

因此我們用複合變量【第4行】替換了errSum變量。它計算了Ki * error的和,而不僅僅是誤差【第15行】。另外,由於Ki位於ITerm中,因此已經從主PID計算中刪除了【第19行】。

結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-onMtPQo5-1585614286987)(/images/GoodIntegral.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VBC5LA15-1585614286990)(/images/GoodIntegralCode.png)]

那麼這是如何解決問題的呢。 在更改ki之前,它會重新調整整個誤差的總和。 使用此代碼,先前的誤差仍然保持不變。

五、積分飽和

問題

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Jtgb13Hq-1585614286992)(/images/Windup.png)]

積分飽和是一個陷阱,他可能比任何其他的內容對初學者有更多的要求。當PID認爲自己可以做一些自己做不到的事情時就會發生這種情況。例如,Arduino上的PWM輸出接受0-255之間的值。默認情況下,PID不知道這一點。如果它認爲300-400-500可以使用,它將嘗試使用那些期望得到所需的值。由於實際上該值固定在255,因此它將繼續嘗試越來越大的數值而沒有任何作用。

問題以怪異的滯後形式顯現出來。上方我們可以看到輸出超出外部極限。當設定值減小後,輸出值必須先減小然後才能降到255線以下。

解決方案—步驟1(積分限幅)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HI0Pp2gq-1585614287008)(/images/No-Windup.png)]

這有幾種方法可以緩解積分飽和,但是我選擇的方法如下:告訴PID的輸出極限是什麼。下面的代碼中可以看到有一個SetOuputLimits()函數。一旦達到了任一限制,PID便停止求和(積分)。他知道無事可做。由於輸出不會飽和,因此當設定值下降到可以執行某項操作的範圍時,我們會立刻得到響應。

解決方案—步驟2(輸出限幅)

不過請注意,在上圖中,儘管我們消除了積分飽和滯後,但並沒有完全解決。 pid認爲發送的內容與發送的內容之間仍然存在差異。爲什麼?比例項和(在較小程度上)微分項。
即使已對積分項進行了安全鉗位,P和D仍會添加上他們兩的部分,使其結果高於輸出限制。我認爲這是不可接受的。如果用戶調用了一個名爲“ SetOutputLimits”的函數,則他們必須假設這意味着“輸出將保持在這些值之內。”因此,對於第2步,我們做出一個有效的假設。除了限制積分項,我們還限制輸出值,使其保持在我們期望的位置。
(注意:您可能會問爲什麼我們需要同時鉗制兩個。如果我們無論如何都要進行輸出,爲什麼還要分別鉗制積分呢?如果我們所做的只是鉗制輸出,那麼積分術語將追溯到越來越多。儘管在升壓過程中輸出看起來不錯,但在降壓過程中我們會看到明顯的滯後。)

代碼

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-n6k5y1QN-1585614287013)(/images/ResetWindup1.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ndrJFWAm-1585614287018)(/images/ResetWindup2.png)]

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
void Compute()
{
   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 PID Output*/
      Output = kp * error + 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)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
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;
}

一個新的允許用戶可以指定輸出限制的函數被添加【52-63行】。這些限制用來鉗制積分項(I-Term)和輸出(Output)。

結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-B7lYf0YE-1585614287034)(/images/No-Winup-Clamped.png)]

正如我們看到的,積分飽和被消除了。另外,輸出保持在我們想要的位置。這意味着沒有必要對輸出進行額外的鉗制。如果你想輸出的變化範圍是23到167,你可以將其設置爲輸出限制。

六、開/關PID控制

問題

擁有PID控制器就好了,有時候您根本不在乎它在說什麼。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-v47XrtIc-1585614287040)(/images/BadForcedOutput.png)]

假設您要在程序中的某個時刻將輸出強制爲某個值(例如0),當然可以在調用例程中執行此操作:

void loop()
{
Compute();
Output=0;
}

這樣無論PID的怎麼說,都可以覆蓋他的值。但是,這在實踐中是一個可怕的想法。PID會變得非常疑惑:“我一直在改變輸出值,什麼也沒發生,出了什麼事情?!讓我改變一個更大的輸出值。”結果,當你停止覆寫輸出並切換回PID時,輸出值可能會立刻發生巨大的改變。

解決方案

解決此問題的方法是有一種方法可以打開和關閉PID。 這些狀態的常用術語是“手動”(我將手動調節值)和“自動”(PID將自動調節輸出)。 讓我們看看如何通過代碼完成此操作:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oYjVJWRG-1585614287043)(/images/onoff1.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rMjyOaP5-1585614287048)(/images/onoff2.png)]

/*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
 
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 PID Output*/
      Output = kp * error + 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)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
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)
{
  inAuto = (Mode == AUTOMATIC);
}

一個相當簡單的解決方案。 如果您未處於自動模式,請立即退出計算函數,而無需調整輸出或任何內部變量。

結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MTZCbVmf-1585614287053)(/images/BetterForcedOutput.png)]

的確,您可以通過不從調用例程中調用Compute來達到類似的效果,但是這種解決方案可以保留PID的工作原理,這正是我們所需要的。 通過將內容保持在內部,我們可以跟蹤所處的模式,更重要的是,它使我們知道何時更改模式。 這就引出了下一個問題……

七、初始化

問題

在上一節中,我們實現了打開和關閉PID的功能。 我們關閉了它,但是現在讓我們看一下重新打開它會發生什麼:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VxvW8BxU-1585614287056)(/images/NoInitialization.png)]

呀!PID跳回到它發送的最後一個輸出值,然後從那裏開始調整。 這會導致我們不願遇到的輸入顛簸。

解決方案

這個很容易修復,因爲我們知道什麼時候(從手動到自動),所以我們只需要初始化一些東西就可以實現平穩的轉換。這意味着對存儲的兩個工作變量(ITerm和lastInput)進行處理,以防止輸出跳躍。

代碼

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kyM03TSM-1585614287060)(/images/Initialization1.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4cvWd74L-1585614287062)(/images/Initialization2.png)]

/*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
 
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 PID Output*/
      Output = kp * error + 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)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
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;
   ITerm = Output;
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}

我們修改了SetMode(…)以檢測從手動到自動的過渡,並添加了初始化功能。它設置ITerm=Output來處理積分項,設置lastInput=Input來防止導數尖峯。比例項不依賴於過去的任何信息,因此不需要任何初始化。

結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GUrzOhjc-1585614287065)(/images/Initialization.png)]

從上圖中我們可以看到,正確的初始化會導致從手動到自動的無障礙轉換:這正是我們所追求的。

更新:爲什麼ITerm = 0?

最近,我收到了很多問題,問爲什麼初始化時不設置ITerm = 0。 作爲回答,我請您考慮以下情形:pid是手動的,並且用戶已將輸出設置爲50。一段時間後,該過程穩定爲輸入75.2。 用戶設定設定值75.2並打開pid。 應該怎麼辦?
我認爲切換到自動後,輸出值應保持在50。由於P和D項將爲零,因此發生這種情況的唯一方法是將ITerm初始化爲Output的值。
如果您需要將輸出初始化爲零,則無需更改上面的代碼。 只需在調用例程中設置Output = 0,然後再將PID從“手動”轉換爲“自動”即可。

八、方向

問題

PID將連接的過程分爲兩類:正作用和反作用。 到目前爲止,我展示的所有示例都是直接方式。 即,輸出的增加導致輸入的增加。 對於反向作用過程,則相反。 例如,在冰箱中,冷卻的增加導致溫度下降。 爲了使初學者PID可以逆向工作,kp,ki和kd的符號都必須爲負。
這本身不是問題,但是用戶必須選擇正確的符號,並確保所有參數都具有相同的符號。

解決方案

爲了簡化這個過程,我要求kp,ki和kd均> = 0。 如果用戶連接到反向過程,則他們使用SetControllerDirection函數單獨指定該過程。 這樣可以確保所有參數都具有相同的符號,並希望使事情更直觀。

代碼

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8qcB0GQa-1585614287069)(/images/direction1.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nx1KldzS-1585614287072)(/images/direction2.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zGx3KKzb-1585614287095)(/images/direction3.png)]

/*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;
 
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 PID Output*/
      Output = kp * error + 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)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
  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;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

PID完成

到此爲止。 我們已經將“初學者的PID”變成了我目前所知道的最強大的控制器。 對於那些正在尋找PID庫詳細說明的讀者,希望您能從中得到幫助。 對於那些編寫自己的PID的人,希望您能夠收集一些想法,從而節省一些時間。
最後兩個注意事項:

  1. 如果本系列中的某些內容看起來不對,請通知我。 我可能錯過了一些東西,或者可能只是需要在我的解釋中更清楚一些。 無論哪種方式,我都想知道。
  2. 這只是基本的PID。 爲了簡化起見,我故意遺漏了許多其他問題。 讓我煩惱的是:使用速度而不是位置來進行前饋,重置領帶,整數數學,不同的pid形式。 如果有興趣讓我探索這些主題,請告訴我。

九、測量比例—代碼

在上一篇文章中,我將所有時間都花在解釋“比例測量”的好處上。 在這篇文章中,我將解釋代碼。 人們似乎很欣賞我上次解釋事情的逐步方式,所以我將在這裏做。 下面的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;
}

:37,38,41,51,55,108爲改動行。

隨着輸入的變化,“測量比例”會提供越來越大的阻礙,但是如果沒有參考系,我們的性能會有些不穩定。 如果第一次打開控制器時PID輸入爲10000,我們真的要開始以Kp * 10000進行抵抗嗎? 否。我們希望將初始輸入用作參考點(第108行),隨着輸入從此處更改(第38行),增加或減少阻力。
我們需要做的另一件事是允許用戶選擇是否要按比例進行誤差或測量。 在上一篇文章之後,PonE似乎毫無用處,但重要的是要記住,對於許多循環來說,它運作良好。 因此,我們需要讓用戶選擇他們想要的模式(第51和55行),然後在計算中採取相應的行動(第37和38行)。

### 第二遍修改—動態調整

儘管上面的代碼確實可以工作,但是它有一個我們之前已經看到的問題。 在運行時更改調整參數時,會出現不希望出現的現象

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0uZf17OZ-1585614287101)(/images/PonM-blip.png)]

爲什麼會這樣呢?

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ntsgKugy-1585614287104)(/images/PonM-blip-math-1.png)]

我們上次看到此結果的原因是,積分是通過新的Ki重新縮放的。 這次是(Input – initInput)由Kp重新縮放。 我選擇的解決方案類似於我對Ki所做的解決方案:我沒有將Input – initInput視爲一個整體單元乘以當前Kp的方法,而是將其分解爲各個步驟,然後分別乘以Kp:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yrycACL1-1585614287108)(/images/PonM-expansion.png)]

/*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 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;
}

:20,39-43,114爲改動行

現在,我們保留一個有效的總和PTerm,而不是將Input-initInput的全部乘以Kp。 在每一步中,我們僅將當前輸入更改乘以Kp並從PTerm中減去(第41行。)在這裏,我們可以看到更改的影響:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uy7AHm4d-1585614287113)(/images/PonM-no-blip.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ff32IjLz-1585614287117)(/images/PonM-no-blip-math.png)]

由於舊的Kps位於“銀行”中,因此調整參數的變化只會影響我們向前邁進

最後一遍–求和問題

關於上述代碼的錯誤,我不會詳細介紹(流行趨勢等)。很好,但是仍然存在重大問題。例如:

  1. 結束時間:雖然最終輸出限制在outMin和outMax之間,但PTerm可能會在不希望的時候增加。它不會像完整的纏繞那樣糟糕,但是仍然不能被接受
  2. 即時更改:如果用戶在運行時從P_ON_M更改爲P_ON_E,則一段時間後返回,則不會對PTerm進行屬性初始化,這會導致輸出顛簸

還有更多,但僅這些就足以瞭解真正的問題是什麼。在創建ITerm之前,我們已經處理了所有這些問題。我決定不爲PTerm重新執行相同的解決方案,而是決定使用一種更美觀的解決方案。
通過將PTerm和ITerm合併到一個稱爲“ outputSum”的變量中,P_ON_M代碼將從已存在的所有ITerm修復中受益,並且由於代碼中沒有兩個和,因此沒有不必要的冗餘。

/*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;
}

:4,20,32,36,42,43,46,114爲改動行.

那裏有。 以上功能是Arduino PID v1.2.0中提供的功能。

但是,等等,還有更多:設定權重。

我沒有在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;
}

:19,20,37,43,58,60,62,63,77,78爲改動行.

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


**注**:19,20,37,43,58,60,62,63,77,78爲改動行.

現在,pOn不再是將pOn設置爲整數,而是以允許允許比率的雙精度形式出現(第58行)。除了一些標誌(第62和63行)以外,還在第77-78行計算了加權Kp項。 然後,在第37和43行上,將加權的PonM和PonE貢獻添加到總體PID輸出中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章