手把手教你看懂並理解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:轉載請註明出處:歐陽天華

相關文章
相關標籤/搜索