一、理解PID
在大多數場合中,用“開關量”來控制一個物理量,就顯得比較簡單粗暴了。有時候,是無法保持穩定的。這時,就需要一種算法:
- 它可以將需要控制的物理量帶到目標附近
- 它可以“預見”這個量的變化趨勢
- 它也可以消除因為散熱、阻力等因素造成的靜態誤差
咱們知道,P,I,D是三種不同的調節作用,既可以單獨使用(P,I,D),也可以兩個兩個用(PI,PD),也可以三個一起用(PID)。這三種作用有什么區別呢?
我們先只說PID控制器的三個最基本的參數:Kp,Ki,Kd
- Kp
P就是比例的意思。它的作用最明顯,原理也最簡單。我們先說這個:需要控制的量,比如水溫,有它現在的『當前值』,也有我們期望的『目標值』。
- 當兩者差距不大時,就讓加熱器“輕輕地”加熱一下。
- 要是因為某些原因,溫度降低了很多,就讓加熱器“稍稍用力”加熱一下。
- 要是當前溫度比目標溫度低得多,就讓加熱器“開足馬力”加熱,盡快讓水溫到達目標附近。
這就是P的作用。實際寫程序時,就讓偏差(目標減去當前)與調節裝置的“調節力度”,建立一個一次函數的關系,就可以實現最基本的“比例”控制了。Kp越大,調節作用越激進,Kp調小會讓調節作用更保守。
要是你正在制作一個平衡車,有了P的作用,你會發現,平衡車在平衡角度附近來回“狂抖”,比較難穩住。如果已經到了這一步——恭喜你!離成功只差一小步了~
- Kd
D的作用更好理解一些,所以先說說D,最后說I。剛才我們有了P的作用。你不難發現,只有P好像不能讓平衡車站起來,水溫也控制得晃晃悠悠,好像整個系統不是特別穩定,總是在“抖動”。
你心里設想一個彈簧:現在在平衡位置上。拉它一下,然后松手。這時它會震盪起來。因為阻力很小,它可能會震盪很長時間,才會重新停在平衡位置。請想象一下:要是把上圖所示的系統浸沒在水里,同樣拉它一下 :這種情況下,重新停在平衡位置的時間就短得多。我們需要一個控制作用,讓被控制的物理量的“變化速度”趨於0,即類似於“阻尼”的作用。因為,當比較接近目標時,P的控制作用就比較小了。越接近目標,P的作用越溫柔。有很多內在的或者外部的因素,使控制量發生小范圍的擺動。D的作用就是讓物理量的速度趨於0,只要什么時候,這個量具有了速度,D就向相反的方向用力,盡力剎住這個變化。Kd參數越大,向速度相反方向剎車的力道就越強。
如果是平衡小車,加上P和D兩種控制作用,如果參數調節合適,它應該可以站起來了~歡呼吧!等等,PID三兄弟好想還有一位。看起來PD就可以讓物理量保持穩定,那還要I干嘛?因為我們忽視了一種重要的情況:
- Ki
以熱水為例,假如有個人把我們的加熱裝置帶到了非常冷的地方,開始燒水了。需要燒到50℃。在P的作用下,水溫慢慢升高。直到升高到45℃時,他發現了一個不好的事情:天氣太冷,水散熱的速度,和P控制的加熱的速度相等了。這可怎么辦?
- P兄這樣想:我和目標已經很近了,只需要輕輕加熱就可以了。
- D兄這樣想:加熱和散熱相等,溫度沒有波動,我好像不用調整什么。
於是,水溫永遠地停留在45℃,永遠到不了50℃。作為一個人,根據常識,我們知道,應該進一步增加加熱的功率。可是增加多少該如何計算呢?前輩科學家們想到的方法是真的巧妙。設置一個積分量。只要偏差存在,就不斷地對偏差進行積分(累加),並反應在調節力度上。這樣一來,即使45℃和50℃相差不太大,但是隨着時間的推移,只要沒達到目標溫度,這個積分量就不斷增加。系統就會慢慢意識到:還沒有到達目標溫度,該增加功率啦!到了目標溫度后,假設溫度沒有波動,積分值就不會再變動。這時,加熱功率仍然等於散熱功率。但是,溫度是穩穩的50℃。Ki的值越大,積分時乘的系數就越大,積分效果越明顯。所以,I的作用就是,減小靜態情況下的誤差,讓受控物理量盡可能接近目標值。I在使用時還有個問題:需要設定積分限制。防止在剛開始加熱時,就把積分量積得太大,難以控制。
- 積分控制理解
PID控制器中的積分對應於下圖中誤差曲線 與坐標軸包圍的面積(圖中的灰色部分)。PID控制程序是周期性執行的,執行的周期稱為采樣周期。計算機的程序用下圖中各矩形面積之和來近似精確的積分,圖中的TS就是采樣周期。每次PID運算時,在原來的積分值的基礎上,增加一個與當前的誤差值ev(n)成正比的微小部分。誤差為負值時,積分的增量為負。
積分運算示意圖
二、PID公式及代碼
PID系統分為兩類:模擬PID(一般由硬件組成,比如電子原件)和數字PID(通過AD/DA,結合數字編程來實現):
- 模擬PID:
- 數字PID:
位置式和增量式的主要區別:位置式在程序中要加積分上限,防止積分飽和;增量式對輸出量做限制。
位置式
位置式代碼
/*位置式PID算法,接口參數結構類型*/ typedef struct { /*PID算法接口變量,用於給用戶獲取或修改PID算法的特性*/ float Kp; //比例系數 float Ki; //積分系數 float Kd; //微分系數 float ErrorLim;//誤差積分上限 float ErrorNow; //當前的誤差 float ControlOut; //控制輸出 /*PID算法內部變量,其值不能修改*/ float ErrorOld; float ErrorP; float ErrorI; float ErrorD; } PID_AbsoluteType; //右后輪控速 void PidSpeedControl_RightBack(void) { if(Motion_State&0X09)PID_RightBack.ErrorNow = TargetSpeed1 - Speed_Right_Back; if(Motion_State&0X06)PID_RightBack.ErrorNow = TargetSpeed2 - Speed_Right_Back; PID_RightBack.Kp = kp; PID_RightBack.Ki = ki; PID_RightBack.Kd = kd; PID_RightBack.ErrorLim = lim; PID_AbsoluteMode(&PID_RightBack); Right_Back_PWM = PID_RightBack.ControlOut; } //功能 :位置式PID算法。公式 :u(k) = Kp*e(k) + [Ki*e(k)+Pi(k-1)] + Kd*[e(k)-e(k-1)] void PID_AbsoluteMode(PID_AbsoluteType* PID) { if(PID->Kp < 0) PID->Kp = -PID->Kp; if(PID->Ki < 0) PID->Ki = -PID->Ki; if(PID->Kd < 0) PID->Kd = -PID->Kd; if(PID->ErrorLim < 0) PID->ErrorLim = -PID->ErrorLim; PID->ErrorP = PID->ErrorNow;//讀取現在的誤差,用於Kp控制 PID->ErrorI += PID->ErrorNow;//誤差積分,用於ki控制 if(PID->ErrorLim != 0)//微分上限和下限 { if(PID->ErrorI > PID->ErrorLim) PID->ErrorI = PID->ErrorLim; else if(PID->ErrorI < -PID->ErrorLim) PID->ErrorI = -PID->ErrorLim; } PID->ErrorD = PID->ErrorNow -PID->ErrorOld;//誤差微分,用於Kd控制 PID->ErrorOld = PID->ErrorNow;//保存現在的誤差 PID->ControlOut= PID->Kp * PID->ErrorP + PID->Ki * PID->ErrorI + PID->Kd * PID->ErrorD;//計算絕對式PID輸出 }
增量式
增量式代碼(這里沒有設置輸出上限)
/*增量式PID算法,接口參數結構類型*/ typedef struct { /*PID算法接口變量,用於給用戶獲取或修改PID算法的特性*/ float kp; //比例系數 float ki; //積分系數 float kd; //微分系數 float errNow; //當前的誤差 float dCtrOut;//控制增量輸出 float ctrOut;//控制輸出 /*PID算法內部變量,其值不能修改*/ float errOld1; float errOld2; }PID_IncrementType; //左前輪控速 void PidSpeedControl_LeftFront(void) { if(Motion_State&0X09)PID_LeftFront.errNow = TargetSpeed1 - Speed_Left_Front; if(Motion_State&0X06)PID_LeftFront.errNow = TargetSpeed2 - Speed_Left_Front; PID_LeftFront.kp = kp; PID_LeftFront.ki = ki; PID_LeftFront.kd = kd; PID_IncrementMode(&PID_LeftFront); //執行增量式PID算法 Left_Front_PWM = PID_LeftFront.ctrOut;//讀取控制值 } //增量式PID算法 //公式:u(k) = u(k) + Kp*[e(k)-e(k-1)] + Ki*e(k) + Kd*[e(k)-2e(k-1)+e(k-2)] void PID_IncrementMode(PID_IncrementType* PID) { float dErrP, dErrI, dErrD; if(PID->kp < 0) PID->kp = -PID->kp; if(PID->ki < 0) PID->ki = -PID->ki; if(PID->kd < 0) PID->kd = -PID->kd; dErrP = PID->errNow - PID->errOld1; dErrI = PID->errNow; dErrD = PID->errNow - 2 * PID->errOld1 + PID->errOld2; PID->errOld2 = PID->errOld1; //二階誤差微分 PID->errOld1 = PID->errNow; //一階誤差微分 //增量式PID計算 PID->dCtrOut = PID->kp * dErrP + PID->ki * dErrI + PID->kd * dErrD; if(PID->kp == 0 && PID->ki == 0 && PID->kd == 0) PID->ctrOut = 0; else PID->ctrOut += PID->dCtrOut; }
三、增量式PID計算公式的4個疑問與理解
PID就是對輸入偏差進行比例積分微分運算,運算的疊加結果去控制執行機構。實踐練習中,如何把這一原理轉化為程序?為什么是用那幾個error進行計算?
以下是我摘錄的一段PID程序,我曾用其對智能車的速度進行閉環控制:
P:Proportional 比例
I:Integrating 積分
D:Differentiation 微分
Pwm_value:輸出Pwm暫空比的值
Current_error:當前偏差 last_error:上次偏差 prev_error:上上次偏差
增量式PID計算公式:
P=Kp*(current_error﹣last_error);
D=Kd*(current_error﹣2*last_error﹢prev_error);
I=Ki*current_error;
PID_add=Pwm_value+P﹢I﹢D;
一、為什么是PID_add=Pwm_value+(P﹢I﹢D)而不是PID_add=P+I+D?
如左圖,有一個人前往目的地A,他用眼睛視覺傳感器目測到距離目的地還有100m,即當前與目的地的偏差為100,他向雙腳輸出Δ=100J的能量,跑呀跑,10s之后,他又目測了一次,此時距離為40m,即current_error=40,他與10s前的偏差last_error=10對比,即current_error—last_error=—60,這是個負數,他意識到自己已經比較接近目的地,可以不用跑那么快,於是輸出Δ=100+(—60)=40J的能量,40J的能量他剛好以4m/s的速度跑呀跑,10s之后,他發現已經到達目的點,此時current_error=0,大腦經過思考得出current_error—last_error=0—40=—40,兩腳獲得的能量Δ=40+(—40)=0,即他已經達到目的地,無需再跑。在剛才的敘述中,可知增量式P+I+D輸出的是一個增量,將該增量與調節量相加后的到的才是最終輸出量,P+I+D反應的是之前的輸出量是在當前的狀態中是該增加還是該減少。
二、純比例控制P=Kp*(current_error﹣last_error),怎樣理解﹙current_error﹣last_error ﹚?
PID中純比例控制就是把被控制量的偏差乘以一個系數作為調節器的輸出,在增量式PID中,反映在程序上的,我們被控制量就是error,而實際上,例如在速度控制中error=目標速度﹣當前速度,所以明確目的:我們通過控制error趨近於0,最終使得當前速度趨近於目標速度。
如右圖,假如考試時有這么一種題:函數經過時間Δt,由y1變化為y2時,問y增長的比例為多少?你很容易地得出答案:K=﹙y2-y1﹚/Δt;
以速度控制為例,若y為error,得右圖,在時間t1到t2的過程中,我們可以得到輸出控制量error變化的趨勢為(current_error—last_error)/Δt。得到偏差的變化趨勢后,乘以Kp使輸出量與error相對變化。這個道理猶如模擬電子電路中,聲音信號經過功放管放大輸出的信號與輸入信號相對應變化。
三、微分控制:
然而,通常情況下,我們的被控制量並非純比例式地變化,如下圖:
比例表示變化趨勢,微分則表示變化趨勢的變化率,映射到一個圖像曲線中即為導數的變化!圖3中若求曲線中x2至x1某點的斜率,當Δt足夠小時,則可近似為(y2—y1)/Δt ,可知x3到x1導數的變化為﹛﹙y3—y2﹚—(y2—y1﹚﹜/Δt =﹙y3—2*y2﹢y1﹚/Δt 。將不同時間的y1、y2、y3映射為prev_error、last_error、current_error;則error變化趨勢的變化為﹛﹙current_error—last_error﹚﹣﹙last_error—prev_error﹚﹜/Δt=﹛﹙current_error—2*last_error﹢prev_error﹚﹜/Δt,可得微分D=Kd*(current_error﹣2*last_error﹢prev_error)。 在系統中加入微分放映系統偏差信號的變化率,能預知偏差變化的趨勢,具有超前控制作用,提前處理偏差。
四、積分控制:
積分控制可以消除偏差,體現在公式中較容易理解,當前的偏差差經過系數Ki的放大后映射為輸出控制量,即I=Ki*current_error。P只要前后偏差之差為0,即current_error—last_current=0,則不進行調節,D只要前后偏差變化率為0,即(current_error﹣2*last_error﹢prev_error)=0,則不進行調節。而對於積分只要偏差存在,調節就始終進行,因此積分可以消除誤差度,但在在某些情況下,一定范圍內的誤差是允許的,而如果此時積分調節始終存在,可能會導致系統穩定性下降,如右圖,此時可通過弱化積分系數Ki使系統穩定。