注:本文為 “PID 算法 | stm32 直流電機控制” 相關合輯。
圖片清晰度受引文原圖所限。
略作重排,未全校去重。
如有內容異常,請看原文。
STM32—PID 控制在直流電機中的應用
Aspirant-GQ 于 2020-04-28 23:23:39 發布
一、PID 控制算法
1. PID(Proportion-Integral-Differential)
在過程控制領域,PID 算法是一種廣泛應用的控制算法。PID 控制算法通過對偏差進行比例、積分、微分運算實現控制,使偏差趨近于某一固定值。PID 控制器由三個核心單元構成:比例單元(P)、積分單元(I)和微分單元(D)。PID 控制本質上是一種基于誤差的控制方式。
PID 控制系統(模擬)的框圖如下:
2. PID 系數的理解
理解 PID 的三個系數時,可結合 PID 調節過程中的響應曲線。若要使實際響應曲線趨近于理想響應曲線,需關注三個關鍵指標:快速性(P)、準確性(I)和穩定性(D)。要使控制量保持在理想狀態,這三個參數必須調整至合理范圍。
以下是三個系數對整體控制效果的影響分析:
Ⅰ. 比例(P)部分
當輸入值與目標值產生偏差 eee 時,需減小偏差。比例系數 PPP 用于縮小偏差,使控制量向減小偏差的方向調整。比例系數 PPP 越大,偏差縮小的速度越快,因此 PPP 的作用是使控制量更快地接近目標值。
然而,當 PPP 過大時,系統易超出目標值,引發反向調整,進而在目標值附近產生振蕩。這是 PPP 過大的局限性:導致系統穩定性下降。
因此,雖然較大的 PPP 可實現快速響應,但也易引發振蕩,破壞系統穩定性。可通過適當增大微分系數 DDD 以提高系統穩定性。
Ⅱ. 積分(I)部分
積分環節的表達式如下:
KpTi∫0te(τ)dτ\frac{K_p}{T_i} \int_{0}^{t} e(\tau) \, d\tau Ti?Kp??∫0t?e(τ)dτ
由表達式可知,只要存在偏差,積分結果就會持續增大,即控制作用會不斷增強;當偏差為 0 時,積分結果為一常數,此時控制作用才可能處于穩定狀態。
積分環節的作用是消除系統的靜態偏差。一旦存在誤差,積分結果便會增加,系統隨之做出反應,直至偏差為 0。因此,積分環節保證了控制的準確性。
積分環節雖能消除靜態偏差,但會降低系統的響應速度,即積分環節對比例環節 PPP 存在抑制作用。
Ⅲ. 微分(D)部分
當比例系數 PPP 過大時,會引發系統振蕩,降低系統穩定性。可通過增大微分系數 DDD 來減弱振蕩。
微分環節的作用是根據偏差的變化趨勢預先進行糾正。由于微分可看作求導運算,而求導能反映函數的變化趨勢,因此通過微分環節可對偏差的變化進行預判并抑制,避免出現矯枉過正的現象。
微分環節的引入有助于減小振蕩,使系統趨于穩定。微分系數 DDD 越大,對比例環節 PPP 的抑制效果越強。
3. PID 的數字化處理
由于計算機控制屬于采樣控制,僅能依據采樣時刻的偏差值計算控制量,因此需通過軟件實現控制算法。對 PID 進行離散化處理(即數字化處理),也就是根據離散采樣點的偏差值實施控制。離散化處理后的變化如下:
- 用差分代替微分
- 用累加代替積分
數字化處理后的 PID 系統如下:
采樣周期越小,系統越接近模擬控制,控制效果也就越優。
二、位置閉環控制
位置閉環控制,又稱位置式 PID 控制,是傳統的 PID 控制方式。其通過使控制偏差逐漸趨近于 0,使控制量趨于目標值。
在電機控制中,位置閉環控制用于調控電機的轉動位置。通過編碼器的脈沖累加測量電機的位置信息,并與目標位置進行比較,得到偏差值。隨后通過比例、積分、微分的 PID 算法進行控制,使偏差趨于 0。
在電機控制中,輸出量為電機控制模塊輸出的占空比,作用于電機轉速。此時離散 PID 的公式如下:
根據位置式離散 PID 公式:
Pwm=Kp?e(k)+Ki?∑e(k)+Kd?[e(k)?e(k?1)]Pwm = K_p \cdot e(k) + K_i \cdot \sum e(k) + K_d \cdot [e(k) - e(k-1)]Pwm=Kp??e(k)+Ki??∑e(k)+Kd??[e(k)?e(k?1)]
其中:
- e(k)e(k)e(k):本次偏差
- e(k?1)e(k-1)e(k?1):上一次的偏差
- ∑e(k)\sum e(k)∑e(k):e(k)e(k)e(k) 及之前偏差的累積和(kkk 為 1, 2, …, kkk)
- PwmPwmPwm 代表輸出
其控制框圖如下:
控制實現代碼如下:
/* 返回輸出
* Encoder 為輸入(編碼器測量的信號,即電機轉速)
* Target 為目標值
*/
// KP、KI、KD 為 PID 的系數
int Position_PID(int Encoder, int Target)
{static float Bias, Pwm, Integral_bias, Last_bias; Bias = Encoder - Target; // 計算偏差Integral_bias += Bias; // 計算偏差積分,累加Pwm = KP * Bias + KI * Integral_bias + KD * (Bias - Last_bias); // 計算輸出,根據 PIDLast_bias = Bias; // 保存為上一次偏差return Pwm; // 返回輸出
}
在定時中斷中實現 PID 控制,定時中斷相當于離散的采樣點。中斷服務函數中可調用控制函數:
// 中斷中如下調用
Moto = Position_PID(Encoder, Target);
// 最終的控制落回到控制電機轉速上
Set_Pwm(Moto);
位置控制的調節經驗:先僅采用 PPP 控制,逐漸增大 PPP 的值,待系統出現振蕩后加入微分控制 DDD 以抑制振蕩,調整 KDK_DKD? 直至振蕩消除,之后再根據系統對響應速度和靜差的要求,調節 P、IP、IP、I 參數。
三、速度閉環控制
速度閉環控制,又稱增量式 PID 控制,與位置式 PID 控制的區別在于:位置式 PID 輸出的是控制量的新狀態,而增量式 PID 輸出的是控制量的增量。
增量式 PID 的離散公式如下:
ΔPwm=KP?[e(k)?e(k?1)]+KI?e(k)+KD?[e(k)?2e(k?1)+e(k?2)]\Delta \text{Pwm} = K_P \cdot [e(k) - e(k-1)] + K_I \cdot e(k) + K_D \cdot [e(k) - 2e(k-1) + e(k-2)] ΔPwm=KP??[e(k)?e(k?1)]+KI??e(k)+KD??[e(k)?2e(k?1)+e(k?2)]
其中:
- ΔPwm\Delta \text{Pwm}ΔPwm 為控制量的增量(如 PWM 占空比的變化量)
- e(k)e(k)e(k) 為當前時刻的偏差(設定值 - 反饋值)
- e(k?1)e(k-1)e(k?1)、e(k?2)e(k-2)e(k?2) 分別為上一時刻、上兩時刻的偏差
- KPK_PKP?、KIK_IKI?、KDK_DKD? 分別為比例、積分、微分系數
位置閉環控制用于使電機準確轉動到某一位置,而速度閉環控制用于使電機以某一速度轉動。在速度閉環控制中,僅需采用 PI 控制 即可,因此簡化后的公式如下:
Pwm+=Kp?[e(k)?e(k?1)]+Ki?e(k)\text{Pwm} += K_p \cdot [e(k) - e(k-1)] + K_i \cdot e(k) Pwm+=Kp??[e(k)?e(k?1)]+Ki??e(k)
注意:公式直接將控制量的增量疊加在上一次控制量的狀態上!
控制框圖如下:
控制代碼如下:
/* 返回輸出
* Encoder 為輸入(編碼器測量的信號,即電機轉速)
* Target 為目標值
*/
// KP、KI 為 PID 的系數
int Incremental_PI(int Encoder, int Target)
{static float Bias, Pwm, Last_bias; Bias = Encoder - Target; // 計算偏差Pwm += KP * (Bias - Last_bias) + KI * Bias; // 增量式 PILast_bias = Bias; // 保存為上一次偏差return Pwm; // 返回輸出
}
在定時中斷中實現 PID 控制,定時中斷相當于離散的采樣點。中斷服務函數中可調用控制函數:
// 中斷中如下調用
Moto = Incremental_PI(Encoder, Target);
// 最終的控制落回到控制電機轉速上
Set_Pwm(Moto);
需要注意的是,速度閉環控制采用 PI 控制,其公式與 PID 有所不同,輸出的是控制量的增量。此處將增量直接疊加在上一次的狀態量上,因此輸出的仍是控制量的新狀態。
最后附上常用的 PID 參數調整口訣:
常用口訣:
參數整定找最佳,從小到大順序查
先是比例后積分,最后再把微分加
曲線振蕩很頻繁,比例度盤要放大
曲線漂浮繞大灣,比例度盤往小扳
曲線偏離回復慢,積分時間往下降
曲線波動周期長,積分時間再加長
曲線振蕩頻率快,先把微分降下來
動差大來波動慢,微分時間應加長
理想曲線兩個波,前高后低 4 比 1
一看二調多分析,調節質量不會低
STM32 直流電機控制—PID 算法篇
“星云 - 視界” 于 2020-10-05 00:58:28 發布
一、常用的控制算法
1. 控制系統的基本結構:
控制系統的基本結構如圖:
控制目的:
控制的根本目的是使控制對象當前的狀態值與用戶的設定值相同(最大限度接近)。
基本思想:
用戶設定值 SvS_vSv? 與被控制對象當前的值 PvP_vPv? 同時送入由特定硬件電路模型或特定軟件算法組成的控制算法邏輯中,利用不同的控制算法對 SvS_vSv? 和 PvP_vPv? 進行分析、判斷、處理,從而產生當前應輸出的控制信號 OUTOUTOUT。控制信號經過執行機構施加到控制對象上,以產生預期的控制效果。
2. 常用控制算法:位式控制
① 二位式控制算法
二位式控制算法具體控制邏輯如圖所示:
特點:
- 二位式控制算法輸出的控制量只有高、低 2 種狀態。
- 執行機構使控制對象要么全額工作,要么停止工作。當 PvP_vPv? 低于 SvS_vSv? 時全額工作,PvP_vPv? 大于或等于 SvS_vSv? 時徹底停止工作。若控制對象是一個 1000 W 的加熱器,溫度未達設定值時以 1000 W 全功率運行,溫度達到設定值時停止工作。
- 由于環境因素、控制系統傳輸延時或控制對象本身的慣性等因素,控制效果往往是 PvP_vPv? 在 SvS_vSv? 的上下有較大波動。
- 在 PvP_vPv? 接近 SvS_vSv? 的臨界點時,控制輸出信號 OUTOUTOUT 往往在 HHH 和 LLL 之間頻繁轉換,導致執行部件的觸點頻繁開關動作,易產生干擾及縮短執行部件的壽命。
② 具有回差的二位式控制算法
具有回差的二位式控制算法具體控制邏輯如圖所示:
特點:
- 取 SvS_vSv? 的正負 10% 左右作為回差調節上下限,高于上限才開始輸出 LLL,低于下限才開始輸出 HHH;
- 避免了一般二位式控制算法在臨界點時執行部件頻繁動作;
- 因控制對象只有全額運行或不運行兩種狀態,仍然存在一般二位式控制算法的缺點:PvP_vPv? 總是在 SvS_vSv? 附近波動。
③ 三位式控制算法
三位式控制算法具體控制邏輯如圖所示:
特點:
- 在二位式控制的基礎上對控制對象的功率分為 0 功率(停止工作)、半功率、全功率三種情況(即三位)。
- 當前值低于設定值一定比例(一般 10%)時 OUT1OUT1OUT1 和 OUT2OUT2OUT2 同時起控制作用,控制對象全功率運行;
- 當前值在設定值的正負 10% 范圍時,OUT1OUT1OUT1 單獨作用,工作于半功率狀態;
- 當前值達到或超過設定值時 OUT1OUT1OUT1 和 OUT2OUT2OUT2 都停止輸出,控制對象停止工作;
- 相對一般二位式控制算法,三位式算法對控制對象的當前狀態值做了簡單分析,并根據不同的當前狀態值輸出不同的控制信號,能夠較好地對輸出產生控制效果。
④ 小結
位式控制的主要特征:
- 控制算法只關注控制當前的狀態值(PvP_vPv?)與設定值之間的差值——二者當前有差值就輸出控制信號,二者當前無差值就不輸出控制信號。
- 位式控制算法的輸出信號狀態單一,只輸出高、低兩種狀態,這兩種狀態對應著控制對象的工作與不工作——如果是溫度控制系統,就是加熱器加熱與不加熱。
- 由于控制系統自身的延時或者控制對象自身的慣性,位式控制算法只能使控制對象當前的狀態值在設定值附近波動,不能很好地跟蹤在設定值附近甚至與之相等。
二、PID 控制算法
1. PID 控制算法的基本思想
PID 控制算法具體控制邏輯如圖所示:
PID 算法是一種具有預見性的控制算法,其核心思想是:
- PID 算法不但考慮控制對象的當前狀態值(現在狀態),而且還考慮控制對象過去一段時間的狀態值(歷史狀態)和最近一段時間的狀態值變化(預期),由這 3 方面共同決定當前的輸出控制信號;
- PID 控制算法的運算結果是一個數,利用這個數來控制被控對象在多種工作狀態(比如加熱器的多種功率,閥門的多種開度等)工作,一般輸出形式為 PWM,基本滿足了按需輸出控制信號、根據情況隨時改變輸出的目的。
2. PID 算法分析
設某控制系統:用戶設定值為 SvS_vSv?(即希望通過 PID 控制算法使被控制對象的狀態值保持在 SvS_vSv? 附近)。
① 從系統投入運行開始,控制算法每隔一段時間對被控制對象的狀態值進行采樣。由此,可得到開機以來由各個采樣時間點被控制對象的狀態值所形成的數據序列:
X1,X2,X3,X4,…,Xk?2,Xk?1,XkX_1, X_2, X_3, X_4, \ldots, X_{k-2}, X_{k-1}, X_kX1?,X2?,X3?,X4?,…,Xk?2?,Xk?1?,Xk?
說明:
X1X_1X1?:開機以來的第一次采樣值
XkX_kXk?:目前的采樣值(最近一次的采樣值)
② 從這個采樣值序列中提取出三方面信息:
-
當前采樣值 XkX_kXk? 與用戶設定值 SvS_vSv? 之間的差值:EkE_kEk?
Ek=Sv?XkE_k = S_v - X_kEk?=Sv??Xk?分析 EkE_kEk?:
Ek>0E_k > 0Ek?>0:說明當前狀態值未達標
Ek=0E_k = 0Ek?=0:說明當前控制狀態值正好滿足要求
Ek<0E_k < 0Ek?<0:說明當前狀態值已經超標結論:EkE_kEk? 反應了控制對象當前值與設定值的偏差程度,可根據 EkE_kEk? 的大小對輸出信號 OUTOUTOUT 進行調整:偏差程度大則 OUTOUTOUT 增大,偏差程度小則 OUTOUTOUT 減小。即輸出信號的強弱與當前偏差程度的大小成比例,因此根據 EkE_kEk? 的大小來給出控制信號 OUTOUTOUT 的當前值的算法稱為比例控制(Proportion)。其數學模型可表示為:
POUT=(Kp?Ek)+Out0P_{\text{OUT}} = (K_p \cdot E_k) + \text{Out}_0POUT?=(Kp??Ek?)+Out0?
其中,KpK_pKp? 一般稱之為比例系數,可理解為硬件上的放大器(或衰減器),適當選取 KpK_pKp? 可將當前誤差值 EkE_kEk? 按一定的增益放大或縮小,以提高控制算法的響應速度;Out0\text{Out}_0Out0? 是一個常數,目的是當 EkE_kEk? 為 0 時,確保輸出信號不為 0,以避免在當前值與設定值相等時控制器輸出信號 OUTOUTOUT 為 0,導致系統處于無控制信號的失控狀態。
-
將投入運行以來的各個采樣值都與設定值相減,可得到開機以來每個采樣時刻的偏差序列數據:
E1,E2,E3,…,Ek?2,Ek?1,EkE_1, E_2, E_3, \ldots, E_{k-2}, E_{k-1}, E_kE1?,E2?,E3?,…,Ek?2?,Ek?1?,Ek?說明:
E1E_1E1?:開機的第一個采樣點與設定值的偏差,E1=Sv?X1E_1 = S_v - X_1E1?=Sv??X1?;
E2=Sv?X2E_2 = S_v - X_2E2?=Sv??X2?;
…\ldots…
Ek?2=Sv?Xk?2E_{k-2} = S_v - X_{k-2}Ek?2?=Sv??Xk?2?;
Ek?1=Sv?Xk?1E_{k-1} = S_v - X_{k-1}Ek?1?=Sv??Xk?1?;
EkE_kEk?:當前的采樣值與設定值的偏差,Ek=Sv?XkE_k = S_v - X_kEk?=Sv??Xk?。分析開機以來的誤差序列:
每個偏差值可能有 >0>0>0、<0<0<0、=0=0=0 這三種可能的值。從開機到現在,控制算法不斷輸出控制信號對被控對象進行控制,導致過去這段時間有時超標(Ex<0E_x < 0Ex?<0),有時未達標(Ex>0E_x > 0Ex?>0),有時正好滿足要求(Ex=0E_x = 0Ex?=0)。若將這些偏差值進行累加求代數和得到 SkS_kSk?,即:Sk=E1+E2+E3+…+Ek?2+Ek?1+EkS_k = E_1 + E_2 + E_3 + \ldots + E_{k-2} + E_{k-1} + E_kSk?=E1?+E2?+E3?+…+Ek?2?+Ek?1?+Ek?
分析 SkS_kSk?:
Sk>0S_k > 0Sk?>0:過去大多數時候未達標
Sk=0S_k = 0Sk?=0:過去控制效果較理想
Sk<0S_k < 0Sk?<0:過去大多數時候已經超標結論:
-
通過對 SkS_kSk? 的分析,可對控制算法過去的控制效果進行綜合評估。這體現了控制算法按照原來的方式輸出的控制信號導致了現在的控制結果,因此應利用這個值對當前要輸出的控制信號 OUTOUTOUT 進行修正,以確保控制對象在將來的一小段時間盡快達到用戶設定的值。
-
SkS_kSk? 實際上是過去每個時間點的誤差相加,與數學上的定積分運算類似,因此根據 SkS_kSk? 對輸出信號進行調節的算法稱為積分(Integral)算法。積分控制的數學模型為:
IOUT=(Kp?(1Ti∫Exdt))+Out0I_{\text{OUT}} = \left( K_p \cdot \left( \frac{1}{T_i} \int E_x \text{d}t \right) \right) + \text{Out}_0IOUT?=(Kp??(Ti?1?∫Ex?dt))+Out0?其中,KpK_pKp? 是一常數,其目的類似硬件上的放大器,用于將 SkS_kSk? 放大或衰減;Out0\text{Out}_0Out0? 是一常數,用于在歷史積分偏差值為 0 時確保系統有一個輸出值,避免失控;TiT_iTi? 是積分時間常數,取值越大,輸出量 OUTOUTOUT 越小,可理解為歷史上較早的誤差值對當前輸出信號的影響;取值越小,輸出 OUTOUTOUT 越強烈,可理解為積分只考慮了最近一段時間的誤差。實際中,若系統已運行“很長”一段時間,早期采樣的偏差值可忽略其對當前控制的影響,因此應根據情況選擇合理的 TiT_iTi? 值以獲得良好的控制效果。
-
-
最近兩次的偏差之差 DkD_kDk?
Dk=Ek?Ek?1D_k = E_k - E_{k-1}Dk?=Ek??Ek?1?
說明:
EkE_kEk?:當前的偏差
Ek?1E_{k-1}Ek?1?:當前的前一個采樣時刻的偏差值(即上一次的偏差值)
分析 DkD_kDk?:
Dk>0D_k > 0Dk?>0:說明從上一采樣時刻到當前誤差有增大趨勢
Dk=0D_k = 0Dk?=0:說明從上一采樣時刻到當前誤差平穩
Dk<0D_k < 0Dk?<0:說明從上一采樣時刻到當前誤差有減小趨勢結論:
-
DkD_kDk? 能夠說明從上次采樣到當前采樣的這段時間被控制對象的狀態變化趨勢,這種變化趨勢很可能在一定程度上延續到下一個采樣時間點,因此可根據這個變化趨勢(DkD_kDk? 的值)對輸出信號 OUTOUTOUT 進行調整,達到提前控制的目的。
-
DkD_kDk? 形如數學上的微分運算,反應了控制對象在一段時間內的變化趨勢及變化量,因此利用 DkD_kDk? 對控制器輸出信號進行調節的算法稱為微分(Differential)算法。其數學模型可表達為:
DOUT=Kp?(Td?dedt)+Out0D_{\text{OUT}} = K_p \cdot \left( T_d \cdot \frac{\text{d}e}{\text{d}t} \right) + \text{Out}_0DOUT?=Kp??(Td??dtde?)+Out0?其中,KpK_pKp? 為一常數,可理解為硬件上的放大器或衰減器,用于對輸出信號 OUTOUTOUT 的增益進行調整;Out0\text{Out}_0Out0? 為一常數,用于在 DkD_kDk? 為 0 時確保 OUTOUTOUT 有一個穩定的控制值,避免失控;TdT_dTd? 叫微分時間常數(猶如硬件上電感器的自感系數),TdT_dTd? 越大,OUTOUTOUT 增大,對輸出信號產生強烈影響。
-
③ PID 算法的形成
比例、積分、微分三種算法的優缺點分析:
POUT=(Kp?Ek)+Out0P_{\text{OUT}} = (K_p \cdot E_k) + \text{Out}_0POUT?=(Kp??Ek?)+Out0? ——比例算法
IOUT=(Kp?(1Ti∫Exdt))+Out0I_{\text{OUT}} = \left( K_p \cdot \left( \frac{1}{T_i} \int E_x \text{d}t \right) \right) + \text{Out}_0IOUT?=(Kp??(Ti?1?∫Ex?dt))+Out0? ——積分算法
DOUT=Kp?(Td?dedt)+Out0D_{\text{OUT}} = K_p \cdot \left( T_d \cdot \frac{\text{d}e}{\text{d}t} \right) + \text{Out}_0DOUT?=Kp??(Td??dtde?)+Out0? ——微分算法
比例算法:只考慮控制對象當前誤差,當前有誤差才輸出控制信號,當前沒有誤差就不輸出控制信號。即只要偏差已產生,比例算法才采取措施進行調整,因此單獨的比例算法不可能將控制對象的狀態值控制在設定值上,始終在設定值上下波動;但比例控制反應靈敏,有誤差馬上就反應到輸出。
積分算法:考慮了被控制對象的歷史誤差情況,過去的誤差狀況參與當前的輸出控制。但在系統未達到目標期間,歷史誤差往往會對當前控制產生干擾(即拖后腿),使用不當反而會干擾當前輸出。不過在系統進入穩定狀態后,特別是當前值與設定值沒有偏差時,積分算法可根據過去的偏差值輸出一個相對穩定的控制信號,以防止產生偏離目標,起到預防作用。
微分算法:單純考慮近期的變化率,當系統的偏差趨近于某一固定值時(變化率為 0),微分算法不輸出信號對其偏差進行調整,因此微分算法不能單獨使用,它只關心偏差的變化速度,不考慮是否有偏差(偏差變化率為 0 時偏差不一定是 0)。但微分算法能獲得控制對象近期的變化趨勢,可協助輸出信號盡早抑制控制對象的變化。可理解為當即將有劇烈變化時,就大幅度調整輸出信號進行抑制,避免控制對象的大幅度變化。
以上三種算法綜合起來產生一個當前的控制量對控制對象進行控制,它們的優缺點互補,即形成經典的 PID 算法。
3. PID 算法數學模型
OUT=POUT+IOUT+DOUTOUT = P_{\text{OUT}} + I_{\text{OUT}} + D_{\text{OUT}}OUT=POUT?+IOUT?+DOUT?
即:
OUT=((Kp?Ek)+Out0)+(Kp?(1Ti∫Exdt)+Out0)+(Kp?(Td?dedt)+Out0)OUT = \left( (K_p \cdot E_k) + \text{Out}_0 \right) + \left( K_p \cdot \left( \frac{1}{T_i} \int E_x \text{d}t \right) + \text{Out}_0 \right) + \left( K_p \cdot \left( T_d \cdot \frac{\text{d}e}{\text{d}t} \right) + \text{Out}_0 \right)OUT=((Kp??Ek?)+Out0?)+(Kp??(Ti?1?∫Ex?dt)+Out0?)+(Kp??(Td??dtde?)+Out0?)
整理該式,將各項的 Out0\text{Out}_0Out0? 歸并為 OUT0OUT_0OUT0?,得到:
OUT=Kp(Ek+(1Ti∫Exdt)+(Td?dedt))+OUT0OUT = K_p \left( E_k + \left( \frac{1}{T_i} \int E_x \text{d}t \right) + \left( T_d \cdot \frac{\text{d}e}{\text{d}t} \right) \right) + OUT_0OUT=Kp?(Ek?+(Ti?1?∫Ex?dt)+(Td??dtde?))+OUT0?
PID 算法在單片機中的應用
1)PID 算法在單片機中應用時,對積分和微分項可作近似變換:
對于積分項,可改寫成:
I=1Ti∑k=0nEk?TI = \frac{1}{T_i} \sum_{k=0}^{n} E_k \cdot TI=Ti?1?∑k=0n?Ek??T
即用過去一段時間的采樣點的偏差值的代數和代替積分。其中,TTT 是采樣周期,也叫控制周期,每隔 TTT 時間段進行一次 PID 計算。
對于微分項,可改寫成:
D=Td?Ek?Ek?1TD = T_d \cdot \frac{E_k - E_{k-1}}{T}D=Td??TEk??Ek?1??
其中,EkE_kEk? 為本次偏差,Ek?1E_{k-1}Ek?1? 為上次的偏差值。
2)位置式 PID 算法數學模型
由此可得到單片機中 PID 算法的表達式:
OUT=Kp(Ek+(1Ti∫Exdt)+(Td?dedt))+OUT0OUT = K_p \left( E_k + \left( \frac{1}{T_i} \int E_x \text{d}t \right) + \left( T_d \cdot \frac{\text{d}e}{\text{d}t} \right) \right) + OUT_0OUT=Kp?(Ek?+(Ti?1?∫Ex?dt)+(Td??dtde?))+OUT0?
即:
OUT=Kp(En+(1Ti∑k=0nEk?T)+(Td?Ek?Ek?1T))+OUT0OUT = K_p \left( E_n + \left( \frac{1}{T_i} \sum_{k=0}^{n} E_k \cdot T \right) + \left( T_d \cdot \frac{E_k - E_{k-1}}{T} \right) \right) + OUT_0OUT=Kp?(En?+(Ti?1?∑k=0n?Ek??T)+(Td??TEk??Ek?1??))+OUT0?
進一步展開得:
OUT=(Kp?Ek)+(Kp?TTi∑k=0nEk)+(Kp?TdT(Ek?Ek?1))+OUT0OUT = (K_p \cdot E_k) + \left( K_p \cdot \frac{T}{T_i} \sum_{k=0}^{n} E_k \right) + \left( K_p \cdot \frac{T_d}{T} (E_k - E_{k-1}) \right) + OUT_0OUT=(Kp??Ek?)+(Kp??Ti?T?∑k=0n?Ek?)+(Kp??TTd??(Ek??Ek?1?))+OUT0?
令 Ki=Kp?TTiK_i = K_p \cdot \frac{T}{T_i}Ki?=Kp??Ti?T?,Kd=Kp?TdTK_d = K_p \cdot \frac{T_d}{T}Kd?=Kp??TTd??,則:
OUT=(Kp?Ek)+(Ki∑k=0nEk)+(Kd(Ek?Ek?1))+OUT0OUT = (K_p \cdot E_k) + \left( K_i \sum_{k=0}^{n} E_k \right) + \left( K_d (E_k - E_{k-1}) \right) + OUT_0OUT=(Kp??Ek?)+(Ki?∑k=0n?Ek?)+(Kd?(Ek??Ek?1?))+OUT0?
程序設計時,利用 C 語言或匯編語言可方便實現這個計算公式。OUTOUTOUT 即為本次運算的結果,利用 OUTOUTOUT 可驅動執行機構輸出對應的控制信號,例如溫度控制中可控制 PWM 的寬度,電磁閥中可改變電磁線圈電流以改變閥門開度,或可控硅的導通角度等。這種 PID 算法計算出的結果(OUTOUTOUT 值)表示當前控制器應該輸出的控制量,因此稱為位置式(直接輸出了執行機構應該達到的狀態值)。
3)增量式 PID 算法
位置式 PID 算法計算量較大,較消耗處理器資源。在有些控制系統中,執行機構本身沒有記憶功能,如 MOS 管是否導通完全取決于控制極電壓,可控硅是否導通取決于觸發信號,繼電器是否接通取決于線圈電流等,只要控制信號丟失,執行機構就停止,在這些應用中應采用位置式 PID。
也有一些執行機構本身具有記憶功能,如步進電機,即使控制信號丟失,由于其自身的機械結構會保持在原來的位置等,在這些控制系統中,PID 算法無需輸出本次應該到達的真實位置,只需說明應在上次的基礎上對輸出信號做多大的修正(可正可負)即可,這就是增量式 PID 算法。
增量式 PID 計算出的是應在當前控制信號上的調整值,若計算結果為正,則增強輸出信號;若為負則減弱輸出信號。
增量式 PID 算法數學模型:
若用 OUTk?1OUT_{k-1}OUTk?1? 表示上次的輸出控制信號值,當前的輸出值應為 OUTkOUT_kOUTk?,則兩者關系為:
OUTk=OUTk?1+ΔOUTOUT_k = OUT_{k-1} + \Delta OUTOUTk?=OUTk?1?+ΔOUT
其中,ΔOUT\Delta OUTΔOUT 即為應輸出的增量值。上式變形得:
ΔOUT=OUTk?OUTk?1\Delta OUT = OUT_k - OUT_{k-1}ΔOUT=OUTk??OUTk?1?
本次的位置式算法輸出:
OUTk=(Kp?Ek)+(Ki∑k=0nEk)+(Kd(Ek?Ek?1))+OUT0OUT_k = (K_p \cdot E_k) + \left( K_i \sum_{k=0}^{n} E_k \right) + \left( K_d (E_k - E_{k-1}) \right) + OUT_0OUTk?=(Kp??Ek?)+(Ki?∑k=0n?Ek?)+(Kd?(Ek??Ek?1?))+OUT0? ——1 式
上次的位置式算法輸出:
OUTk?1=(Kp?Ek?1)+(Ki∑k=0n?1Ek)+(Kd(Ek?1?Ek?2))+OUT0OUT_{k-1} = (K_p \cdot E_{k-1}) + \left( K_i \sum_{k=0}^{n-1} E_k \right) + \left( K_d (E_{k-1} - E_{k-2}) \right) + OUT_0OUTk?1?=(Kp??Ek?1?)+(Ki?∑k=0n?1?Ek?)+(Kd?(Ek?1??Ek?2?))+OUT0? ——2 式
上述 1 式減 2 式即得到相鄰兩次的增量:
如前所述,Ki=Kp?TTiK_i = K_p \cdot \frac{T}{T_i}Ki?=Kp??Ti?T?,Kd=Kp?TdTK_d = K_p \cdot \frac{T_d}{T}Kd?=Kp??TTd??,則:
ΔOUT=OUTk?OUTk?1=Kp(Ek?Ek?1)+(Kp?TTi)Ek+(Kp?TdT)(Ek?2Ek?1+Ek?2)\Delta OUT = OUT_k - OUT_{k-1} = K_p (E_k - E_{k-1}) + \left( K_p \cdot \frac{T}{T_i} \right) E_k + \left( K_p \cdot \frac{T_d}{T} \right) (E_k - 2E_{k-1} + E_{k-2})ΔOUT=OUTk??OUTk?1?=Kp?(Ek??Ek?1?)+(Kp??Ti?T?)Ek?+(Kp??TTd??)(Ek??2Ek?1?+Ek?2?)
其中,EkE_kEk? 為本次的偏差,Ek?1E_{k-1}Ek?1? 為上次的偏差,Ek?2E_{k-2}Ek?2? 為上上次的偏差,KpK_pKp? 為算法增益調節,TiT_iTi? 為積分時間,TdT_dTd? 為微分時間常數。
結論:
增量式 PID 的計算只需最近 3 次的偏差(本次偏差、上次偏差、上上次偏差),不需要處理器存儲大量的歷史偏差值,計算量也相對較少,容易實現。
4)關于 TiT_iTi? 和 TdT_dTd? 的理解
在 PID 控制算法中,當前的輸出信號由比例項、積分項、微分項共同作用形成。當比例項輸出不為 0 時,若積分項對運算輸出的貢獻作用與比例項對運算輸出的貢獻相同(即同為正或同為負),積分項相當于重復了一次比例項產生作用的時間,這個時間可理解為積分時間。
當比例項不為 0 時,若微分項在一段時間里計算的結果與比例項對輸出的貢獻相同(即同為正或同為負),微分項相當于在一段時間里重復了比例項的作用,這段時間可理解為微分時間。
實際應用中應合理選擇 KpK_pKp?、TiT_iTi?、TdT_dTd? 以確保三者對輸出的貢獻平衡,從而使控制對象穩定在設定值附近。
4. 小結
PID 是比例、積分、微分的簡稱,PID 控制的難點并非編程,而是控制器的參數整定。參數整定的關鍵是正確理解各參數的物理意義,PID 控制的原理可用人對爐溫的手動控制來理解。閱讀本文不需要高深的數學知識。
-
比例控制
有經驗的操作人員手動控制電加熱爐的爐溫,可獲得非常好的控制品質,PID 控制與人工控制的控制策略有很多相似之處。下面介紹操作人員怎樣用比例控制的思想來手動控制電加熱爐的爐溫。假設用熱電偶檢測爐溫,用數字儀表顯示溫度值。在控制過程中,操作人員用眼睛讀取爐溫,并與爐溫給定值比較,得到溫度的誤差值。然后用手操作電位器,調節加熱的電流,使爐溫保持在給定值附近。
操作人員知道爐溫穩定在給定值時電位器的大致位置(我們將它稱為位置 LLL),并根據當時的溫度誤差值調整控制加熱電流的電位器的轉角。爐溫小于給定值時,誤差為正,在位置 LLL 的基礎上順時針增大電位器的轉角,以增大加熱的電流。爐溫大于給定值時,誤差為負,在位置 LLL 的基礎上反時針減小電位器的轉角,并令轉角與位置 LLL 的差值與誤差成正比。上述控制策略就是比例控制,即 PID 控制器輸出中的比例部分與誤差成正比。
閉環中存在著各種各樣的延遲作用。例如調節電位器轉角后,到溫度上升到新的轉角對應的穩態值時有較大的時間延遲。由于延遲因素的存在,調節電位器轉角后不能馬上看到調節的效果,因此閉環控制系統調節困難的主要原因是系統中的延遲作用。
比例控制的比例系數如果太小,即調節后的電位器轉角與位置 LLL 的差值太小,調節的力度不夠,使系統輸出量變化緩慢,調節所需的總時間過長。比例系數如果過大,即調節后電位器轉角與位置 LLL 的差值過大,調節力度太強,將造成調節過頭,甚至使溫度忽高忽低,來回震蕩。
增大比例系數使系統反應靈敏,調節速度加快,并且可以減小穩態誤差。但是比例系數過大會使超調量增大,振蕩次數增加,調節時間加長,動態性能變壞,比例系數太大甚至會使閉環系統不穩定。
單純的比例控制很難保證調節得恰到好處,完全消除誤差。
-
積分控制
PID 控制器中的積分對應于圖 1 中誤差曲線與坐標軸包圍的面積(圖中的灰色部分)。PID 控制程序是周期性執行的,執行的周期稱為采樣周期。計算機的程序用圖 1 中各矩形面積之和來近似精確的積分,圖中的 TST_STS? 就是采樣周期。圖 1 積分運算示意圖
每次 PID 運算時,在原來的積分值的基礎上,增加一個與當前的誤差值 ev(n)ev(n)ev(n) 成正比的微小部分。誤差為負值時,積分的增量為負。
手動調節溫度時,積分控制相當于根據當時的誤差值,周期性地微調電位器的角度,每次調節的角度增量值與當時的誤差值成正比。溫度低于設定值時誤差為正,積分項增大,使加熱電流逐漸增大,反之積分項減小。因此只要誤差不為零,控制器的輸出就會因為積分作用而不斷變化。積分調節的“大方向”是正確的,積分項有減小誤差的作用。一直要到系統處于穩定狀態,這時誤差恒為零,比例部分和微分部分均為零,積分部分才不再變化,并且剛好等于穩態時需要的控制器的輸出值,對應于上述溫度控制系統中電位器轉角的位置 LLL。因此積分部分的作用是消除穩態誤差,提高控制精度,積分作用一般是必須的。
PID 控制器輸出中的積分部分與誤差的積分成正比。因為積分時間 TIT_ITI? 在積分項的分母中,TIT_ITI? 越小,積分項變化的速度越快,積分作用越強。
-
PI 控制
控制器輸出中的積分項與當前的誤差值和過去歷次誤差值的累加值成正比,因此積分作用本身具有嚴重的滯后特性,對系統的穩定性不利。如果積分項的系數設置得不好,其負面作用很難通過積分作用本身迅速地修正。而比例項沒有延遲,只要誤差一出現,比例部分就會立即起作用。因此積分作用很少單獨使用,它一般與比例和微分聯合使用,組成 PI 或 PID 控制器。PI 和 PID 控制器既克服了單純的比例調節有穩態誤差的缺點,又避免了單純的積分調節響應慢、動態性能不好的缺點,因此被廣泛使用。
如果控制器有積分作用(例如采用 PI 或 PID 控制),積分能消除階躍輸入的穩態誤差,這時可以將比例系數調得小一些。
如果積分作用太強(即積分時間太小),相當于每次微調電位器的角度值過大,其累積的作用會使系統輸出的動態性能變差,超調量增大,甚至使系統不穩定。積分作用太弱(即積分時間太大),則消除穩態誤差的速度太慢,積分時間的值應取得適中。
-
微分作用
誤差的微分就是誤差的變化速率,誤差變化越快,其微分絕對值越大。誤差增大時,其微分為正;誤差減小時,其微分為負。控制器輸出量的微分部分與誤差的微分成正比,反映了被控量變化的趨勢。有經驗的操作人員在溫度上升過快,但是尚未達到設定值時,根據溫度變化的趨勢,預感到溫度將會超過設定值,出現超調。于是調節電位器的轉角,提前減小加熱的電流。這相當于士兵射擊遠方的移動目標時,考慮到子彈運動的時間,需要一定的提前量一樣。
圖 2 階躍響應曲線
圖 2 中的 c(∞)c(\infty)c(∞) 為被控量 c(t)c(t)c(t) 的穩態值或被控量的期望值,誤差 e(t)=c(∞)?c(t)e(t) = c(\infty) - c(t)e(t)=c(∞)?c(t)。在圖 2 中啟動過程的上升階段,當 c(t)<c(∞)c(t) < c(\infty)c(t)<c(∞) 時,被控量尚未超過其穩態值。但是因為誤差 e(t)e(t)e(t) 不斷減小,誤差的微分和控制器輸出的微分部分為負值,減小了控制器的輸出量,相當于提前給出了制動作用,以阻礙被控量的上升,所以可以減少超調量。因此微分控制具有超前和預測的特性,在超調尚未出現之前,就能提前給出控制作用。
閉環控制系統的振蕩甚至不穩定的根本原因在于有較大的滯后因素。因為微分項能預測誤差變化的趨勢,這種“超前”的作用可以抵消滯后因素的影響。適當的微分控制作用可以使超調量減小,增加系統的穩定性。
對于有較大的滯后特性的被控對象,如果 PI 控制的效果不理想,可以考慮增加微分控制,以改善系統在調節過程中的動態特性。如果將微分時間設置為 0,微分部分將不起作用。
微分時間與微分作用的強弱成正比,微分時間越大,微分作用越強。如果微分時間太大,在誤差快速變化時,響應曲線上可能會出現“毛刺”。
微分控制的缺點是對干擾噪聲敏感,使系統抑制干擾的能力降低。為此可在微分部分增加慣性濾波環節。
-
采樣周期
PID 控制程序是周期性執行的,執行的周期稱為采樣周期。采樣周期越小,采樣值越能反映模擬量的變化情況。但是太小會增加 CPU 的運算工作量,相鄰兩次采樣的差值幾乎沒有什么變化,將使 PID 控制器輸出的微分部分接近為零,所以也不宜將采樣周期取得過小。應保證在被控量迅速變化時(例如啟動過程中的上升階段),能有足夠多的采樣點數,不致因為采樣點數過少而丟失被采集的模擬量中的重要信息。
-
PID 參數的調整方法
在整定 PID 控制器參數時,可以根據控制器的參數與系統動態性能和穩態性能之間的定性關系,用實驗的方法來調節控制器的參數。有經驗的調試人員一般可以較快地得到較為滿意的調試結果。在調試中最重要的問題是在系統性能不能令人滿意時,知道應該調節哪一個參數,該參數應該增大還是減小。為了減少需要整定的參數,首先可以采用 PI 控制器。為了保證系統的安全,在調試開始時應設置比較保守的參數,例如比例系數不要太大,積分時間不要太小,以避免出現系統不穩定或超調量過大的異常情況。給出一個階躍給定信號,根據被控量的輸出波形可以獲得系統性能的信息,例如超調量和調節時間。應根據 PID 參數與系統性能的關系,反復調節 PID 的參數。
如果階躍響應的超調量太大,經過多次振蕩才能穩定或者根本不穩定,應減小比例系數、增大積分時間。如果階躍響應沒有超調量,但是被控量上升過于緩慢,過渡過程時間太長,應按相反的方向調整參數。
如果消除誤差的速度較慢,可以適當減小積分時間,增強積分作用。
反復調節比例系數和積分時間,如果超調量仍然較大,可以加入微分控制,微分時間從 0 逐漸增大,反復調節控制器的比例、積分和微分部分的參數。
總之,PID 參數的調試是一個綜合的、各參數互相影響的過程,實際調試過程中的多次嘗試是非常重要的,也是必須的。
三、STM32 代碼控制說明
1. 硬件電路搭建
本實驗使用司南電氣控制 STM32 單片機開發板來實現使用 PID 算法對直流電機的速度控制。
實驗材料:
- 司南電氣控制 STM32 單片機開發板
- 帶編碼器的直流電機
- OLED 顯示屏
接線如圖:
2. 軟件實現
PID 算法,速度控制代碼如下:
PID pid; //存放 PID 算法所需要的數據
void PID_Calc() //pid 計算
{u8 strr[2],str[2]; //檢測脈沖變量float DelEk;float ti,ki;
// float Iout;
// float Pout;
// float Dout;float td;float kd;float out;extern float pulse; //檢測脈沖變量if(pid.C200ms < (pid.T)) //計算時間未到,200ms{return ;}LED4=!LED4;//為什么是 2.84?/*因為電機轉一圈黃色線向單片機輸出 105.6 個脈沖=>pulse/105.6 (200ms 的圈數)=>(pulse/105.6)*5 (1s 的圈數)=>(pulse/105.6)*5*60 (1min 的圈數)=> pulse*2.48 (1min 的圈數)*/pid.Pv = pulse * 2.84; //速度采樣pid.Ek = pid.Sv - pid.Pv; //得到當前的偏差值pid.Pout = pid.Kp * pid.Ek; //比例輸出pid.SEk += pid.Ek; //歷史偏差總和DelEk = pid.Ek - pid.Ek_1; //最近兩次偏差之差ti = pid.T / pid.Ti;ki = ti * pid.Kp;pid.Iout = ki * pid.SEk * pid.Kp; //積分輸出td = pid.Td / pid.T;kd = pid.Kp * td;pid.Dout = kd * DelEk; //微分輸出out = pid.Pout + pid.Iout + pid.Dout;//////////////////////////////////////////////////////////if(out > pid.pwmcycle){pid.OUT = pid.pwmcycle;}else if(out < 0){pid.OUT = pid.OUT0; }else {pid.OUT = out;}//pid.OUT +=; //本次的計算結果pid.Ek_1 = pid.Ek; //更新偏差TIM2->CCR1 = (u16)pid.OUT; TIM2->CCR4 = 0; //PWM 輸出TIM2->CCR2 = 0; TIM2->CCR3 = (u16)pid.OUT; //PWM 輸出sprintf(strr,"%0.1f\0",pid.OUT); //打印 PWM 輸出值OLED_ShowString(48,3,strr); //打印 PWM 輸出值sprintf(str,"%0.1f\0",pid.Pv); //打印當前速度輸出值OLED_ShowString(56,6,str); //打印當前速度輸出值pid.C200ms = 0;pulse = 0; //檢測脈沖變量清零
}
基于 STM32 的減速直流電機 PID 算法控制
混分巨獸龍某某 已于 2022-06-11 21:52:53 修改
本例程采用 HAL 庫進行項目開發(主要使用軟件 CubeMX 和 Keil5),文章末尾附有代碼開源,歡迎各位對文章進行指正和探討。
一、硬件模塊與原理圖
1. 硬件組成
硬件組成包括:STM32F103C8T6 最小系統板;0.96 英寸 LED12864(I2C 通訊模式);智能小車 12V 移動電源;25GA370 直流減速電機(帶霍爾編碼器);JDY-31 藍牙模塊;L298N 電機驅動模塊;若干杜邦線;1 個面包板。
圖片如下:
2. 模塊分析
1. L298N 電機驅動模塊
- 該模塊可驅動兩路直流電機,輸出 A 和 B 各接一直流電機即可。
- 若使用 12V 供電,將 12V 供電端口及 GND 接上電源正負即可,同時 5V 供電端可作為最小系統板的輸入電源。
- 若不需要使用 PWM 調速,僅需控制電機正反轉,則邏輯 A 與 B 跳線帽插上即可,相當于始終使能。
- 若需要使用 PWM 調速,需將跳線帽拔起,將使能端接上單片機 IO 口(定時器 IO 口,PWM 輸出模式)。
- 邏輯輸入四個端口 IN1、IN2、IN3、IN4 接單片機四個 IO 口,每兩個端口控制一路電機。
溫馨提示: 特別不建議新手或資金有限的情況下,使用電機驅動模塊直連成品開發板,很容易燒壞。
原因如下:
- 由于電機的特性,在堵轉或高負載下,電流會增大,可能影響單片機。
- 新手操作單片機可能出現短路等情況,容易導致開發板損壞。
L298N 的轉動邏輯圖:
2. 0.96 英寸 OLED(I2C 通訊)
- 目前市面上主要分為 OLED 與 LCD 兩種屏幕。
- OLED 具有自發光特性,而 LCD 需要背光,OLED 的顯示效果更好。
- OLED 支持多種接口方式:6800、8080 兩種并行接口方式,4 線的 SPI 接口,以及 IIC 接口(2 線)。
- OLED 的工作電壓不宜過高,3.3V 即可正常工作。
- OLED 的不足之處在于做大尺寸屏幕時成本較高。
本實驗采用 0.96 英寸 OLED 屏幕(通訊方式為 IIC),4 個接線柱(SCL、SDA、GND、VCC)。
IIC 通訊實現方式: IIC(Inter-Integrated Circuit)總線是一種由 PHILIPS 公司開發的兩線式串行總線,用于連接微控制器及其外圍設備。它由數據線 SDA 和時鐘線 SCL 構成,可發送和接收數據。高速 IIC 總線一般可達 400kbps 以上。
模擬 IIC 通訊:
I2C 支持多從機,即一個 I2C 控制器下可以掛載多個 I2C 從設備。這些不同的 I2C 從設備具有不同的器件地址,I2C 主控制器可以通過 I2C 設備的器件地址訪問指定的 I2C 設備。SDA 和 SCL 這兩根線必須接一個上拉電阻,一般為 4.7kΩ。其余的 I2C 從器件都掛接到 SDA 和 SCL 這兩根線上,通過 SDA 和 SCL 這兩根線訪問多個 I2C 設備。
I2C 協議: 包括起始位、停止位、數據傳輸、應答信號、I2C 寫時序、I2C 讀時序。
I2C 寫時序:
- 發送起始信號。
- 發送 I2C 設備地址,每個 I2C 器件都有一個設備地址,通過發送具體的設備地址決定訪問哪個 I2C 器件。這是一個 8 位的數據,其中高 7 位是設備地址,最后 1 位是讀寫位。若為 1,則表示這是一個讀操作;若為 0,則表示這是一個寫操作。
- I2C 器件地址后面跟著一個讀寫位,為 0 表示寫操作,為 1 表示讀操作。
- 從機發送的 ACK 應答信號。
- 重新發送起始信號。
- 發送要寫入數據的寄存器地址。
- 從機發送的 ACK 應答信號。
- 發送要寫入寄存器的數據。
- 從機發送的 ACK 應答信號。
- 發送停止信號。
I2C 讀時序:
I2C 單字節讀時序比寫時序復雜,分為四大步:第一步是發送設備地址,第二步是發送要讀取的寄存器地址,第三步重新發送設備地址,最后一步是從 I2C 從器件中讀取寄存器值。具體步驟如下:
- 主機發送起始信號。
- 主機發送要讀取的 I2C 從設備地址。
- 讀寫控制位,由于是向 I2C 從設備發送數據,因此是寫信號。
- 從機發送的 ACK 應答信號。
- 重新發送 START 信號。
- 主機發送要讀取的寄存器地址。
- 從機發送的 ACK 應答信號。
- 重新發送 START 信號。
- 重新發送要讀取的 I2C 從設備地址。
- 讀寫控制位,這里是讀信號,表示接下來是從 I2C 從設備中讀取數據。
- 從機發送的 ACK 應答信號。
- 從 I2C 器件中讀取到的數據。
- 主機發出 NO ACK 信號,表示讀取完成,不需要從機再發送 ACK 信號。
- 主機發出 STOP 信號,停止 I2C 通信。
3. JDY-31 藍牙模塊
市場上藍牙模塊種類繁多,常見的有 JDY-xx 和 HC-xx 系列。這些看似高級的藍牙功能背后,實際上是簡單的串口通訊。
USART 的全稱是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/異步串行收發器。相比 UART,它多了一條時鐘線,具備同步功能。一般 USART 可以作為 UART 使用,即不使用其同步功能。
串口通訊協議:
數據包由發送設備通過自身的 TXD 接口傳輸到接收設備的 RXD 接口。在協議層中,數據包的內容包括起始位、主體數據(8 位或 9 位)、校驗位以及停止位。通訊雙方必須將數據包的格式約定一致,才能正常收發數據。
具體如圖所示:
波特率: 由于異步通信中沒有時鐘信號,接收雙方需約定好波特率,即每秒傳輸的碼元個數,以便對信號進行解碼。常見波特率有 4800、9600、115200 等。STM32 中波特率的設置通過串口初始化結構體實現。
注意:MCU 設置的波特率大小要與藍牙 APP 設置的大小一致!
- 6 線減速電機(帶編碼器)模塊:
市面上電機種類繁多,常用的有步進電機、直流減速電機、伺服電機等。編碼器是一種用于測量電機轉速的儀器元件,常見的有霍爾編碼器、光電編碼器等。電機的驅動原理很簡單,給電壓差即可使電機轉動,調速則利用 PWM 調節實現。
編碼器原理: 編碼器是一種將角位移或角速度轉換成一串電數字脈沖的旋轉式傳感器。霍爾編碼器由霍爾碼盤和霍爾元件組成。霍爾碼盤在一定直徑的圓板上等分布置不同磁極。霍爾碼盤與電動機同軸,電動機旋轉時,霍爾元件檢測輸出若干脈沖信號。為判斷轉向,一般輸出兩組存在一定相位差的方波信號。
注意:通過判斷 A 與 B 相哪一位在前,即可判斷正轉還是反轉。
二、CubexMX 設置
使用的 MCU 為 STM32F103C8T6:
RCC:
SYS:
注意:Debug 一定要設置為 Serial Wire,否則可能出現芯片自鎖。
GPIO 設置:
定時 TIM2 用于測速與測量正轉反轉(計數器模式):
定時 3:PWM 調節:
I2C:
USART1:
之后按照個人習慣生成初始化文件。
三、代碼
自動生成的:
需要自己編寫的:
I2C 代碼:
#include "oled.h"
#include "asc.h"
#include "main.h"void WriteCmd(unsigned char I2C_Command)
{HAL_I2C_Mem_Write(&hi2c2, OLED0561_ADD, COM, I2C_MEMADD_SIZE_8BIT, &I2C_Command, 1, 100);
}void WriteDat(unsigned char I2C_Data)
{HAL_I2C_Mem_Write(&hi2c2, OLED0561_ADD, DAT, I2C_MEMADD_SIZE_8BIT, &I2C_Data, 1, 100);
}void OLED_Init(void)
{HAL_Delay(100);WriteCmd(0xAE); // display offWriteCmd(0x20); // Set Memory Addressing ModeWriteCmd(0x10); // 00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,InvalidWriteCmd(0xb0); // Set Page Start Address for Page Addressing Mode,0-7WriteCmd(0xc8); // Set COM Output Scan DirectionWriteCmd(0x00); // ---set low column addressWriteCmd(0x10); // ---set high column addressWriteCmd(0x40); // --set start line addressWriteCmd(0x81); // --set contrast control registerWriteCmd(0xff); // 0x00~0xffWriteCmd(0xa1); // --set segment re-map 0 to 127WriteCmd(0xa6); // --set normal displayWriteCmd(0xa8); // --set multiplex ratio(1 to 64)WriteCmd(0x3F);WriteCmd(0xa4); // 0xa4,Output follows RAM content;0xa5,Output ignores RAM contentWriteCmd(0xd3); // -set display offsetWriteCmd(0x00); // -not offsetWriteCmd(0xd5); // --set display clock divide ratio/oscillator frequencyWriteCmd(0xf0); // --set divide ratioWriteCmd(0xd9); // --set pre-charge periodWriteCmd(0x22);WriteCmd(0xda); // --set com pins hardware configurationWriteCmd(0x12);WriteCmd(0xdb); // --set vcomhWriteCmd(0x20); // 0x20,0.77xVccWriteCmd(0x8d); // --set DC-DC enableWriteCmd(0x14);WriteCmd(0xaf); // --turn on oled panel
}void OLED_SetPos(unsigned char x, unsigned char y)
{WriteCmd(0xb0 + y);WriteCmd(((x & 0xf0) >> 4) | 0x10);WriteCmd((x & 0x0f) | 0x01);
}void OLED_Fill(unsigned char fill_Data)
{unsigned char m, n;for (m = 0; m < 8; m++){WriteCmd(0xb0 + m); // page0-page1WriteCmd(0x00); // low column start addressWriteCmd(0x10); // high column start addressfor (n = 0; n < 128; n++){WriteDat(fill_Data);}}
}void OLED_CLS(void)
{OLED_Fill(0x00);
}void OLED_ON(void)
{WriteCmd(0X8D); // ???WriteCmd(0X14); // ???WriteCmd(0XAF); // OLED???
}void OLED_OFF(void)
{WriteCmd(0X8D); // ???WriteCmd(0X10); // ???WriteCmd(0XAE); // OLED???
}// Parameters : x,y -- ???(x:0~127, y:0~7); ch[] -- ???; TextSize -- ???(1:6*8 ; 2:8*16)
// Description : ??codetab.h??ASCII??,?6*8?8*16???
void OLED_ShowStr(unsigned char x, unsigned char y, unsigned char ch[], unsigned char TextSize)
{unsigned char c = 0, i = 0, j = 0;switch (TextSize){case 1:{while (ch[j] != '\0'){c = ch[j] - 32;if (x > 126){x = 0;y++;}OLED_SetPos(x, y);for (i = 0; i < 6; i++)WriteDat(F6x8[c][i]);x += 6;j++;}}break;case 2:{while (ch[j] != '\0'){c = ch[j] - 32;if (x > 120){x = 0;y++;}OLED_SetPos(x, y);for (i = 0; i < 8; i++)WriteDat(F8X16[c * 16 + i]);OLED_SetPos(x, y + 1);for (i = 0; i < 8; i++)WriteDat(F8X16[c * 16 + i + 8]);x += 8;j++;}}break;}
}// Parameters : x,y -- ???(x:0~127, y:0~7); N:???.h????
// Description : ??ASCII_8x16.h????,16*16???
void OLED_ShowCN(unsigned char x, unsigned char y, unsigned char N)
{unsigned char wm = 0;unsigned int adder = 32 * N;OLED_SetPos(x, y);for (wm = 0; wm < 16; wm++){WriteDat(F16x16[adder]);adder += 1;}OLED_SetPos(x, y + 1);for (wm = 0; wm < 16; wm++){WriteDat(F16x16[adder]);adder += 1;}
}// ????????????????,????????“??——???——????”??????ascll.h?????(????)
// ??????:x:?????
// ??????y:???(??0-7)
// ??????begin:????????????????ascll.c????????
// ??????num:????????
// ??????“??”,??????????????????0,1,???0,??????,??:x:0,y:2,begin:0,num:2
void OLED_ShowCN_STR(u8 x, u8 y, u8 begin, u8 num)
{u8 i;for (i = 0; i < num; i++){OLED_ShowCN(i * 16 + x, y, i + begin);} // OLED????
}// Parameters : x0,y0 -- ???(x0:0~127, y0:0~7); x1,y1 -- ???(???)???(x1:1~128,y1:1~8)
// Description : ??BMP???
void OLED_DrawBMP(unsigned char x0, unsigned char y0, unsigned char x1, unsigned char y1, unsigned char BMP[])
{unsigned int j = 0;unsigned char x, y;if (y1 % 8 == 0)y = y1 / 8;elsey = y1 / 8 + 1;for (y = y0; y < y1; y++){OLED_SetPos(x0, y);for (x = x0; x < x1; x++){WriteDat(BMP[j++]);}}
}void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 Char_Size)
{unsigned char c = 0, i = 0;c = chr - ' '; // ???????if (x > 128 - 1){x = 0;y = y + 2;}if (Char_Size == 16){OLED_SetPos(x, y);for (i = 0; i < 8; i++)WriteDat(F8X16[c * 16 + i]);OLED_SetPos(x, y + 1);for (i = 0; i < 8; i++)WriteDat(F8X16[c * 16 + i + 8]);}else{OLED_SetPos(x, y);for (i = 0; i < 6; i++)WriteDat(F6x8[c][i]);}
}u32 oled_pow(u8 m, u8 n)
{u32 result = 1;while (n--)result *= m;return result;
}// ??2???
// x,y :???
// len :????
// size:????
// mode:?? 0,????;1,????
// num:??(0~4294967295);
void OLED_ShowNum(u8 x, u8 y, u32 num, u8 len, u8 size2)
{u8 t, temp;u8 enshow = 0;for (t = 0; t < len; t++){temp = (num / oled_pow(10, len - t - 1)) % 10;if (enshow == 0 && t < (len - 1)){if (temp == 0){OLED_ShowChar(x + (size2 / 2) * t, y, ' ', size2);continue;}elseenshow = 1;}OLED_ShowChar(x + (size2 / 2) * t, y, temp + '0', size2);}
}
UART 代碼:
#include "uart.h"
uint8_t USART1_RX_BUF[USART1_REC_LEN]; // ???,??USART_REC_LEN???.
uint16_t USART1_RX_STA = 0; // ??????//bit15:??????,bit14~0:??????????
uint8_t USART1_NewData; // ??????????1????????
extern int flag;void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) // ????????
{if (huart == &huart1){if ((USART1_RX_STA & 0x8000) == 0) // ??????{if (USART1_NewData == 0x5A) // ?????0x5A{USART1_RX_STA |= 0x8000; // ??????,?USART2_RX_STA??bit15(15?)?1}else{USART1_RX_BUF[USART1_RX_STA & 0X7FFF] = USART1_NewData;if (USART1_RX_BUF[1] == 0x01){flag = 2;}USART1_RX_STA++; // ??????1if (USART1_RX_STA > (USART1_REC_LEN - 1))USART1_RX_STA = 0; // ???????,??????}}HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1);}
}
常規的編寫如上,但本人的 MCU 存在問題,單片機未能接收到預設的數據。因此,本人項目中采用了下方代碼:
#include "uart.h"
uint8_t USART1_RX_BUF[USART1_REC_LEN]; // ???,??USART_REC_LEN???.
uint16_t USART1_RX_STA = 0; // ??????//bit15:??????,bit14~0:??????????
uint8_t USART1_NewData; // ??????????1????????
extern int flag;void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) // ????????
{if (huart == &huart1){USART1_RX_BUF[USART1_RX_STA & 0X7FFF] = USART1_NewData;USART1_RX_STA++; // ??????1if (USART1_RX_STA > (USART1_REC_LEN - 1))USART1_RX_STA = 0; // ???????,??????if (USART1_RX_BUF[USART1_RX_STA - 4] == 0xA0){flag = 1;}if (USART1_RX_BUF[USART1_RX_STA - 4] == 0x90){flag = 2;}if (USART1_RX_BUF[USART1_RX_STA - 4] == 0xD0){flag = 3;}if (USART1_RX_BUF[USART1_RX_STA - 4] == 0x88){flag = 4;}if (USART1_RX_BUF[USART1_RX_STA - 4] == 0x48){flag = 5;}HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1);}
}
如果大家自己使用,可以根據自己的藍牙 APP 修改這段程序,有問題歡迎留言。
Motor 代碼:
#include "motor.h"void MOTOR_GO()
{__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 3000);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}void MOTOR_BACK()
{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}void MOTOR_STOP()
{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
}void MOTOR_UP()
{__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}void MOTOR_DOWN()
{__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 400);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}
PID
PID 算法:
PID 分為位置型和增量型。增量型通過 u(k)?u(k?1)u(k) - u(k-1)u(k)?u(k?1) 得出公式:
公式的第一部分是比例項,用于讓值按一定比例達到目標值;
第二部分是積分項,正值,在計算過程中往往會受到環境等其他因素的影響,導致值不能到達目標值;
第三部分是微分項,通常是負值,后一次偏差值往往小于前一次偏差值,目的是防止值增加過大,起到阻礙作用。
PID 代碼:
#include "pid.h"
#include "tim.h"
#include "main.h"
#include "math.h"
#include "i2c.h"
#include "oled.h"unsigned int MotorSpeed; // 全局變量,電機當前轉速
int SpeedTarget = 750; // 目標轉速
int MotorOutput; // 電機輸出// 1. 使用 TIM2 計算電機轉速
void GetMotorSpeed(void)
{int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); // 獲取計數值__HAL_TIM_GET_COUNTER(&htim2) = 0; // 清零計數器int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2); // 判斷計數方向if (Direction == 1){CaptureNumber -= 65535;}MotorSpeed = CaptureNumber;OLED_ShowNum(40, 0, MotorSpeed, 4, 16); // 顯示轉速HAL_Delay(100);OLED_CLS();
}// 2. 增量式 PID 控制器(PID 常見的有位置 PID 和增量 PID)
int Error_Last, Error_Prev; // 上次誤差,上上次誤差
int Pwm_add, Pwm; // PWM 增量,PWM 值
int Kp = 5, Ki = 3, Kd = 1; // PID 參數,比例、積分、微分系數int SpeedInnerControl(int Speed, int Target) // 速度內環控制
{int Error = Target - Speed; // 誤差 = 目標速度 - 實際速度Pwm_add = Kp * (Error - Error_Last) + // 比例項Ki * Error + // 積分項Kd * (Error - 2.0f * Error_Last + Error_Prev); // 微分項Pwm += Pwm_add; // 輸出 = 初始值 + 增量Error_Prev = Error_Last; // 更新上上次誤差Error_Last = Error; // 更新上次誤差if (Pwm > 4999)Pwm = 3000; // 限制 PWM 上下限if (Pwm < -4999)Pwm = -3000;return Pwm; // 返回輸出值
}// 3. 設置電機電壓和方向(PID 控制)
void SetMotorVoltageAndDirection(int Pwm)
{if (Pwm < 0) // 如果 PWM 小于 0{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);Pwm = (-Pwm); // PWM 取絕對值__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); // 設置 PWM 占空比}else{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIO_PIN_5, GPIO_PIN_SET);__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, Pwm); // 設置 PWM 占空比}
}void ModePID()
{GetMotorSpeed();MotorOutput = SpeedInnerControl(MotorSpeed, SpeedTarget);SetMotorVoltageAndDirection(MotorOutput);
}
主函數代碼:
while (1)
{switch (flag){case (1):MOTOR_GO();break;case (2):MOTOR_BACK();break;case (3):MOTOR_STOP();break;case (4):MOTOR_UP();break;case (5):ModePID();break;default:break;}if (flag != 5){int CaptureNumber = (short)__HAL_TIM_GET_COUNTER(&htim2); // 獲取計數值__HAL_TIM_GET_COUNTER(&htim2) = 0;int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);if (Direction == 1){CaptureNumber -= 65535;}int Speed = CaptureNumber;OLED_ShowNum(40, 0, Speed, 5, 16);HAL_Delay(100);OLED_CLS();}int Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);OLED_ShowCN_STR(0, 0, 0, 3);OLED_ShowNum(90, 0, Speed, 4, 16);OLED_ShowStr(90, 0, "cm/s", 2);OLED_ShowCN_STR(0, 3, 3, 2);if (Direction == 0){OLED_ShowCN_STR(40, 3, 5, 2);}if (Direction == 1){OLED_ShowCN_STR(40, 3, 7, 2);}
}
藍牙 APP 源代碼以及技術論文:
鏈接:https://pan.baidu.com/s/1-rbicxuyLVCq6rglCWcJTg
提取碼:huzm
使用 STM32 實現電機的 PID 控制
倩倩草原暉太郎 已于 2024-06-13 19:14:02 修改
PID 控制是一種非常古老且應用廣泛的控制算法,小到熱水壺溫度控制,大到無人機的飛行姿態和飛行速度控制等。在電機控制中,PID 算法尤為常見。本文將由淺入深介紹使用 STM32 實現電機的 PID 控制,希望能幫助到有需要的人。
直接上代碼倉庫鏈接:gitee-基于 STM32 的 PID 電機控制源碼
一、電機基本控制
直流電機的內部結構和工作原理在高中的物理教材中已經講過,這里主要討論如何使用單片機和電機驅動模塊驅動一個直流電機。
1.1 器件準備和接線
本文介紹使用的單片機型號是 STM32F103RCT6,也可以使用 C8T6。
電機驅動選擇 TB6612。
直流電機選擇這種帶霍爾傳感器的編碼電機,12V,減速比 1/30,速度 330rpm。
為了方便觀察和操作,使用了一塊 0.96 英寸的 OLED。
接線表:
模塊引腳 | 單片機引腳 |
---|---|
OLED_SCL | PB8 |
OLED_SDA | PB9 |
按鍵 K1 | PC9 |
按鍵 K2 | PC8 |
TB6612_AIN1 | PB12 |
TB6612_AIN2 | PB13 |
編碼器 A 相 | PB6 |
編碼器 B 相 | PB7 |
1.2 代碼展示
TB6612 的驅動非常簡單,使用兩個普通的 GPIO 輸出高低電平控制電機正反轉,再使用一個復用定時器的 IO 生成一個 PWM 控制電機轉速即可。
motor.c
部分代碼如下:
#include "motor.h"/*** @brief 電機方向控制引腳設置* @param None* @retval None*/
static void motor_gpio_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能 PB 端口時鐘GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13; // 端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽輸出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 50MGPIO_Init(GPIOB, &GPIO_InitStructure); // 根據設定參數初始化 GPIOB
}/*** @brief 定時器初始化* @param arr:自動重裝值,設為一個時鐘頻率的最大值* @param psc:預分頻值* @retval None*/
void Motor_PWM_Init(u16 arr, u16 psc)
{GPIO_InitTypeDef GPIO_InitStructure;TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM_OCInitTypeDef TIM_OCInitStructure;motor_gpio_Init();RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 使能 TIM1 時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 GPIOA 時鐘// 設置該引腳為復用輸出功能, 輸出 TIM1 CH1 CH4 的 PWM 脈沖波形GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // TIM_CH1 // TIM_CH4GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 復用推挽輸出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);TIM_TimeBaseStructure.TIM_Period = arr; // 設置在下一個更新事件裝入活動的自動重裝載寄存器周期的值TIM_TimeBaseStructure.TIM_Prescaler = psc; // 設置用來作為 TIMx 時鐘頻率除數的預分頻值TIM_TimeBaseStructure.TIM_ClockDivision = 0; // 設置時鐘分割: TDTS = Tck_timTIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // TIM 向上計數模式TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 根據 TIM_TimeBaseInitStruct 中指定的參數初始化 TIMx 的時間基數單位TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 選擇定時器模式: TIM 脈沖寬度調制模式 1TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 比較輸出使能TIM_OCInitStructure.TIM_Pulse = 0; // 設置待裝入捕獲比較寄存器的脈沖值TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 輸出極性: TIM 輸出比較極性高TIM_OC4Init(TIM1, &TIM_OCInitStructure); // 根據 TIM_OCInitStruct 中指定的參數初始化外設 TIMxTIM_CtrlPWMOutputs(TIM1, ENABLE); // MOE 主輸出使能TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable); // CH4 預裝載使能TIM_ARRPreloadConfig(TIM1, ENABLE); // 使能 TIMx 在 ARR 上的預裝載寄存器TIM_Cmd(TIM1, ENABLE); // 使能 TIM1
}
使用一個函數即可,輸入的是帶符號的整型變量,正負號代表選擇方向,絕對值代表占空比。
/*** @brief 電機輸出控制* @param motor_pwm: 占空比 0-7200* @retval None*/
void Set_Pwm(int motor_pwm)
{if (motor_pwm > 0){BIN1 = 1;BIN2 = 0; // 前進}else if (motor_pwm < 0){BIN1 = 0;BIN2 = 1; // 后退}else // 停止{BIN1 = 0;BIN2 = 0;}PWMB = myabs(motor_pwm);TIM_SetCompare4(TIM1, PWMB);
}
1.3 效果展示
OLED 的第一行顯示運行狀態(ON 或 OFF),第二行顯示電機 PWM 數值(+ 表示正轉,- 表示反轉)。按下 K1 切換電機旋轉方向,按下 K2,電機啟動/關閉。
gitee-基于 STM32 的 PID 電機控制源碼
二、電機速度讀取
所用的編碼器是一個霍爾傳感器,兩個霍爾元件相差 90 度放置。編碼電機速度的讀取方式從單片機讀取方式上分有定時器輸入捕獲法和外部中斷法,從編碼器原理上又分為二倍頻和四倍頻,從速度計算方式上又分為 M 法測速和 T 法測速。
下面簡單了解 M 法測速和 T 法測速。
-
M 法測速:在一個固定的計時周期內,統計這段時間內的編碼器脈沖數,從而計算速度值。
轉速 nnn 的計算公式為:
n=M0C×T0n = \frac{M_0}{C \times T_0} n=C×T0?M0??
其中:
- CCC:編碼器單圈總脈沖數
- T0T_0T0?:計數周期,單位為秒(s)
- M0M_0M0?:在計數周期內統計到的編碼器脈沖數
假設已知編碼器轉過一圈需要 100 個脈沖,在 100ms 內測得產生了 20 個脈沖。通過公式計算 n=20100×0.1=2n = \frac{20}{100 \times 0.1} = 2n=100×0.120?=2(圈/秒)。
-
T 法測速:這種方法是建立一個已知頻率的高頻脈沖并對其計數。
轉速 nnn 的計算公式為:
n=F0C×M1n = \frac{F_0}{C \times M_1} n=C×M1?F0??
其中:
- CCC:編碼器單圈總脈沖數
- F0F_0F0?:高頻脈沖的頻率
- M1M_1M1?:一個脈沖轉的圈數
T 法是利用一個已知脈沖來測量編碼器兩個脈沖之間的時間,從而計算出速度。
假設編碼器轉過一圈需要 100 個脈沖(C=100C = 100C=100),則 1 個脈沖轉了 1100\frac{1}{100}1001? 圈,用時為 20ms,即 50Hz,轉速為 0.5 圈/秒。
M 法測速和 T 法測速是解決速度計算層面的問題,下面以定時器輸入捕獲法和外部中斷法展開討論。
2.1 定時器輸入捕獲法
2.1.1 定時器輸入捕獲內部結構
首先是兩個正交信號從 GPIO 輸入到濾波器,然后再進行邊沿檢測和極性選擇,接到定時器內部的編碼器接口。
使用定時器輸入捕獲法測速其實是利用了 STM32 定時器自帶的編碼器接口功能:
編碼器接口可接收增量(正交)編碼器的信號,根據編碼器旋轉產生的正交信號脈沖,自動控制 CNT 自增或自減,從而指示編碼器的位置、旋轉方向和旋轉速度。
- 每個高級定時器和通用定時器都擁有 1 個編碼器接口。
- 兩個輸入引腳借用了輸入捕獲的通道 1 和通道 2。
下面是計數方向與編碼信號的關系:
2.1.2 代碼展示
encoder.c
部分代碼如下:
/*** @brief 編碼器初始化, 使用定時器輸入捕獲法* @param None* @retval None*/
void Encoder_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARRTIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; // PSCTIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);TIM_ICInitTypeDef TIM_ICInitStructure;TIM_ICStructInit(&TIM_ICInitStructure);TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;TIM_ICInitStructure.TIM_ICFilter = 0xF;TIM_ICInit(TIM4, &TIM_ICInitStructure);TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;TIM_ICInitStructure.TIM_ICFilter = 0xF;TIM_ICInit(TIM4, &TIM_ICInitStructure);/* TI1 和 TI2 都計數,上升沿計數 */TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);TIM_Cmd(TIM4, ENABLE);
}/*** @brief 獲取定時器的計數值* @param None* @retval None*/
int16_t Encoder_Get(void)
{int16_t Temp;Temp = TIM_GetCounter(TIM4);TIM_SetCounter(TIM4, 0);return Temp;
}
此外,還需要再使用一個定時器定時采集速度:
/*** @brief 定時器中斷,每 100ms 更新一次速度* @param None* @retval None*/
void TIM2_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){Speed = Encoder_Get();TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
2.2 外部中斷法
2.2.1 外部中斷法簡介
使用單片機引腳的跳變信號觸發中斷,然后在中斷里判斷兩個編碼器引腳的電平,讓計數值增加或者減少。我們規定,正轉計數值 +1+1+1,反轉計數值 ?1-1?1。
采用 4 倍頻計數,即 A 相的上升沿、下降沿和 B 相的上升沿、下降沿都觸發中斷。
A 相邊沿 | B 相電平 | 對應區域 | 計數值 |
---|---|---|---|
上升沿 | L | 2 | Encoder_EXTI++ |
下降沿 | H | 4 | Encoder_EXTI++ |
B 相邊沿 | A 相電平 | 對應區域 | 計數值 |
---|---|---|---|
上升沿 | H | 3 | Encoder_EXTI++ |
下降沿 | L | 1 | Encoder_EXTI++ |
當電機反轉時,A 相、B 相的信號與正轉時的相位相差 90 度。換句話說,正轉時 A 相先觸發上升沿,反轉時 B 相先觸發上升沿。
A 相邊沿 | B 相電平 | 對應區域 | 計數值 |
---|---|---|---|
上升沿 | H | 3 | Encoder_EXTI– |
下降沿 | L | 1 | Encoder_EXTI– |
B 相邊沿 | A 相電平 | 對應區域 | 計數值 |
---|---|---|---|
上升沿 | L | 2 | Encoder_EXTI– |
下降沿 | H | 4 | Encoder_EXTI– |
2.2.2 代碼展示
部分代碼如下:
/*** @brief 編碼器初始化, 使用外部中斷法* @param None* @retval None*/
void Encoder_Init(void)
{GPIO_InitTypeDef GPIO_InitStruct;EXTI_InitTypeDef EXTI_InitStruct;NVIC_InitTypeDef NVIC_InitStruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPD;GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;GPIO_Init(GPIOB, &GPIO_InitStruct);RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);// 設置 IO 口與中斷線的映射關系GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource6);GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource7);// 初始化線上中斷EXTI_InitStruct.EXTI_Line = EXTI_Line6 | EXTI_Line7;EXTI_InitStruct.EXTI_LineCmd = ENABLE;EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 跳變沿觸發EXTI_Init(&EXTI_InitStruct);// 配置中斷分組NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;NVIC_Init(&NVIC_InitStruct);
}/*** @brief 中斷服務函數,采用 4 倍頻測速* @param None* @retval None*/
int Encoder_EXTI = 0;
void EXTI9_5_IRQHandler(void)
{if (EXTI_GetITStatus(EXTI_Line6) != RESET) // 右輪 A 相 PB6{EXTI_ClearITPendingBit(EXTI_Line6); // 清除 LINE 上的中斷標志位if (PBin(6) == 0) // 這里判斷檢測到的是否是下降沿{if (PBin(7) == 0) Encoder_EXTI++; // B 相的電平如果是低,電機就是正轉加 1else Encoder_EXTI--; // 否則就是反轉減 1}else // 上升沿{if (PBin(7) == 1) Encoder_EXTI++; // B 相電平如果為高,電機就是正轉加 1else Encoder_EXTI--; // 否則就是反轉減 1}}if (EXTI_GetITStatus(EXTI_Line7) != RESET) // 右輪 B 相 PB7{EXTI_ClearITPendingBit(EXTI_Line7); // 清除 LINE 上的中斷標志位if (PBin(7) == 0) // 這里判斷檢測到的是否是下降沿{if (PBin(6) == 1) Encoder_EXTI++; // A 相的電平如果是高,電機就是正轉加 1else Encoder_EXTI--; // 否則就是反轉減 1}else // 上升沿{if (PBin(6) == 0) Encoder_EXTI++; // A 相電平如果為低,電機就是正轉加 1else Encoder_EXTI--; // 否則就是反轉減 1}}
}/*** @brief 獲取中斷的計數值* @param None* @retval None*/
int16_t Encoder_Get(void)
{int16_t Temp;Temp = Encoder_EXTI;Encoder_EXTI = 0;return Temp;
}/*** @brief 定時器中斷,每 100ms 更新一次速度* @param None* @retval None*/
void TIM2_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){Speed = Encoder_Get();TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
2.2.3 效果展示
關于電機輪子實際轉速的計算:
-
首先根據速度的計算公式本質
v=stv = \frac{s}{t} v=ts?
其中:
- ttt:已知的編碼器計數周期(100ms)
- sss:輪子在這個計數周期內走過的路程
-
下面計算 sss:
我們知道,編碼器采集的是單位時間內的脈沖個數,而且編碼器的碼盤安裝在電機軸的末端,測的是電機輸出軸的速度。電機軸還需要經過減速器才能和輪子相連。
因此,我們可以先算出單位時間內,一個脈沖輪子走過的距離:
l=2×π×R4×n×il = \frac{2 \times \pi \times R}{4 \times n \times i} l=4×n×i2×π×R?
其中:
- 2×π×R2 \times \pi \times R2×π×R 是輪子周長,RRR 是輪子半徑(34mm)
- 使用 4 倍頻計數,nnn 是編碼器的碼盤轉一圈的脈沖數(11)
- iii 是電機的減速比(30)
-
一個脈沖輪子走過的距離可以直接手算出來,那么我們再乘以編碼器測量的脈沖個數,就可以知道輪子在這個計數周期內走過的路程 sss:
s=m×ls = m \times l s=m×l
其中 mmm 是編碼器測量的脈沖個數。
上述計算過程使用 C 代碼表示如下:
/*** @brief 編碼器讀數轉換為輪子速度(mm/s)* @param encoder:編碼器計數* @retval : Velocity 輪子速度*/
int Get_Velocity_Form_Encoder(int encoder)
{float Distance, Velocity;Distance = 2 * 3.14159 * 34 / (4 * 11 * 30); // 單位是 mmVelocity = encoder * Distance / 0.1; // 單位是 mm/s。0.1 就是編碼器計數周期 100ms,0.1sreturn Velocity;
}
三、位置式 PID
3.1 計算公式
在電機控制中,我們給電機輸出的是一個 PWM 占空比的數值。
位置式 PID 控制的輸出公式為:
Pwm=Kp?e(k)+Ki?∑j=0ke(j)+Kd[e(k)?e(k?1)]\text{Pwm} = K_p \cdot e(k) + K_i \cdot \sum_{j=0}^{k} e(j) + K_d \left[ e(k) - e(k-1) \right]Pwm=Kp??e(k)+Ki??j=0∑k?e(j)+Kd?[e(k)?e(k?1)]
其中:
- e(k)e(k)e(k):本次偏差
- e(k?1)e(k-1)e(k?1):上一次的偏差
- ∑j=0ke(j)\sum_{j=0}^{k} e(j)∑j=0k?e(j):e(k)e(k)e(k) 及之前偏差的累積和(其中 kkk 為 1, 2, …)
- Pwm\text{Pwm}Pwm:輸出
- KpK_pKp?:比例項參數
- KiK_iKi?:積分項參數
- KdK_dKd?:微分項參數
控制流程圖如下:
上圖中的目標位置一般可以通過按鍵或者開關等方式編程實現改變目標值,測量位置就是通過 STM32 去采集編碼器的數據。目標位置和測量位置之間做差,這個就是目前系統的偏差。送入 PID 控制器進行計算輸出,然后再經過電機驅動的功率放大控制電機的轉動去減小偏差,最終達到目標位置的過程。
3.2 C 語言實現
位置式 PID 具體通過 C 語言實現的代碼如下:
先定義一個 PID 參數的結構體:
typedef struct
{float target_val; // 目標值float Error; // 第 $k$ 次偏差float LastError; // $\text{Error}[-1]$,第 $k-1$ 次偏差float PrevError; // $\text{Error}[-2]$,第 $k-2$ 次偏差float Kp, Ki, Kd; // 比例、積分、微分系數float integral; // 積分值float output_val; // 輸出值
} PID;
然后定義一個 PID 參數初始化的函數:
/*** @brief PID 參數初始化* @note 無* @retval 無*/
void PID_param_init(void)
{PosionPID.target_val = 3600;PosionPID.output_val = 0.0;PosionPID.Error = 0.0;PosionPID.LastError = 0.0;PosionPID.integral = 0.0;PosionPID.Kp = 10;PosionPID.Ki = 0.5;PosionPID.Kd = 0.8;
}
最后根據公式寫出位置式 PID 的實現函數:
/*** @brief 位置 PID 算法實現* @param actual_val: 實際測量值* @note 無* @retval 通過 PID 計算后的輸出*/
float PosionPID_realize(PID *pid, float actual_val)
{/* 計算目標值與實際值的誤差 */pid->Error = pid->target_val - actual_val;/* 積分項 */pid->integral += pid->Error;/* PID 算法實現 */pid->output_val = pid->Kp * pid->Error +pid->Ki * pid->integral +pid->Kd * (pid->Error - pid->LastError);/* 誤差傳遞 */pid->LastError = pid->Error;/* 返回當前實際值 */return pid->output_val;
}
函數入口參數為編碼器的速度測量值和 PID 參數的結構體,返回值為電機控制 PWM。
四、增量式 PID
4.1 計算公式
增量式 PID 也稱速度環 PID,速度閉環控制就是根據單位時間獲取的脈沖數(這里使用了 M 法測速)測量電機的速度信息,并與目標值進行比較,得到控制偏差,然后通過對偏差的比例、積分、微分進行控制,使偏差趨向于零的過程。
增量式 PID 控制的輸出公式為:
Pwm+=Kp[e(k)?e(k?1)]+Ki?e(k)+Kd[e(k)?2e(k?1)+e(k?2)]\text{Pwm} += K_p \left[ e(k) - e(k-1) \right] + K_i \cdot e(k) + K_d \left[ e(k) - 2e(k-1) + e(k-2) \right]Pwm+=Kp?[e(k)?e(k?1)]+Ki??e(k)+Kd?[e(k)?2e(k?1)+e(k?2)]
其中:
- e(k)e(k)e(k):本次偏差
- e(k?1)e(k-1)e(k?1):上一次的偏差
- e(k?2)e(k-2)e(k?2):上上次的偏差
- KpK_pKp?:比例項參數
- KiK_iKi?:積分項參數
- KdK_dKd?:微分項參數
- Pwm\text{Pwm}Pwm:代表增量輸出
在速度控制閉環系統中只使用 PI 控制,因此對 PID 控制器可簡化為以下公式:
Pwm+=Kp[e(k)?e(k?1)]+Ki?e(k)Pwm += K_p [e(k) - e(k-1)] + K_i \cdot e(k)Pwm+=Kp?[e(k)?e(k?1)]+Ki??e(k)
控制框圖和位置式的一樣。
上圖中的目標速度一般可以通過按鍵或者開關等方式編程實現改變目標值,測量速度前面在編碼器的章節已經有提到,即通過單片機定時去采集編碼器的數據并清零。目標速度和測量速度之間做差,這個就是目前系統的偏差。送入 PID 控制器進行計算輸出,然后再經過電機驅動的功率放大控制電機的轉動去減小偏差,最終達到目標速度的過程。
4.2 C 語言實現
增量式 PID 的結構體定義和成員初始化與位置式相同,通過 C 語言實現的代碼如下:
/*** @brief 速度 PID 算法實現* @param actual_val: 實際值* @note 無* @retval 通過 PID 計算后的輸出*/
float addPID_realize(PID *pid, float actual_val)
{/* 計算目標值與實際值的誤差 */pid->Error = pid->target_val - actual_val;/* PID 算法實現,照搬公式 */pid->output_val += pid->Kp * (pid->Error - pid->LastError) +pid->Ki * pid->Error +pid->Kd * (pid->Error - 2 * pid->LastError + pid->PrevError);/* 誤差傳遞 */pid->PrevError = pid->LastError;pid->LastError = pid->Error;/* 返回當前實際值 */return pid->output_val;
}
函數入口參數為編碼器的速度測量值和 PID 參數的結構體,返回值為電機控制 PWM。可以看出增量式 PID 只與最近三次的測量值有關。
五、串級 PID
串級 PID 就是先輸入位置 PID,再經過速度 PID,最后輸出。
六、P、I、D 各個參數的作用
自動控制系統的性能指標主要有三個方面:穩定性、快速性和準確性。
-
穩定性:系統在受到外作用后,若控制系統使其被控變量隨時間的增長而最終與給定期望值一致,則稱系統是穩定的,我們一般稱為系統收斂。
如果被控量隨時間的增長,越來越偏離給定值,則稱系統是不穩定的,我們一般稱為系統發散。
穩定的系統才能完成自動控制的任務,所以,系統穩定是保證控制系統正常工作的必要條件。一個穩定的控制系統其被控量偏離給定值的初始偏差應隨時間的增長逐漸減小并趨于零。
-
快速性:快速性是指系統的動態過程進行的時間長短。過程時間越短,說明系統快速性越好,過程時間持續越長,說明系統響應遲鈍,難以實現快速變化的指令信號。穩定性和快速性反映了系統在控制過程中的性能。系統在跟蹤過程中,被控量偏離給定值越小,偏離的時間越短,說明系統的動態精度偏高。
-
準確性:是指系統在動態過程結束后,其被控變量(或反饋量)對給定值的偏差而言,這一偏差即為穩態誤差,它是衡量系統穩態精度的指標,反映了動態過程后期的性能。
在實踐生產工程中,不同的控制系統對控制器效果的要求不一樣。比如平衡車、倒立擺對系統的快速性要求很高,響應太慢會導致系統失控。智能家居里面的門窗自動開合系統,對快速性要求就不高,但是對穩定性和準確性的要求就很高,所以需要嚴格控制系統的超調量和靜差。
總結
本文主要介紹了在電機的 PID 控制中常用的位置式 PID 和增量式 PID。
上述基于 STM32 的 PID 電機控制源碼庫鏈接:
- stm32_Motor_PID: 個人學習 PID 時做的,基于 stm32 的 PID 電機控制源碼,本實驗包含 6 個電機控制實驗,電機的 PWM 控制,編碼器測速_定時器捕獲法和外部中斷法,位置式 PID,增量式 PID, 串級 PID,代碼簡潔易懂,十分適合 PID 入門。 - Gitee.com
https://gitee.com/tang176084/stm32_Motor_PID/tree/master
via:
-
STM32—PID 控制在直流電機中的應用_分析 p,i,d 控制參數對直流電機運行的影響 - CSDN 博客
https://blog.csdn.net/qq_43743762/article/details/105827410 -
stm32 直流電機控制 —PID 算法篇_直流電機速度控制的 pid 算法 - CSDN 博客
https://blog.csdn.net/weixin_43281206/article/details/108916349 -
基于 stm32 的減速直流電機 PID 算法控制_直流減速電機的控制 - CSDN 博客
https://blog.csdn.net/black_sneak/article/details/125237524 -
使用 stm32 實現電機的 PID 控制_stm32pid 控制電機 - CSDN 博客
https://blog.csdn.net/weixin_43811044/article/details/127956227