PID控制器的數字實現及C語法講解
為方便學習與交流,根據自己的理解與經驗寫了這份教程,有錯誤之處請各位讀者予以指出,具體包含以下三部分內容:
(1) PID數字化的推導過程(實質:微積分的近似計算);
(2) 程序風格介紹(程序風格來源於TI官方案例);
(3) C有關語法簡述(語法會結合實例進行講解)。
PID控制器是工業過程控制中廣泛采用的一種控制器,其中,P、I、D分別為比例(Proportion)、積分(Integral)、微分(Differential)的簡寫;將偏差的比例、積分和微分通過線性組合構成控制量,用該控制量對受控對象進行控制,稱為PID算法。
為了用軟件實現PID算法,需將PID控制器離散化。
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;
分析:
第一步:整體分析
該語句表示定義了一個數據類型Myint(這裡Myint為數據類型標識符);
(至於其具體表示什麼類型,請看下步分析)
第二步:正常變量聲明(去掉typedef)
int Myint;
其所表示的類型為變量Myint(這裡Myint為變量標識符)的數據類型,即整型類型。
應用:
Myint a; //聲明整型變量a
[例2]
typedef struct { //省略成員 }PID;
分析:
第一步:整體分析
該語句表示定義了一個數據類型PID(這裡PID為類型標識符)
(至於其具體表示什麼類型,請看下步分析)
第二步:正常變量聲明(去掉typedef)
struct { //省略成員 }PID;
其所表示的類型為結構體變量PID(這裡PID為變量標識符)的數據類型,即結構體類型,且其具有的成員同結構體變量PID。
應用:
PID ASR; //定義結構體變量ASR
[例3]
typedef void (*PFun)(int );
分析:
第一步:整體分析
該語句表示定義了一個數據類型PFun(這裡PFun為類型標識符)
(至於其具體表示什麼類型,請看下步分析)
第二步:正常變量聲明(去掉typedef)
void (*PFun)(int );
其所表示的類型為函數指針PFun(這裡PFun為變量標識符)的數據類型,即函數指針類型,且指針所指向的函數類型:形參為整型,無返回值的一類函數。
應用:
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`(函數名代表對應函數的入口地址)使用強制類型轉換(轉換為函數指針類型),也可不用,我都調試驗證過;現把程序截取出來,方便大家理解:
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->a1、p->a2賦值 ****************************************************/ void pid_calc(PID *p) { //使用條件編譯進行功能切換 #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; //為下步計算做准備 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=2; //速度給定 ASR.Fdb=1; //速度反饋 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 pid_calc(&ASR); //PI計算(直接函數調用) } //=================================================== //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->a1、p->a2賦值 ****************************************************/ void pid_calc(PID *p) { //使用條件編譯進行功能切換 #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; //為下步計算做准備 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=2; //速度給定 ASR.Fdb=1; //速度反饋 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 ASR.calc(&ASR); //PI計算_調用PID計算函數 } //=================================================== //End of File //===================================================