手把手教你看懂並理解Arduino PID控制庫——引子

介紹

本文主要依託於Brett Beauregard大神針對Arduino平臺撰寫的PID控制庫Arduino PID Library及其對應的幫助博客Improving the Beginner’s PID。在沒有Brett Beauregard幫助以前,也嘗試過按照PID控制基本理論寫過PID控制程序,併成功應用於工業設備中,但從未深刻考慮過將其寫成適合工業控制的通用庫。根據Brett Beauregard的理念,此PID庫主要想爲如下兩類人服務:git

  1. 想要從事Arduino PID控制的同志,提供一個快速入門的方法
  2. 已經擁有本身的PID控制算法,想要從中獲取到一些新點子的同志。

本文在上述基礎上,主要有如下幾方面工做:算法

  1. 對Brett Beauregard的PID控制庫代碼進行必要的說明
  2. 對其博客教程核心思想進行必要的說明
  3. 對其依託PID控制庫改進的autoPID控制庫進行必要的說明。

背景

接觸過PID控制的工程師應當都會對下面的公式印象深入:app

上述公式的具體說明就不加以說明了,請各位參考維基百科的PID controller。大部分同志可能會寫出以下代碼(或者相似),包括我本身less

/*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控制量計算的任什麼時候候被調用,在這樣的代碼支持下,PID控制能夠工做得很好。可是,若是是一個性能較強的工業控制器,還須要考慮一下幾個問題:ide

  1. 採樣時間——改變採樣時間會帶來怎樣的後果
  2. 微分項的影響——忽然改變設定值或者微分時間,如何避免衝擊
  3. PID參數改變——PID控制參數的忽然改變,如何避免突變
  4. 積分參數——忽然改變I參數,如何便面衝擊
  5. 開關——在控制過程當中,PID調節開關忽然的開啓及關閉
  6. 初始化——PID運行一段時候後關閉,通過一段時間再次開啓,如何避免突變
  7. 調節的方向——這個不是大問題,僅僅是爲了保證系統超預計的方向運行

若是上述幾個問題沒有太多的理解,不要緊,先看一下PID庫中代碼是如何寫的(若是僅想看上述7個問題的解決方案請跳過下一章節)。函數

代碼註釋

頭文件oop

#ifndef PID_v1_h
#define PID_v1_h
#define LIBRARY_VERSION	1.1.1

class PID
{
public:

	//Constants used in some of the functions below
	// 這裏定義的兩個變量分別指代兩種工做模式:AUTOMATIC 對應 PID控制開啓; MANUAL 對應PID控制關閉
	#define AUTOMATIC	1
	#define MANUAL	0
	// 這裏定義兩個變量分別指代控制量與被控量方向:DIRECT 對應二者同向; REVERSE 對應二者反向
	// 其中同向指: 若是控制量增大,那麼被控量也會增大;反之亦然。
	// 其中反向指: 若是控制量增大,那麼被控量缺減少;反之亦然。
	#define DIRECT  0
	#define REVERSE  1

	//commonly used functions **************************************************************************
	//構造函數
	PID(double*, double*, double*,        // * constructor.  links the PID to the Input, Output, and 
		double, double, double, int);     //   Setpoint.  Initial tuning parameters are also set here

	// 設置自動模式仍是手動模式,二者區別目前還未清楚
	void SetMode(int Mode);               // * sets PID to either Manual (0) or Auto (non-0)

	// 計算PID, 在每一個計算週期都應當調用 ,計算頻率和是否計算能夠在setMode和SetSampleTime中指定
	bool Compute();                       // * performs the PID calculation.  it should be
										  //   called every time loop() cycles. ON/OFF and
										  //   calculation frequency can be set using SetMode
										  //   SetSampleTime respectively

	//指定輸出的範圍,其中0-255,表示可限制的輸出範圍
	void SetOutputLimits(double, double); //clamps the output to a specific range. 0-255 by default, but
										  //it's likely the user will want to change this depending on
										  //the application



	//available but not commonly used functions ********************************************************
	// 設定P、I、D參數,能夠在運行的時間週期內,指定運行須要的參數
	void SetTunings(double, double,       // * While most users will set the tunings once in the 
		double);         	  //   constructor, this function gives the user the option
							  //   of changing tunings during runtime for Adaptive control

	// 設定控制器的方向,限制輸出的正反向,僅須要在開始的時候設置一次
	void SetControllerDirection(int);	  // * Sets the Direction, or "Action" of the controller. DIRECT
										  //   means the output will increase when error is positive. REVERSE
										  //   means the opposite.  it's very unlikely that this will be needed
										  //   once it is set in the constructor.

	// 採樣週期,以毫秒做爲設置單位,默認爲10
	void SetSampleTime(int);              // * sets the frequency, in Milliseconds, with which 
										  //   the PID calculation is performed.  default is 100



	 //Display functions ****************************************************************
	// 獲取PID運行參數
	double GetKp();						  // These functions query the pid for interal values.
	double GetKi();						  //  they were created mainly for the pid front-end,
	double GetKd();						  // where it's important to know what is actually 
	// 獲取運行模式
	int GetMode();						  //  inside the PID.
	//獲取PID 方向
	int GetDirection();					  //

private:
	// 此函數初始化,還不知什麼用,須要參考CPP
	void Initialize();

	double dispKp;				// * we'll hold on to the tuning parameters in user-entered 
	double dispKi;				//   format for display purposes
	double dispKd;				//

	double kp;                  // * (P)roportional Tuning Parameter
	double ki;                  // * (I)ntegral Tuning Parameter
	double kd;                  // * (D)erivative Tuning Parameter

	int controllerDirection;

	// 其中包含了INput、 OUTput以及setPoint
	double *myInput;              // * Pointers to the Input, Output, and Setpoint variables
	double *myOutput;             //   This creates a hard link between the variables and the 
	double *mySetpoint;           //   PID, freeing the user from having to constantly tell us
								  //   what these values are.  with pointers we'll just know.
	// 此3個參數須要參考CPP才知道		  
	unsigned long lastTime;
	double ITerm, lastInput;

	unsigned long SampleTime;
	double outMin, outMax;
	// 是否自動參數的標誌
	bool inAuto;
};
#endif

源文件性能

/**********************************************************************************************
 * Arduino PID Library - Version 1.1.1
 * by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com
 * This Library is licensed under a GPLv3 License
 **********************************************************************************************/
