- 概述
為方便學(xué)習(xí)與交流,根據(jù)自己的理解與經(jīng)驗寫了這份教程,有錯誤之處請各位讀者予以指出,具體包含以下三部分內(nèi)容:
(1) PID數(shù)字化的推導(dǎo)過程(實質(zhì):微積分的近似計算);
(2) 程序風(fēng)格介紹(程序風(fēng)格來源于TI官方案例);
(3) C有關(guān)語法簡述(語法會結(jié)合實例進(jìn)行講解)。
==========================================================================================================================================
- PID控制器的數(shù)字化
PID控制器是工業(yè)過程控制中廣泛采用的一種控制器,其中,P、I、D分別為比例(Proportion)、積分(Integral)、微分(Differential)的簡寫;將偏差的比例、積分和微分通過線性組合構(gòu)成控制量,用該控制量對受控對象進(jìn)行控制,稱為PID算法。
為了用軟件實現(xiàn)PID算法,需將PID控制器離散化。
- 整體思路
2. 方框圖
PID控制器的方框圖如圖所示:
3. 拉氏域的表達(dá)式
根據(jù)方框圖,可寫出PID控制器對應(yīng)的傳遞函數(shù):
(其中,Kp為比例系數(shù),ki為積分系數(shù),Kd為微分系數(shù))
4. 時域的表達(dá)式
在分析時,通常借助于拉氏空間,例如判斷系統(tǒng)的穩(wěn)定性與相對穩(wěn)定性;而現(xiàn)在我們關(guān)心的是時域里的問題,因此對上式進(jìn)行拉普拉斯逆變換,得到時域里的表達(dá)式:
其對應(yīng)的結(jié)構(gòu)框圖如圖所示:
5. 差分方程
該時域里的表達(dá)式不便于編程處理,因此需對該式進(jìn)行離散化處理,從而得到可編程實現(xiàn)的差分方程,分析過程如下:
(說明:PID離散化的實質(zhì)為微積分的離散化(數(shù)值化處理),由于這個推導(dǎo)過程很多教材上都有介紹,因而略去推導(dǎo)過程,只給出最終表達(dá)式,程序的算法就是基于此表達(dá)式而寫的)
數(shù)字PID控制器的增量式算法:
(其中,T為步長,即采樣周期(由微控制器的定時器確定))
記u(kT)=u(k),便得到PID控制器增量式算法的差分方程:
這樣就可編程實現(xiàn)了(或許有人會問,為什么差分方程就可編程實現(xiàn)呢?這是因為解差分方程的一般解法就是迭代法,而迭代法只需初值跟通項公式,這在計算機編程中很容易實現(xiàn))
為使編程方便,可引入中間變量,定義如下:
則,PID控制器增量式算法的差分方程變?yōu)椋?/strong>
說明:
(1)在PID增量式算法中只需對輸出u(t)作限幅處理;
(2)當(dāng)微分系數(shù) Kd=0 時,PID控制器就成了PI控制器(在編寫PID程序時默認(rèn)使其為PI調(diào)節(jié)器);
當(dāng)積分系數(shù) Ki=0 時,PID控制器就成了PD控制器。
=======================================================================================
- 基于微控制器的算法實現(xiàn)
我寫的數(shù)字PID程序如圖所示(在最后的附件部分),有兩套代碼,一套是直接函數(shù)調(diào)用(C/C++通用),另一套是使用函數(shù)指針進(jìn)行函數(shù)調(diào)用(僅適用于C),現(xiàn)從兩個方面對該程序做講解:
(一)程序風(fēng)格
程序采用了模塊化編程的思想,這樣做的目的是增強代碼的可移植性及程序的可讀性。
程序被拆分成三個模塊:
一個是PID的頭文件’PID.h’:主要是定義算法實現(xiàn)有關(guān)的數(shù)據(jù)類型;
一個是PID的源文件’PID.c’:主要是定義算法實現(xiàn)的函數(shù);
一個是主函數(shù)文件’amain.c’:PID程序的使用方法,即在主程序中做相應(yīng)的初始化工作,在中斷服務(wù)程序中進(jìn)行PID的計算。
說明:讀這個程序時可能有點困難,不過這屬情理之中的事,畢竟剛接觸這種風(fēng)格的童鞋不太能理解這種風(fēng)格的產(chǎn)生(為什么這么做)及用意(這么做的好處);我的建議是:在理解算法的原理后,根據(jù)自己的編程風(fēng)格嘗試著寫一下,然后再跟這套程序?qū)Ρ戎鴣砝斫?,推敲一下別人為什么要這么做;當(dāng)熟悉了整個流程后,你才能體會這種程序風(fēng)格的優(yōu)勢,再將這種編程風(fēng)格慢慢轉(zhuǎn)化為自己的編程風(fēng)格。
(二)程序中涉及的C語法講解
這里,我只講述為什么要采用這些語法以及采用這些語法所帶來的好處,至于細(xì)枝末節(jié)的問題,就請各位童鞋自行查閱有關(guān)資料,順帶給大家推薦一本不錯的C語言教材:C Primer Plus,畢竟學(xué)習(xí)的興趣濃度跟書籍的編排也有關(guān)。
1. 條件編譯指令
第一處:#ifndef PID_H語句
使用該語句的目的是避免造成把重復(fù)定義語句(如,結(jié)構(gòu)體類型定義)添加到工程中,而使得編譯出錯
說明:其實也可不用#ifndef語句,因為每個定義的變量都具有特定的物理含義,不會造成重復(fù)定義現(xiàn)象。
第二處:#if (PID_DEBUG) 語句
使用該語句的目的是實現(xiàn)功能切換(注意了:是在校正PID參數(shù)后手動切換,通過改變宏定義語句#define PID_DEBUG 1中的宏體實現(xiàn)),具體請看程序清單。
2. 結(jié)構(gòu)體及結(jié)構(gòu)體指針
使用結(jié)構(gòu)體類型的好處:可為實現(xiàn)某一功能的各變量進(jìn)行“打包”處理
使用結(jié)構(gòu)體指針的好處:通過傳址調(diào)用,對方便對結(jié)構(gòu)體變量本身進(jìn)行操作
3. typedef數(shù)據(jù)類型定義
使用typedef數(shù)據(jù)類型定義的好處是方便跨平臺進(jìn)行代碼移植操作;但由于教材的緣故,造成很多童鞋都停留在表面層次上的理解(typedef 數(shù)據(jù)類型 別名),因而此處作重點講解。
我的理解:任何一個typedef聲明中的標(biāo)識符不再是一個變量,而是代表一個數(shù)據(jù)類型,其表示的數(shù)據(jù)類型為正常變量聲明(去掉typedef)的那個標(biāo)識符的數(shù)據(jù)類型。
理解起來可能有點困難,現(xiàn)結(jié)合實例來講解:
[例1]
typedef int Myint;
分析:
第一步:正常變量聲明(去掉typedef)
int Myint;
該語句表示定義一個int型變量Myint(這里,Myint為變量名);
第二步:整體分析
typedef int Myint;
該語句表示定義一個Myint類型(此時,Myint為數(shù)據(jù)類型標(biāo)識符),其具體所表示的類型:int型;
應(yīng)用:
Myint a; //聲明整型變量a
[例2]
typedef struct { //省略成員 }PID;
分析:
第一步:正常變量聲明(去掉typedef)
struct { //省略成員 }PID;
該語句表示定義一個結(jié)構(gòu)體變量PID(這里,PID為變量名);
第二步:整體分析
typedef struct { //省略成員 }PID;
該語句表示定義一個PID類型(此時,PID為數(shù)據(jù)類型標(biāo)識符),其具體所表示的類型:結(jié)構(gòu)體類型,且其具有的成員同結(jié)構(gòu)體變量PID(這里,PID為變量名);
應(yīng)用:
PID ASR; //定義結(jié)構(gòu)體變量ASR
[例3]
typedef void (*PFun)(int );
分析:
第一步:正常變量聲明(去掉typedef)
void (*PFun)(int );
該語句表示定義一個函數(shù)指針PFun(這里,PFun為變量名);
第二步:整體分析
typedef void (*PFun)(int );
該語句表示定義一個PFun類型(此時,PFun為數(shù)據(jù)類型標(biāo)識符),其具體所表示的類型:函數(shù)指針類型,且其指向形參為int型,無返回值的一類函數(shù);
應(yīng)用:
PFun pf; //定義函數(shù)指針pf
說明:typedef的用法與宏定義#define的用法類似,但又有區(qū)別,體現(xiàn)在以下兩點:
(a) typedef是對數(shù)據(jù)類型的定義,而#define是對數(shù)值的定義;
(b) typedef由編譯器解釋,而#define由預(yù)處理器執(zhí)行。
4. 空形參函數(shù)和形參帶(void)函數(shù)
這是在C/C++中相當(dāng)容易混淆的地方,因此這里重點介紹一下,若是這個知識點沒搞懂,那么這個程序你就無法看懂為什么會如此定義函數(shù)指針及利用函數(shù)指針來進(jìn)行函數(shù)調(diào)用。
void本身就是一種數(shù)據(jù)類型(空類型),把void作為形參時,表示這個函數(shù)不需要參數(shù)。
在C++中,空形參表與新參為void是等價的,這是C++中明確規(guī)定的;但在C中則是兩回事:C中的空形參表僅表示函數(shù)的形參個數(shù)和類型不確定,并非沒有參數(shù),這會暫時掛起編譯器的類型檢查機制,從而造成類型安全隱患,所以在C中欲表示函數(shù)無形參時,最好用void,此時編譯器將進(jìn)行函數(shù)參數(shù)類型驗證。
[例]
void pid_calc(int); //函數(shù)聲明void (*calc_1)(int); //函數(shù)指針聲明void (*calc_2)(); //函數(shù)指針聲明void main() { //將函數(shù)的入口地址賦給函數(shù)指針 calc_1=pid_calc; //C編譯通過;C++編譯通過 calc_2=pid_calc; //C編譯通過;C++編譯失敗}
5. 函數(shù)指針及其函數(shù)調(diào)用
函數(shù)調(diào)用,除了直接調(diào)用”函數(shù)名(實參)”這種語法外,還可通過函數(shù)指針來實現(xiàn),兩者并無區(qū)別,但為了代碼的緊湊性及美觀性,建議大家使用函數(shù)指針來進(jìn)行函數(shù)調(diào)用。
在我放出的兩套代碼中,一套是直接函數(shù)調(diào)用(C/C++通用),另一套是使用函數(shù)指針進(jìn)行函數(shù)調(diào)用(僅適用于C),大家可體會這兩種用法的區(qū)別。
6. 數(shù)據(jù)類型轉(zhuǎn)換
C語言中的數(shù)據(jù)類型分為自動類型轉(zhuǎn)換與強制類型轉(zhuǎn)換
(1) 自動類型轉(zhuǎn)換(由編譯器完成)
(自動轉(zhuǎn)換的適用場合及其轉(zhuǎn)換規(guī)則,請讀者查閱有關(guān)資料)
(2) 強制類型轉(zhuǎn)換(通過類型轉(zhuǎn)換運算實現(xiàn))
在本程序中,即可以將自定義函數(shù)的函數(shù)名pid_calc(函數(shù)名代表對應(yīng)函數(shù)的入口地址)直接賦值給函數(shù)指針calc,也可將自定義函數(shù)的函數(shù)名pid_calc先強制類型轉(zhuǎn)換(轉(zhuǎn)換為函數(shù)指針)后,再賦值給函數(shù)指針calc;這兩種方式雖說能達(dá)到同樣的效果,但其所反映的思想?yún)s有所不同。
現(xiàn)把代碼截取出來,方便大家對比:
void pid_calc(PID *p); //函數(shù)聲明void (*calc)(); //函數(shù)指針:指向PID計算函數(shù)void main() { //將函數(shù)的入口地址賦給指針變量 calc=(void (*)(unsigned long))pid_calc; //編譯通過(強制類型轉(zhuǎn)換) calc=pid_calc; //編譯通過}
7. 代碼換行問題
為了代碼的美觀及調(diào)試方便,需涉及到代碼換行問題。
在本程序的宏定義語句中使用了”\”,這是宏定義中連接上下行的連接符,表示該宏定義還未結(jié)束。
//定義PID控制器的初始值#define PID_DEFAULTS {0,0, \ 0,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0, \ (void (*)(unsigned long))pid_calc}
=======================================================================================
附件一:直接函數(shù)調(diào)用(C/C++通用)
PID.h文件
//===================================================//PID.h//===================================================#ifndef PID_H#define PID_H//定義PID計算用到的結(jié)構(gòu)體類型typedef struct{ float Ref; //輸入:系統(tǒng)待調(diào)節(jié)量的給定值 float Fdb; //輸入:系統(tǒng)待調(diào)節(jié)量的反饋值 //PID控制器部分 float Kp; //參數(shù):比例系數(shù) float Ki; //參數(shù):積分系數(shù) float Kd; //參數(shù):微分系數(shù) float T; //參數(shù):離散化系統(tǒng)的采樣周期 float a0; //變量:a0 float a1; //變量: a1 float a2; //變量: a2 float Err; //變量:當(dāng)前的偏差e(k) float Err_1; //歷史:前一步的偏差e(k-1) float Err_2; //歷史:前前一步的偏差e(k-2) float Out; //輸出:PID控制器的輸出u(k) float Out_1; //歷史:PID控制器前一步的輸出u(k-1) float OutMax; //參數(shù):PID控制器的最大輸出 float OutMin; //參數(shù):PID控制器的最小輸出 }PID;//定義PID控制器的初始值#define PID_DEFAULTS {0,0, \ 0,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0}//條件編譯的判別條件#define PID_DEBUG 1 //函數(shù)聲明void pid_calc(PID *p);#endif//===================================================//End of file.//===================================================
PID.c文件
#include pid_calc(PID * (PID_DEBUG) a0=p->Kp+p->Ki*p->T+p->Kd/p->=p->Kp+*p->Kd/p->=p->Kd/p-> p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p-> p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p-> (p->Out>p->->Out=p->(p->Out<p->->Out=p-> p->Out_1=p->->Err_2=p->->Err_1=p->
amain.c主函數(shù)文件
//===================================================//amain.c//===================================================//將用戶定義的頭文件包含進(jìn)來#include "PID.h"//=============宏定義=====================#define T0 0.0002 //離散化采樣周期,單位s//============全局變量========================//定義PID控制器對應(yīng)的結(jié)構(gòu)體變量PID ASR=PID_DEFAULTS; //速度PI調(diào)節(jié)器ASR//定義PID控制器的參數(shù)及輸出限幅值float SpeedKp=2,SpeedKi=1,SpeedLimit=10; //速度PI調(diào)節(jié)器ASR//===============主程序=======================void main() { //初始化PID控制器 ASR.Kp=SpeedKp; ASR.Ki=SpeedKi; ASR.T=T0; ASR.OutMax=SpeedLimit; ASR.OutMin=-SpeedLimit; }//============中斷服務(wù)程序====================interrupt void T1UFINT_ISR(void) { //轉(zhuǎn)速調(diào)節(jié)ASR ASR.Ref=input1; //速度給定 ASR.Fdb=input2; //速度反饋 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 pid_calc(&ASR); //函數(shù)調(diào)用:啟動PID計算 output=ASR.Out; //讀取PID控制器的輸出 }//===================================================//End of file.//===================================================
=======================================================================================
附件二:使用函數(shù)指針進(jìn)行函數(shù)調(diào)用(僅適用于C)
PID.h文件
//===================================================//PID.h//===================================================#ifndef PID_H#define PID_H//定義PID計算用到的結(jié)構(gòu)體類型typedef struct{ float Ref; //輸入:系統(tǒng)待調(diào)節(jié)量的給定值 float Fdb; //輸入:系統(tǒng)待調(diào)節(jié)量的反饋值 //PID控制器部分 float Kp; //參數(shù):比例系數(shù) float Ki; //參數(shù):積分系數(shù) float Kd; //參數(shù):微分系數(shù) float T; //參數(shù):離散化系統(tǒng)的采樣周期 float a0; //變量:a0 float a1; //變量: a1 float a2; //變量: a2 float Err; //變量:當(dāng)前的偏差e(k) float Err_1; //歷史:前一步的偏差e(k-1) float Err_2; //歷史:前前一步的偏差e(k-2) float Out; //輸出:PID控制器的輸出u(k) float Out_1; //歷史:PID控制器前一步的輸出u(k-1) float OutMax; //參數(shù):PID控制器的最大輸出 float OutMin; //參數(shù):PID控制器的最小輸出 void (*calc)(); //函數(shù)指針:指向PID計算函數(shù) }PID;//定義PID控制器的初始值#define PID_DEFAULTS {0,0, \ 0,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0, \ (void (*)(unsigned long))pid_calc} //加與不加強制類型轉(zhuǎn)換都沒影響//條件編譯的判別條件#define PID_DEBUG 1 //函數(shù)聲明void pid_calc(PID *p);#endif//===================================================//End of file.//===================================================
PID.c文件
//===================================================//PID.c//===================================================#include "PID.h"//===================函數(shù)定義========================/**************************************************** *說 明: * (1)PID控制器默認(rèn)為PI調(diào)節(jié)器 * (2)使用了條件編譯進(jìn)行功能切換:節(jié)省計算時間 * 在校正PID參數(shù)時,使用宏定義將PID_DEBUG設(shè)為1; * 當(dāng)參數(shù)校正完成后,使用宏定義將PID_DEBUG設(shè)為0,同時,在初始化時 * 直接為p->a0、p->a1、p->a2賦值 ****************************************************/void pid_calc(PID *p) { //使用條件編譯進(jìn)行功能切換 #if (PID_DEBUG) float a0,a1,a2; //計算中間變量a0、a1、a2 a0=p->Kp+p->Ki*p->T+p->Kd/p->T; a1=p->Kp+2*p->Kd/p->T; a2=p->Kd/p->T; //計算PID控制器的輸出 p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p->Err_2; #else //計算PID控制器的輸出 p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p->Err_2; #endif //輸出限幅 if(p->Out>p->OutMax) p->Out=p->OutMax; if(p->Out<p->OutMin) p->Out=p->OutMin; //為下步計算做準(zhǔn)備 p->Out_1=p->Out; p->Err_2=p->Err_1; p->Err_1=p->Err; }//===================================================//End of file.//===================================================
amain.c主函數(shù)文件
//===================================================//amain.c//===================================================//將用戶定義的頭文件包含進(jìn)來#include "PID.h"//=============宏定義=====================#define T0 0.0002 //離散化采樣周期,單位s//============全局變量========================//定義PID控制器對應(yīng)的結(jié)構(gòu)體變量PID ASR=PID_DEFAULTS; //速度PI調(diào)節(jié)器ASR//定義PID控制器的參數(shù)及輸出限幅值float SpeedKp=2,SpeedKi=1,SpeedLimit=10; //速度PI調(diào)節(jié)器ASR//===============主程序=======================void main() { //初始化PID控制器 ASR.Kp=SpeedKp; ASR.Ki=SpeedKi; ASR.T=T0; ASR.OutMax=SpeedLimit; ASR.OutMin=-SpeedLimit; }//============中斷服務(wù)程序====================interrupt void T1UFINT_ISR(void) { //轉(zhuǎn)速調(diào)節(jié)ASR ASR.Ref=input1; //速度給定 ASR.Fdb=input2; //速度反饋 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 ASR.calc(&ASR); //函數(shù)調(diào)用:啟動PID計算 output=ASR.Out; //讀取PID控制器的輸出 }//===================================================//End of file.//===================================================