文章目錄
- 引言
- 一、基本原理
- 1.1.簡介
- 1.2.開環與閉環
- 1.3.PID 的公式
- 1.3.1.比例項(Proportional)
- 1.3.2.積分項(Integral)
- 1.3.3.微分項(Differential)
- 1.4.連續形式與離散形式的 PID 公式
- 1.4.1.連續形式
- 1.4.2.離散形式
- 1.5.位置式 PID 與增量式 PID
- 1.5.1.位置式
- 1.5.2.增量式
- 1.6.程序實現
- 二、實驗
- 2.1.位置式PID定速控制
- 2.2.增量式 PID 定速控制
- 2.3.位置式 PID 定位置控制
- 2.4.增量式 PID 定位置控制
- 三、算法改進
- 3.1.積分限幅
- 3.2.積分分離
- 3.3.變速積分
- 3.4.微分先行
- 3.5.不完全微分
- 3.6.輸出偏移
- 3.7.輸入死區
引言
在工業領域上,精準性和穩定性是非常重要的,人工的實時調節不能夠完全勝任高精度和高穩定性的工作,于是用數學手段替代人工經驗,自動校正系統誤差(設定值與實際值的偏差)。
一、基本原理
1.1.簡介
PID(比例-積分-微分)控制器是一種廣泛應用于工業控制系統的反饋控制算法,PID 是一種閉環控制算法,它動態改變施加到被控對象的輸出值(Out),使得被控對象某一物理量的實際值(Actual),能夠快速、準確、穩定地跟蹤到指定的目標值(Target)。PID 是一種基于誤差(Error)調控的算法,其中規定:誤差=目標值-實際值,PID 的任務是使誤差始終為 0,對被控對象模型要求低,無需建模,即使被控對象內部運作規律不明確,PID 也能進行調控。PID 就像駕駛汽車時:
- P:眼睛看到偏離車道,立即打方向盤修正(現在)
- I:如果持續偏離,逐漸加大修正力度(過去)
- D:預判車輛趨勢,提前減速或減速防止過度轉向(未來)
1.2.開環與閉環
- 開環(Open Loop):控制器單向輸出值給被控對象,不獲取被控對象的反饋,控制器對被控對象的執行狀態不清楚
- 閉環(Closed Loop):控制器輸出值給被控對象,同時獲取被控對象的反饋,控制器知道被控對象的執行狀態,可以根據反饋修改輸出值以優化控制
1.3.PID 的公式
- 定義誤差:
- PID輸出值:
下圖是 PID 閉環控制圖,首先,用戶輸入目標值給被控對象并通過被控對象反饋出來的實際值算出誤差,基于本次誤差和歷史誤差算出輸出值給被控對象,實現實際值跟蹤目標值:
1.3.1.比例項(Proportional)
比例項的輸出值僅取決于當前時刻的誤差,與歷史時刻無關。當前存在誤差時,比例項輸出一個與誤差呈正比的值,當前不存在誤差時,比例項輸出 0,下面式子是只含有比例項的 PID 輸出值:
Kp 越大,比例項權重越大,系統響應越快,但超調也會隨之增加,如下圖紅框所示:
如果只是純比例項控制,系統一般會存在穩態誤差,Kp 越大,穩態誤差越小,實際值與目標值存在恒定偏差,即穩態誤差。產生穩態誤差的原因:純比例項控制時,若誤差為 0,則比例項結果也為 0。被控對象輸入 0 時,一般會自發地向一個方向偏移,產生誤差。產生誤差后,誤差非 0,比例項負反饋調控輸出,當調控輸出力度和自發偏移力度相同時,系統達到穩態.
1.3.2.積分項(Integral)
積分項的輸出值取決于 0 ~ t 所有時刻誤差的積分,與歷史時刻有關。積分項將歷史所有時刻的誤差累積,乘上積分項系數 Ki 后作為積分項輸出值,用于彌補純比例項產生的穩態誤差,若系統持續產生誤差,則積分項會不斷累積誤差,直到控制器產生動作,讓穩態誤差消失。下面是含有比例項和積分項的 PID 輸出值:
Ki 越大,積分項權重越大,穩態誤差消失越快,但系統滯后性也會隨之增加,如下圖的紅框所示:
滯后性是實際值靠近目標值的增長速度慢,這樣的現象會導致一些需要高平衡性的項目,帶來較大的影響。
1.3.3.微分項(Differential)
微分項的輸出值取決于當前時刻誤差變化的斜率,與當前時刻附近誤差變化的趨勢有關。當誤差急劇變化時,微分項會負反饋輸出相反的作用力,阻礙誤差急劇變化,斜率一定程度上反映了誤差未來的變化趨勢,這使得微分項具有 “預測未來,提前調控”的特性。微分項給系統增加阻尼,可以有效防止系統超調,尤其是慣性比較大的系統,下面式子是含有比例項、積分項和微分項的 PID 輸出值:
Kd 越大,微分項權重越大,系統阻尼越大,但系統卡頓現象也會隨之增加,如果微分項過大,就會導致比例項和積分項讓被控對象啟動,微分項卻阻止它們。
上圖的紅線是目標值,藍線是實際值,綠線是輸出值,當實際值斜率很大的時候,說明變化速度很快,輸出值就要反向輸出,讓實際值的速度減緩。
1.4.連續形式與離散形式的 PID 公式
1.4.1.連續形式
為了更好的在程序上實現 PID 式子,將積分符號和求導,變成簡單的加、減、乘、除形式,因此該式子變為離散形式。
1.4.2.離散形式
比例項不變,積分項原本的意思就是從 0 時刻開始到 t 時刻的誤差之和,離散之后就是相當于求曲線下面的面積,如下圖所示:每個調控周期為 T,一共有 8 個 T,每個時刻的誤差乘以調控周期 T,最后相加。j 是相當于 for 循環里面的變量 i,用于遍歷。
微分項就是在某點求導,離散之后,就是求當前時刻和上一個時刻連線的斜率,如下圖的紅線,求 5T 時刻的斜率,離散之后還需知道上一次的 4T 時的誤差,兩點相減之后再除以調控周期 T 即可求得:
經過以上變化,得出離散的 PID 式子:
上面式子需要考慮調控周期 T,如果將調控周期 T 直接并入 Ki 和 Kd,即可獲得下面式子:
該式子只需考慮當前誤差(error(k)),歷史誤差(error(j)),上一個誤差(error(k-1))三個誤差,其中 Kp、Ki 和 Kd 由用戶自己設置。
1.5.位置式 PID 與增量式 PID
1.5.1.位置式
位置式 PID 由連續形式 PID 直接離散得到,每次計算得到的是全量的輸出值,可以直接給被控對象,例如,閥門控制項目,位置式 PID 每次都會反饋完整的閥門狀態。
1.5.2.增量式
增量式 PID 由位置式 PID 推導得到,每次計算得到的是輸出值的增量,如果直接給被控對象,則需要被控對象內部有積分功能;增量式 PID 也可在控制器內進行積分,然后輸出積分后的結果,此時增量式 PID 與位置式 PID 整體功能沒有區別,下面是增量式 PID 的推導:
當 k = k - 1,再讓 out(k) - out(k-1),得到增量式,增量式需要用到當前時刻的誤差、上一次的誤差和上上次的誤差:
1.6.程序實現
先確定一個調控周期 T,本次實驗選擇軟件定時器設置中斷,每中斷 一次就是一次調控周期,這樣設計,主函數能夠運行更多的代碼,提高了 CPU 利用率,下面是大致代碼實現:
int main(void)
{Timer_Init();while(1){}
}void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){//每隔時間T,程序執行到該中斷服務函數//在該中斷服務函數里執行PID調控TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
- 位置式
float Target, Actual, Out; //目標值、實際值、輸出值
float Kp = 值, Ki = 值, Kd = 值; //比例項、積分項、微分項
float Error0, Error1, ErrorInt; //本次誤差、上次誤差、誤差積分int main(void)
{Timer_Init();while(1){//用戶在此處根據需求寫入PID控制器的目標值Target = 值;}
}void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){//每隔時間T,程序執行到該中斷服務函數//在該中斷服務函數里執行PID調控//獲取實際值Actual = 讀取傳感器();//獲取本次誤差和上次誤差Error1 = Error0;Error0 = Target - Actual;//誤差積分(累加)ErrorInt += Error0;//PID計算 Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);//輸出限幅if(Out > 上限){Out = 上限;}if(Out < 下限){Out = 下限;}//執行控制輸出至被控對象(Out);TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
- 增量式:增量式不需要積分累加,但是需要知道上上次的誤差,該程序是在控制器內進行積分,也就是輸出的 Out 值已經累加好的
float Target, Actual, Out; //目標值、實際值、輸出值
float Kp = 值, Ki = 值, Kd = 值; //比例項、積分項、微分項
float Error0, Error1, Error2; //本次誤差、上次誤差、上上次誤差int main(void)
{Timer_Init();while(1){//用戶在此處根據需求寫入PID控制器的目標值Target = 值;}
}void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){//每隔時間T,程序執行到該中斷服務函數//在該中斷服務函數里執行PID調控//獲取實際值Actual = 讀取傳感器();//獲取本次誤差、上次誤差和上上次的誤差Error2 = Error1;Error1 = Error0;Error0 = Target - Actual;//誤差積分(累加)ErrorInt += Error0;//PID計算 Out = Kp * (Error0 - Error1) + Ki * Error0 + Kd * (Error0 - 2 * Error1 + Error2);//輸出限幅if(Out > 上限){Out = 上限;}if(Out < 下限){Out = 下限;}//執行控制輸出至被控對象(Out);TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
二、實驗
2.1.位置式PID定速控制
實驗目的:使用電位器旋鈕修改 Kp、Ki、Kd 和 Target,修改 Kp、Ki、Kd 使得實際值盡量貼合目標值。下面是代碼實現:
int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化Key_Init(); //非阻塞式按鍵初始化Motor_Init(); //電機初始化Encoder_Init(); //編碼器初始化RP_Init(); //電位器旋鈕初始化Serial_Init(); //串口初始化,波特率9600Timer_Init(); //定時器初始化,定時中斷時間1ms/*OLED打印一個標題*/OLED_Printf(0, 0, OLED_8X16, "Speed Control");OLED_Update();while (1){/*電位器旋鈕修改Kp、Ki、Kd和目標值*//*RP_GetValue函數返回電位器旋鈕的AD值,范圍:0~4095*//* 除4095.0可以把AD值歸一化,再乘上一個系數,可以調整到一個合適的范圍*/Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,調整范圍:0~2Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,調整范圍:0~2Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,調整范圍:0~2Target = RP_GetValue(4) / 4095.0 * 300 - 150; //修改目標值,調整范圍:-150~150/*OLED顯示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Kp); //顯示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Ki); //顯示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Kd); //顯示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Target); //顯示目標值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Actual); //顯示實際值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Out); //顯示輸出值OLED_Update(); //OLED更新,調用顯示函數后必須調用此函數更新,否則顯示的內容不會更新到OLED上Serial_Printf("%f,%f,%f\r\n", Target, Actual, Out); //串口打印目標值、實際值和輸出值//配合SerialPlot繪圖軟件,可以顯示數據的波形}
}void TIM1_UP_IRQHandler(void)
{/*定義靜態變量(默認初值為0,函數退出后保留值和存儲空間)*/static uint16_t Count; //用于計次分頻if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序執行到這里一次*/Key_Tick(); //調用按鍵的Tick函數/*計次分頻*/Count ++; //計次自增if (Count >= 40) //如果計次40次,則if成立,即if每隔40ms進一次{Count = 0; //計次清零,便于下次計次/*獲取實際速度值*//*Encoder_Get函數,可以獲取兩次讀取編碼器的計次值增量*//*此值正比于速度,所以可以表示速度,但它的單位并不是速度的標準單位*//*此處每隔40ms獲取一次計次值增量,電機旋轉一周的計次值增量約為408*//*因此如果想轉換為標準單位,比如轉/秒*//*則可將此句代碼改成Actual = Encoder_Get() / 408.0 / 0.04;*/Actual = Encoder_Get();/*獲取本次誤差和上次誤差*/Error1 = Error0; //獲取上次誤差Error0 = Target - Actual; //獲取本次誤差,目標值減實際值,即為誤差值/*誤差積分(累加)*//*如果Ki不為0,才進行誤差積分,這樣做的目的是便于調試*//*因為在調試時,我們可能先把Ki設置為0,這時積分項無作用,誤差消除不了,誤差積分會積累到很大的值*//*后續一旦Ki不為0,那么因為誤差積分已經積累到很大的值了,這就導致積分項瘋狂輸出,不利于調試*/if (Ki != 0) //如果Ki不為0{ErrorInt += Error0; //進行誤差積分}else //否則{ErrorInt = 0; //誤差積分直接歸0}/*PID計算*//*使用位置式PID公式,計算得到輸出值*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*輸出限幅*/if (Out > 100) {Out = 100;} //限制輸出值最大為100if (Out < -100) {Out = -100;} //限制輸出值最小為100/*執行控制*//*輸出值給到電機PWM*//*因為此函數的輸入范圍是-100~100,所以上面輸出限幅,需要給Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
2.2.增量式 PID 定速控制
本次實驗和位置式定速控制的實驗目的一樣,下面是中斷服務函數代碼:
float Target, Actual, Out; //目標值,實際值,輸出值
float Kp, Ki, Kd; //比例項,積分項,微分項的權重
float Error0, Error1, Error2; //本次誤差,上次誤差,上上次誤差void TIM1_UP_IRQHandler(void)
{/*定義靜態變量(默認初值為0,函數退出后保留值和存儲空間)*/static uint16_t Count; //用于計次分頻if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序執行到這里一次*/Key_Tick(); //調用按鍵的Tick函數/*計次分頻*/Count ++; //計次自增if (Count >= 40) //如果計次40次,則if成立,即if每隔40ms進一次{Count = 0; //計次清零,便于下次計次/*獲取實際速度值*//*Encoder_Get函數,可以獲取兩次讀取編碼器的計次值增量*//*此值正比于速度,所以可以表示速度,但它的單位并不是速度的標準單位*//*此處每隔40ms獲取一次計次值增量,電機旋轉一周的計次值增量約為408*//*因此如果想轉換為標準單位,比如轉/秒*//*則可將此句代碼改成Actual = Encoder_Get() / 408.0 / 0.04;*/Actual = Encoder_Get();/*獲取本次誤差、上次誤差和上上次誤差*/Error2 = Error1; //獲取上上次誤差Error1 = Error0; //獲取上次誤差Error0 = Target - Actual; //獲取本次誤差,目標值減實際值,即為誤差值/*PID計算*//*使用增量式PID公式,計算得到輸出值*/Out += Kp * (Error0 - Error1) + Ki * Error0+ Kd * (Error0 - 2 * Error1 + Error2);/*輸出限幅*/if (Out > 100) {Out = 100;} //限制輸出值最大為100if (Out < -100) {Out = -100;} //限制輸出值最小為100/*執行控制*//*輸出值給到電機PWM*//*因為此函數的輸入范圍是-100~100,所以上面輸出限幅,需要給Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
增量式的現象和位置式的現象看起來是一樣的,但是在增量式的實驗中,當 PID 三個值確定之后,設定一個目標值,再讓實際值跟上目標值,最后把 PID 三個值調整為 0,這個時候還有輸出值,是因為增量式在程序里式疊加的,如果 PID 三項都為 0,輸出值還保留著上一次的輸出值,只是不再有變化而已,如下圖所示:
2.3.位置式 PID 定位置控制
實驗目的:使用電位器旋鈕修改 Kp、Ki、Kd 和 Target,使得實際值轉動到目標值,本次實驗代碼與位置式定速控制實驗代碼一樣,只在中斷服務函數里面修改實際值 += 獲取編碼器速度,因為 += 就是積分表達,速度的積分就是位置。
Actual += Encoder_Get();
有下面圖可以看出,純比例項調節也可以做到盡可能貼合目標值了,綠色的輸出線在實際值與目標值變化的時候才有波動,當實際值和目標值重合時輸出值為 0。
2.4.增量式 PID 定位置控制
實驗目的:使用電位器旋鈕修改 Kp、Ki、Kd 和 Target,使得實際值轉動到目標值,本次實驗代碼與增量式定速控制實驗代碼一樣,只在中斷服務函數里面修改實際值 += 獲取編碼器速度。
Actual += Encoder_Get();
如果將 Ki 調到 0 并快速扭動改變目標值,就會出現實際值與目標值嚴重偏移的現象:
增量式定位置控制非常依賴這個積分項,沒有積分項的增量式 PID,容易出現實際值和目標值偏移的問題,或者,如果上一次的輸出值式錯誤的,同時后續的 Error 變化很小,則純比例項調節很難糾正這個錯誤。此外,增量式的純比例項調節,需要借助上一次的輸出值,這使得即使是純比例項調節,也會有一點積分項的特性,因此,增量式 PID 最好不要給 Ki 為 0。
三、算法改進
以下修改均在位置式 PID 定位置控制實驗里。
3.1.積分限幅
要解決的問題:如果執行器因為卡住、斷電、損壞等原因不能消除誤差,則誤差積分會無限制加大,進而達到深度飽和狀態,此時 PID 控制器會持續輸出最大的調控力,即使后續執行器恢復正常,PID 控制器在短時間內也會維持最大的調控力,直到誤差積分從深度飽和狀態退出。例如,用手指抵住正在旋轉的轉盤,過一段時間再放手,得到的波形如下圖所示:
抵住的時間越久,積分項累積的輸出值就越大,恢復到穩定輸出值的時間就越久。積分限幅實現思路:對誤差積分或積分項輸出進行判斷,如果幅值超過指定閾值,則進行限制,具體操作如下:
/*誤差積分(累加)*/ErrorInt += Error0;/*積分限幅*/if (ErrorInt > 500) {ErrorInt = 500;} //限制誤差積分最大為500if (ErrorInt < -500) {ErrorInt = -500;} //限制誤差積分最小為-500Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);
3.2.積分分離
積分項作用一般位于調控后期,用來消除持續的誤差,調控前期一般誤差較大且不需要積分項作用,如果此時仍然進行積分,則調控進行到后期時,積分項可能已經累積了過大的調控力,這會導致超調。如下圖所示,定位置控制實驗里,需要實際值盡可能貼近目標值,當目標值改變過快時,就會出現下面情況,積分項一開始正方向累計輸出值過多,因此需要負方向輸出抵消,拿實例來說明,轉盤轉動過快,出現轉過目標位置的情況,因此需要往回轉動回到目標位置。
積分分離實現思路:對誤差大小進行判斷,如果誤差絕對值小于指定閾值,則加入積分項作用,反之,則直接將誤差積分清零或不加入積分項作用,下面實現積分分離,在獲得本次誤差和上次誤差下面加上一下代碼:
/*誤差積分+積分分離*/if (fabs(Error0) < 50) //如果當前誤差值小于指定的閾值{ErrorInt += Error0; //才進行正常的誤差積分}else //否則,即當前誤差值過大{ErrorInt = 0; //此時不進行積分,同時把誤差積分直接清零}Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);
閾值 50,是通過多次實驗得出,如果超過閾值,積分項完全消失,就不會出現超過目標值的情況;如果閾值設定的太大,實驗現象的效果不是很好。
3.3.變速積分
如果積分分離閾值沒有設定好,被控對象正好在閾值之外停下來,則此時控制器完全沒有積分作用,誤差不能消除,如下圖所示:
這時可以通過調大閾值的方法來解決,也可以通過變速積分來解決。變速積分簡單來說只是在積分和不積分之間加了緩沖區,誤差值越大,積分越弱,誤差值越小,積分越強,如下圖所示:
代碼實現如下所示:
/*定義一個系數C,表示積分的速度,C的值與誤差絕對值大小呈反比,誤差絕對值越大,積分速度越慢*/float C = 1 / (0.2 * fabs(Error0) + 1); //根據公式計算得到系數C/*誤差積分*/ErrorInt += C * Error0; //積分的速度由C確定,C的取值范圍是0~1Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);
3.4.微分先行
普通 PID 的微分項對誤差進行微分,當目標值大幅度跳變時,誤差也會瞬間大幅度跳變,這會導致微分項突然輸出一個很大的調控力,如果系統的目標值頻繁大幅度切換,則此時的微分項不利于系統穩定。微分項本來就是一個阻尼的作用,當 Kp、Ki 為 0 時,Kd 無論調成多少,按理來說都不會產生輸出值,但是現實卻是 Kp、Ki 為 0 時,也會有輸出值,如下圖所示:
微分先行實現思路:將對誤差的微分替換為對實際值的微分。下面是將微分項修改之后的式子:
為什么需要加一個負號,因為是對實際值求微分,實際值在上升過程中需要一個反方向的輸出才能平緩的到達目標值;而對誤差求微分,誤差逐漸變小的時候,斜率本來就是負的,因此對實際值求微分,需要添加一個負號。
下面是實現微分先行的代碼:
float DifOut, Actual1; //定義一個微分項輸出(方便查看波形),上次實際值Actual1 = Actual; //獲取上次實際值Actual += Encoder_Get(); //獲取本次實際值/*獲取本次誤差和上次誤差*/Error1 = Error0; //獲取上次誤差Error0 = Target - Actual; //獲取本次誤差,目標值減實際值,即為誤差值/*PID計算,先得到微分項*/DifOut = - Kd * (Actual - Actual1); //這一句是微分先行的微分項計算公式//計算結果要取負,因為實際值的變化趨勢和誤差變化趨勢相反Out = Kp * Error0 + Ki * ErrorInt + DifOut;
3.5.不完全微分
傳感器獲取的實際值經常會受到噪聲干擾,而 PID 控制器中的微分項對噪聲最為敏感,這些噪聲干擾可能會導致微分項輸出抖動,進而影響系統性能,如下圖所示,下圖是噪聲對 PID 三項的影響:
- 對 P 項的影響:P 項只取決于當前誤差,影響較小
- 對 I 項的影響:原本是綠色波形下面的面積,受到噪聲的影響之后變成紅色波形下面的面積了,但是 I 項面積有正負抵消的作用,影響也不是很大
- 對 D 項的影響:原本綠色的某一時刻的斜率是正的,受到噪聲影響之后變成負的,因此受到噪聲影響最大的是 D 項
不完全微分實現思路:可以給 PID 三項都加上濾波器,這樣會導致系統響應較慢,產生一定的滯后性;只給微分項加入一階慣性單元(低通濾波器),這是對輸出值反應速度較高的做法。如下圖所示:
下面式子是不完全微分 PID 的微分項輸出,上面的是算出當前的 D 項輸出:
其中加號左邊是本次的 D 項輸入,右邊是上一次的 D 項輸出,由 a 來解決那一邊的比重較高,該式子整體的意思是本次算的 D 項輸入和上次輸出的 dout 取平均值。
下面是代碼實現:
/*模擬給實際值添加一些噪聲干擾*/Actual += rand() % 41 - 20; //使用隨機數生成函數rand生成隨機數,并將結果限定在-20~20的范圍內/*定義一個濾波強度系數a,a的取值范圍是0~1,值越大表示濾波作用越強*/float a = 0.9;DifOut = (1 - a) * Kd * (Error0 - Error1) + a * DifOut; //這一句是不完全微分的微分項計算公式Out = Kp * Error0 + Ki * ErrorInt + DifOut;
不完全微分的微分項計算公式中,等號左邊是當前的微分項輸出,右邊的是上一次的微分項輸出。
3.6.輸出偏移
對于一些啟動需要一定力度的執行器,若輸出值較小,執行器可能完全無動作,這可能會引起調控誤差,同時會降低系統響應速度。例如,啟動一個電機,當 PWM 為 2% 時,無法使得電機轉動,因此引起了調控誤差,如下圖所示:
圖中,目標值和實際值相差為 10,差值還是比較大的,可以用 Ki 來減小,但是當轉盤停下來了,輸出值還是不為 0,這是因為 P 項還有一點輸出值來驅動轉盤,但是這一點輸出值不足以使得轉盤轉動。輸出偏移實現思路:若輸出值為0,則正常輸出0,不進行調控;若輸出值非0,則給輸出值加一個固定偏移,跳過執行器無動作的階段。下面是代碼實現:
/*輸出偏移*/if (Out > 0) //如果輸出為正{Out += 6; //則直接給輸出值加上一個固定偏移}else if (Out < 0) //如果輸出為負{Out -= 6; //則直接給輸出值減去一個固定偏移}else //輸出為0{Out = 0; //輸出0}
在代碼中,固定偏移量是在現實實驗中嘗試出來的,多次嘗試,看出輸出值為多少時,轉盤才會啟動。
3.7.輸入死區
在某些系統中,輸入的目標值或實際值有微小的噪聲波動,或者系統有一定的滯后,這些情況可能會導致執行器在誤差很小時頻繁調控,不能最終穩定下來,特別是加上輸出偏移的式子,輸出值很難達到 0。解決方法是設置一個輸入死區:若誤差絕對值小于一個限度,則固定輸出 0,不進行調控,也就是容忍一定的誤差存在。當誤差小于設定的值,輸出值為 0;若大于設定的值,則有輸出值。下面是代碼實現:
if (fabs(Error0) < 5) //如果誤差絕對值比較小{Out = 0; //則直接輸出0,不進行調控}else //否則,即誤差絕對值比較大,下面進行正常的PID調控{/*誤差積分(累加)*//*如果Ki不為0,才進行誤差積分,這樣做的目的是便于調試*//*因為在調試時,我們可能先把Ki設置為0,這時積分項無作用,誤差消除不了,誤差積分會積累到很大的值*//*后續一旦Ki不為0,那么因為誤差積分已經積累到很大的值了,這就導致積分項瘋狂輸出,不利于調試*/if (Ki != 0) //如果Ki不為0{ErrorInt += Error0; //進行誤差積分}else //否則{ErrorInt = 0; //誤差積分直接歸0}/*PID計算*//*使用位置式PID公式,計算得到輸出值*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*輸出偏移*/if (Out > 0) //如果輸出為正{Out += 6; //則直接給輸出值加上一個固定偏移}else if (Out < 0) //如果輸出為負{Out -= 6; //則直接給輸出值減去一個固定偏移}else //輸出為0{Out = 0; //輸出0}}