#include "PID_v1.h"

/*Constructor (...)*********************************************************
 *    The parameters specified here are those for for which we can't set up 
 *    reliable defaults, so we need to have the user set them.
 ***************************************************************************/
PID::PID(double* Input, double* Output, double* Setpoint,
        double Kp, double Ki, double Kd, int ControllerDirection)
{
	// 賦值控制量、被控量及設定值初始地址,注意這裏是地址
    myOutput = Output;
    myInput = Input;
    mySetpoint = Setpoint;
	// 初始化auto模式爲false
	inAuto = false;
	
	// 默認控制量限制在0到255,此函數能夠根據實際系統須要修改控制量輸出限制範圍
	PID::SetOutputLimits(0, 255);				//default output limit corresponds to 
												//the arduino pwm limits
	// 默認採樣週期爲100ms,一樣能夠根據需求修改
    SampleTime = 100;							//default Controller Sample Time is 0.1 seconds

	// 設置輸出的方向
    PID::SetControllerDirection(ControllerDirection);
	// 設置PID 控制參數
    PID::SetTunings(Kp, Ki, Kd);

	// 用於存儲PID構造時,對應的系統運行時間
	// millis()做用是獲取當前系統運行時間(單位ms),此函數針對arduino;移植到別的系統,能夠其餘相似做用函數替代
	// 這裏減去SampleTime是爲了保證在構造後能力立刻進行PID控制,而不須要等待到下一個SampleTime週期
    lastTime = millis()-SampleTime;				
}
 
 
/* Compute() **********************************************************************
 *     This, as they say, is where the magic happens.  this function should be called
 *   every time "void loop()" executes.  the function will decide for itself whether a new
 *   pid Output needs to be computed.  returns true when the output is computed,
 *   false when nothing has been done.
 *   此函數用於PID控制量計算,函數能夠頻繁的在進程中被調用。
 **********************************************************************************/ 
bool PID::Compute()
{
	// 若是沒有開啓PID返回 計算失敗,退出;控制量不變,仍爲上一次控制量
   if(!inAuto) return false;
   // 獲取當前系統運行時間並求出相對上一次計算時間間隔
   unsigned long now = millis();
   unsigned long timeChange = (now - lastTime);
   // 若是時間間隔大於或者等於採樣時間,那麼則計算,不然不知足採樣條件,計算失敗,退出;
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
	   // 保存當前被控量,若是是一個實時控制系統,此時被控量可能與構造時的被控量不一致
	  double input = *myInput;
	  // 求出設定值與當前被控量之間的誤差
      double error = *mySetpoint - input;
	  // 計算積分項 此處積分項和標準PID控制方程略微有差距
      ITerm+= (ki * error);
	  // 若是 積分項超過最大限制,那麼設置積分項爲最大限制;一樣,最小限制也作一樣處理
	  // 此處爲什麼這麼作一句兩句說不清楚,主要是爲了PID 控制量長時間超限後,忽然下降設定值,可以讓系統立刻反應而不會產生一個時間滯後。
      if(ITerm > outMax) ITerm= outMax;
      else if(ITerm < outMin) ITerm= outMin;

	  // 求出兩個被控量之間誤差,也就是在計算週期(這裏不用採用週期是由於計算週期可能會超過採樣週期)被控量的變化。
	  // 其實就是微分項的 因子,可是看起來和標準表達式也不同啊!!!
	  // 。。。。一兩句也說不清楚,總的來講是爲了防止控制量和被控量突變
      double dInput = (input - lastInput);
 
      /*Compute PID Output*/
	  // PID 調節算式,這就不須要說明了
      double output = kp * error + ITerm- kd * dInput;

      // 這裏作限制和ITerm作限制的做用是同樣的。。
	  if(output > outMax) output = outMax;
      else if(output < outMin) output = outMin;
	  *myOutput = output;
	  
      /*Remember some variables for next time*/
      lastInput = input;
      lastTime = now;
	  return true;
   }
   else return false;
}


