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