閑話:
作為一個控制專業的學生,說起PID,真是讓我又愛又恨。甚至有時候會覺得我可能這輩子都學不會pid了,但是經過一段時間的反復琢磨,pid也不是很復雜。所以在看懂pid的基礎上,寫下這篇文章,方便學習和交流。
=======================================================
PID控制器是工業過程控制中廣泛采用的一種控制器,其中,P、I、D分別為比例(Proportion)、積分(Integral)、微分(Differential)的簡寫;將偏差的比例、積分和微分通過線性組合構成控制量,用該控制量對受控對象進行控制,稱為PID算法 。
其中KP、KI、KD分別為比例系數、積分系數、微分系數。
比例系數KP :反應系統當前最基本的誤差,系數大,可以加快調節,減小誤差,但是過大的比例使系統穩定性下降,甚至造成系統的不穩定。
積分系數KI :反應系統的累計誤差,使系統消除穩態誤差,提高無差度,只要有誤差,積分調節就會起作用。
微分系數KD :反應系統誤差的變化率,具有預見性,們可以預見偏差的變化趨勢,產生超前的控制效果。因此可以改善系統的動態性能。但是微分對噪聲有放大作用,會減弱系統的抗干擾性。
轉化為數學語言就是:
由上面的方框圖,可以知道其傳遞函數(拉式域表達式)為:
控制理論和數學分析中,我們一般借助拉普拉斯變化和傅里葉變換來幫助我們分析系統的某些特性,比如暫態響應、穩態響應等。但是在程序中,我們只能根據時域中的離散化的表達式來編寫算法。
所以將上式轉化為時域表達式為:
我們需要在計算機中通過編程實現這個式子,而上式又是連續時間的表達式,所以只能先將其離散化,推導出他的差分方程如下:
......................................(1)
這就是增量式pid算法的差分方程。
其中 T 為PID的計算周期,就是 dt 。其值由微控制器的定時器決定,自己設置pid的運算周期。
e(k) 為本次計算時,傳感器反饋的值和設定值之間的差。
e(k-1) 為上次計算時,傳感器反饋的值和設定值之間的差。
e(k-2) 為上上次計算時,傳感器反饋的值和設定值之間的差。
有了pid算法的差分方程,就可以通過編程來實現這個算法。之所以得通過差分方程來實現,是因為差分方程的一般解法是迭代法,而迭代法只需要知道初值和通項公式就能計算出所有時刻的的值。這是計算機最擅長的事情了。
這個式子就是最本質的離散化PID表達式。后面我將基於他推導出其他幾個常見的表達式,但是實質都是這個式子。
在推導其他的表達式之前,請注意這幾個變量的 關系:
比例系數:KP
積分系數:KI = KP*T/Ti 其中Ti 為積分時間
微分系數:KD = KP*Td/T 其中Td為微分時間
之所以叫時間,是因為考慮了系統的量綱。
因為pid輸出的控制信號應該和被控量或參考信號有同樣的物理單位。因為在物理系統中他們是描述的同一種物理量。假設被控量是位移,單位是m, 時間單位是s,那么KI KD的單位應該為單位1,而根據上面的式子,有一個運算周期T,所以Ti Td 相應的應該也有與之倒數的時間單位,來抵消T 的單位。所以稱之為時間。
因為KP 和 T 都是常數,所以積分系數和積分時間、微分系數和微分時間都是成嚴格的比例關系的。他們的區別就是數值不一樣,實質都是積分項和微分項的系數。如果調節的時候,看到別人說增大積分時間,而你的程序中只有積分系數。那么這時,就應該減小積分系數。其他類推。
對於上面的(1)式,因為 KD/T 是一個常數,KI * T也是常數,所以就有些人由於默認pid運算周期,就把 KD / T直接寫成 KD,KI * T直接寫成 KI。
其實嚴格來說,這樣寫是不嚴謹的。因為調節過程中,運算周期 T 也是一個很重要的參數。也需要對他進行反復的調節。
在調節完畢之后,我們可以根據這幾個常數自己計算他們的乘積,直接寫在代碼中,以減少以后處理器的運算量。
所以能推導出這么幾個式子:
但是不管怎么變換,他的本質都是那個原式,在他的基礎上把常量統一了一下,或者做了一下變量代換。增量式pid的式子就一個,同樣的迭代式,同樣的項,不同的人在不同的程序中取不同的名字,而初學者又容易吧相同名字的變量當成一個東西,以為所有編寫代碼的人都像商量好一樣遵守同樣的原則,但是現實中你看到的代碼往往事與願違。不規范的程序到處都是。容易讓人產生誤解。
==============================================================================
現在根據最初的原式來進行pid算法的實現。
首先做一下變量代換,方便后面進行調試:
所以差分方程就變成了
KP不能為0,沒有ID或者單個I 、 D的控制器
當KD為0的時候,系統為PI控制器。
當KI為0的時候,系統為PD控制器。
當KD KI 都不為0的時候,系統為PID控制器。
可以根據系統的情況選擇使用不同的控制器。我使用的是PID控制器。
首先就是PID中需要用到的參數的聲明,建議最好把需要用到的參數全部都聲明一下,就算有的變量顯得有些多余,這樣可以幫助自己理解。減少BUG
聲明pid需要參數的結構體:
1 //================== 2 //PID.H 3 //================== 4 5 6 #ifndef __PID_H 7 #define __PID_H 8 9 10 //PID計算需要的參數 11 typedef struct pid 12 { 13 float ref; //系統待調節量的設定值 14 float fdb; //系統待調節量的反饋值,就是傳感器實際測量的值 15 16 17 float KP; //比例系數 18 float KI; //積分系數 19 float KD; //微分系數 20 21 float T; //離散化系統的采樣周期 22 23 float a0; //變量代替三項的運算結果 24 float a1; 25 float a2; 26 27 float error; //當前偏差e(k) 28 float error_1; //前一步的偏差 29 float error_2; //前前一步的偏差 30 31 float output; //pid控制器的輸出 32 float output_1; //pid的前一步輸出 33 float out_max; //輸出上限 34 float out_min; //輸出下限 35 36 37 }PID_value; //定義一個PID_value類型, 此時PID_value為一個數據類型標識符,數據類型為結構體 38 49 50 //條件編譯的判別條件,用於調試 51 #define PID_DEBUG 1 52 53 //pid函數聲明 54 void PID_operation(PID_value *p); 55 void PID_out(void); 56 57 float constrain_float(float amt, float low, float high); //浮點數限幅 58 //int constrain_int16(int amt, int low, int high); //整型數限幅 59 60 #endif 61 62 //======================== 63 //END OF FILE 64 //========================
接下來就是按照上面推導的公式進行編程實現:
1 //============= 2 //PID.C 3 //============= 4 5 #include "main.h" 6 7 8 #define set_distance 10.00 //設定距離 9 #define allow_error 1.0 //死區,允許誤差帶,0.5太小,系統不穩定,一直在調節,2就太大, 10 11 extern float real_distance ; //實際距離 12 extern PID_value xdata ASR ; 13 14 15 /* ******************************************************** 16 ** 作者 :Andrew 17 ** 日期 :2018.3.8 18 ** 說明: 19 1、PID默認為PI調節器 20 2、使用了條件編譯進行功能切換,節省計算時間 21 在校正PID參數的時候,將宏定義 PID_DEBUG 設為1; 22 校正完畢后,置0; 23 3、同時在初始化的時候直接為a0,a1,a2賦值 24 ******************************************************** */ 25 void PID_operation(PID_value *p) 26 { 27 28 //使用條件編譯進行功能切換 29 #if (PID_DEBUG) 30 31 float a0,a1,a2; 32 33 //計算中間變量a0,a1,a2; 34 a0 = p->KP + p->KI*p->T + p->KD/p->T ; 35 a1 = p->KP + 2*p->KD/p->T ; 36 a2 = p->KD/p->T ; 37 //計算輸出 38 p->output = p->output_1 + a0*p->error - a1*p->error_1 + a2*p->error_2 ; 39 40 #else 41 //非調試狀態下,直接給a賦值計算輸出,減小計算量,因為一旦三個系數確定,T已知,即可自己計算出對應的a 42 p->output = p->output_1 + p->a0*p->error - p->a1*p->error_1 + p->a2*p->error_2 ; 43 44 #endif 45 46 //輸出限幅 47 p->output = constrain_float(p->output,p->out_min,p->out_max); 48 49 //為下次計算迭代 50 //這里順序千萬不要搞錯,不然輸出占空比是錯誤的。 51 p->output_1 = p->output; 52 p->error_2 = p->error_1; 53 p->error_1 = p->error; 54 55 } 56 57 /* ******************************************************** 58 ** 作者 :Andrew 59 ** 日期 :2018.3.8 60 ** 說明: 61 1、首先根據設定與實際的距離差,判斷需要前進還是后退 62 2、在誤差為 allow_error 之內,停車,防止小車一直在抖,畢竟超聲波有誤差。 63 ******************************************************** */ 64 void PID_out(void) 65 { 66 float xdata duty; 67 68 ASR.ref = set_distance; //距離給定 69 ASR.fdb = real_distance; //獲取實際距離反饋 70 71 ASR.error = ASR.ref - ASR.fdb; //偏差 72 73 PID_operation(&ASR); 74 75 duty = ASR.output; 76 77 if(ASR.error > allow_error) //設定值大於實際值,小車需要后退 78 { 79 left_go_back; 80 right_go_back; 81 Left_Forward_Duty = (int)Low_Speed + (int)duty; //帶符號數和無符號數運算會轉換為無符號數 82 Right_Forward_Duty = Left_Forward_Duty; //速度一樣 83 } 84 else if((-ASR.error) > allow_error) //設定值小於實際值,小車前進 85 { 86 left_go_ahead; 87 right_go_ahead; 88 Left_Forward_Duty = (int)Low_Speed + (int)duty; 89 Right_Forward_Duty = Left_Forward_Duty ; //速度一樣 90 } 91 else //在誤差范圍內,停車 92 car_stop(); 93 } 94 95 96 //浮點數限幅,constrain ->約束,限制 97 //如果輸入不是數字,則返回極端值的平均值 98 //isnan函數檢測輸入是否是數字,is not a number 99 float constrain_float(float amt, float low, float high) 100 { 101 // if (isnan(amt)) //51里面沒有這個庫函數,需要自己實現 102 // { 103 // return (low+high)*0.5f; 104 // } 105 return ((amt)<(low)?(low):((amt)>(high)?(high):(amt))); 106 } 107 108 /* 109 //16位整型數限幅 110 int constrain_int16(int amt, int low, int high) 111 { 112 return ((amt)<(low)?(low):((amt)>(high)?(high):(amt))); 113 } 114 */ 115 116 //======================== 117 //END OF FILE 118 //========================
注意在增量式pid算法中需要對輸出結果做限幅處理。根據實際情況選擇自己需要的幅度。
關於pid結構體的初始化,我在主函數中使用 memset()函數進行初始化,比較方便。
初始化之后,就是給賦上自己調試的參數。
//初始化PI調節器參數全為0, 該函數在string.h memset(&ASR, 0, sizeof(PID_value)); ASR.KP = distance_kp; ASR.KI = distance_ki; ASR.KD = distance_kd; ASR.T = T0; ASR.out_max = limit_max; ASR.out_min = limit_min;
然后利用定時器定時調用 PID_out() 函數即可。
============================================================
關於參數的整定:
首先是一位師傅給出的建議,記在這里,作為指導;《大體上是這個架構,采集,然后計算,但是有幾個要注意得問題,不注意這些,可能永遠調不好PID或者調出來得不理想,首先你的傳感器響應速度?以及執行單元執行到溫度傳感器得時間是多少,這個要去測量一下。比如我加熱單元開始加熱,我需要0.5得控制精度,那么我從開始加熱到傳感器發現溫度變化0.5所需得時間,t,那么就以這個得0.3-0.6去做采集時間,並以這個采集時間得3-5倍去作為PID計算得時間,並把這個時間,和Ki,Kd系數做一個處理。》
首先得有一個實際的項目,才能進行參數的整定。我采用的是簡單的小車距離保持。
主控:STC89C52
超聲波模塊獲取實際距離。三次平均濾波后,精度可達1cm。
程序中我設置距離為10cm,死區為(+-1.0cm)
當僅僅采用比例控制時,小車震盪比較嚴重,基本無法穩定
PID參數的整定方式:
1、理論計算法:采用被控對象的准確模型進行數學建模。
2、工程整定法:不依賴被控對象的數學模型,直接在控制系統中進行現場整定,也就是現場調試。
其實這個調節關鍵還是多看,多試,如果有能力進行模型建立的話,那將更加精確。但是一般情況下使用試湊法。剛開始我嘗試了不大概30組參數,總是不能達到理想的效果。
顯示比例系數KP一點點的測試,在超調量不大且反應靈敏的基礎上增加積分或者微分系數,但是不要一下子全加上。例如我增加了微分,控制小車的震盪,然后對KD進行反復的測試。
最后增加KI用以消除系統的穩態誤差,但是注意要一點點的增加。不要加的太多。
進行了pid整定以后,效果如圖。但是由於芯片處理速度和電機驅動性的問題,導致一直無法調節到最佳狀態。
pid調節大法:
/******************************
參數整定找最佳,從小到大順序查,
先是比例后積分,最后再把微分加,
曲線振盪很頻繁,比例度盤要放大,
曲線漂浮繞大灣,比例度盤往小扳,
曲線偏離回復慢,積分時間往下降,
曲線波動周期長,積分時間再加長,
曲線振盪頻率快,先把微分降下來,
動差大來波動慢,微分時間應加長,
理想曲線兩個波,前高后低四比一,
一看二調多分析,調節質量不會低
*******************************/
關於PID還有很多很多地方等着去實驗,我這里只是面向新手入門的。寫的很淺顯。但是一點點經驗不至於讓大家走那么多彎路。能對PID有一個初步的了解,這篇文章的目的算是達到了。
關於項目的完整代碼,在我的碼雲中:https://gitee.com/Andrew_Qian/incremental_pid_car_distance_maintenance
===========================================================
參考資料:
1、原理介紹:http://www.cnblogs.com/cjq0301/p/5184808.html
2、原理介紹:https://wenku.baidu.com/view/827c5423647d27284a735105.html
3、調參經驗:http://www.51hei.com/bbs/dpj-51884-1.html
謹以此文祭奠我在pid上面花費的那么多時間。