/* SetTunings(...)*************************************************************
 * This function allows the controller's dynamic performance to be adjusted. 
 * it's called automatically from the constructor, but tunings can also
 * be adjusted on the fly during normal operation
 * 此函數用於設定PID調節參數
 ******************************************************************************/ 
void PID::SetTunings(double Kp, double Ki, double Kd)
{
	// 若是PID參數中有小於0的參數,那麼設定失敗,直接退出,仍然沿用原來的參數
   if (Kp<0 || Ki<0 || Kd<0) return;
	// 僅作顯示用。
   dispKp = Kp; dispKi = Ki; dispKd = Kd;
   
   // 獲取採樣時間,由ms轉爲s
   double SampleTimeInSec = ((double)SampleTime)/1000;  
   // 調整PID參數, I 和 D 參數的調節主要是爲了知足採樣週期改變帶致使的影響,
   // 主要是 積分項和 微分項是和時間有關的參數,因此採樣週期改變會致使這兩項須要從新計算,這裏爲了減小這些工做,將採樣週期變換轉換我I D參數變化
   // 至於爲何能夠這麼作,是由於前面作了特殊處理,修改了PID標準表達式,使每一次計算對歷史依賴較小
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
	//  設定PID調節方向
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
  
/* SetSampleTime(...) *********************************************************
 * sets the period, in Milliseconds, at which the calculation is performed	
 ******************************************************************************/
//更新新的採樣時間,同時按照比例更新ID參數
void PID::SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
/* SetOutputLimits(...)****************************************************
 *     This function will be used far more often than SetInputLimits.  while
 *  the input to the controller will generally be in the 0-1023 range (which is
 *  the default already,)  the output will be a little different.  maybe they'll
 *  be doing a time window and will need 0-8000 or something.  or maybe they'll
 *  want to clamp it from 0-125.  who knows.  at any rate, that can all be done
 *  here.
 * 此函數容易產生控制量的突變,在運行過程當中,儘可能不要縮小範圍
 **************************************************************************/
void PID::SetOutputLimits(double Min, double Max)
{
	// 賦值限制
   if(Min >= Max) return;
   outMin = Min;
   outMax = Max;
 
   if(inAuto)
   {
	   if(*myOutput > outMax) *myOutput = outMax;
	   else if(*myOutput < outMin) *myOutput = outMin;
	 
	   if(ITerm > outMax) ITerm= outMax;
	   else if(ITerm < outMin) ITerm= outMin;
   }
}

/* SetMode(...)****************************************************************
 * Allows the controller Mode to be set to manual (0) or Automatic (non-zero)
 * when the transition from manual to auto occurs, the controller is
 * automatically initialized
 ******************************************************************************/ 
void PID::SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
	// 若是模式不同,那麼則從新初始化
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        PID::Initialize();
    }
    inAuto = newAuto;
}
 
/* Initialize()****************************************************************
 *	does all the things that need to happen to ensure a bumpless transfer
 *  from manual to automatic mode.
 ******************************************************************************/ 
void PID::Initialize()
{
   ITerm = *myOutput;
   lastInput = *myInput;
   if(ITerm > outMax) ITerm = outMax;
   else if(ITerm < outMin) ITerm = outMin;
}

/* SetControllerDirection(...)*************************************************
 * The PID will either be connected to a DIRECT acting process (+Output leads 
 * to +Input) or a REVERSE acting process(+Output leads to -Input.)  we need to
 * know which one, because otherwise we may increase the output when we should
 * be decreasing.  This is called from the constructor.
 ******************************************************************************/
void PID::SetControllerDirection(int Direction)
{
   if(inAuto && Direction !=controllerDirection)
   {
	  kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }   
   controllerDirection = Direction;
}

/* Status Funcions*************************************************************
 * Just because you set the Kp=-1 doesn't mean it actually happened.  these
 * functions query the internal state of the PID.  they're here for display 
 * purposes.  this are the functions the PID Front-end uses for example
 ******************************************************************************/
double PID::GetKp(){ return  dispKp; }
double PID::GetKi(){ return  dispKi;}
double PID::GetKd(){ return  dispKd;}
int PID::GetMode(){ return  inAuto ? AUTOMATIC : MANUAL;}
int PID::GetDirection(){ return controllerDirection;}

(這裏代碼過長,提供下載地址)。ui

上述代碼提供對PID庫的必要註釋,其中有些註釋沒法一兩句話就能說清,特別是針對上述7個問題的解決方案,具體的代碼分析,請參考下一章節。this

若有不足之處請告知,^.^

下一章節將分析採樣時間變化對PID控制的影響

NEXT

PS:轉載請註明出處:歐陽天華

相關文章
相關標籤/搜索