PID算法完全講解

原貼地址
https://www.arduino.cn/thread-12813-1-1.html

總所周知,PID算法是個很經典的東西。而做自平衡小車,飛行器PID是一個必須翻過的坎。因此本節我們來好好講解一下PID,根據我在學習中的體會,力求通俗易懂。並舉出PID的形象例子來幫助理解PID。

一、首先介紹一下PID名字的由來:
P:Proportion(比例),就是輸入偏差乘以一個常數。
I  :Integral(積分),就是對輸入偏差進行積分運算。
D:Derivative(微分),對輸入偏差進行微分運算。
注:輸入偏差=讀出的被控制對象的值-設定值。比如說我要把溫度控制在26度,但是現在我從溫度傳感器上讀出溫度爲28度。則這個26度就是”設定值“,28度就是“讀出的被控制對象的值”。然後來看一下,這三個元素對PID算法的作用,瞭解一下即可,不懂不用勉強。
P,打個比方,如果現在的輸出是1,目標輸出是100,那麼P的作用是以最快的速度達到100,把P理解爲一個係數即可;而I呢?大家學過高數的,0的積分才能是一個常數,I就是使誤差爲0而起調和作用;D呢?大家都知道微分是求導數,導數代表切線是吧,切線的方向就是最快到至高點的方向。這樣理解,最快獲得最優解,那麼微分就是加快調節過程的作用了。

二、然後要知道PID算法具體分兩種:一種是位置式的 ,一種是增量式的。在小車裏一般用增量式,爲什麼呢?位置式PID的輸出與過去的所有狀態有關,計算時要對e(每一次的控制誤差)進行累加,這個計算量非常大,而明顯沒有必要。而且小車的PID控制器的輸出並不是絕對數值,而是一個△,代表增多少,減多少。換句話說,通過增量PID算法,每次輸出是PWM要增加多少或者減小多少,而不是PWM的實際值。所以明白增量式PID就行了。


三、接着講PID參數的整定,也就是PID公式中,那幾個常數係數Kp,Ti,Td等是怎麼被確定下來然後帶入PID算法中的。如果要運用PID,則PID參數是必須由自己調出來適合自己的項目的。通常四旋翼,自平衡車的參數都是由自己一個調節出來的,這是一個繁瑣的過程。本次我們可以不管,關於PID參數怎麼確定的,網上有很多經驗可以借鑑。比如那個經典的經驗試湊口訣:
                         參數整定找最佳, 從小到大順序查。
                         先是比例後積分, 最後再把微分加。
                         曲線振盪很頻繁, 比例度盤要放大。
                         曲線漂浮繞大彎, 比例度盤往小扳。
                         曲線偏離回覆慢, 積分時間往下降。
                         曲線波動週期長, 積分時間再加長。
                         曲線振盪頻率快, 先把微分降下來。
                         動差大來波動慢, 微分時間應加長。
                         理想曲線兩個波, 前高後低四比一。
                         一看二調多分析, 調節質量不會低。

四、接下來我們用例子來輔助我們把常用的PID模型講解了。(PID控制並不一定要三者都出現,也可以只是PI、PD控制,關鍵決定於控制的對象。)(下面的內容只是介紹一下PID模型,可以不看,對理解PID沒什麼用)
例子:我們要控制一個人,讓他一PID的控制方式來行走110步後停下來。
1)P比例控制,就是讓他按照一定的比例走,然後停下。比如比例係數爲108,則走一次就走了108步,然後就不走了。
說明:P比例控制是一種最簡單的控制方式,控制器的輸出與輸入誤差信號成比例關係。但是僅有比例控制時系統輸出存在穩態誤差。比如上面的只能走到108,無論怎樣都走不到110。
2)PI積分控制,就是按照一定的步伐走到112步然後回頭接着走,走到108步位置時,然後又回頭向110步位置走。在110位置處來回晃盪幾次,最後停在110步的位置。
說明:在積分I控制中,控制器的輸出與輸入誤差信號的積分成正比關係。對一個自動控制系統來說,如果在進入穩態後存在穩態誤差,則稱這個控制系統是有穩態誤差的或簡稱有差系統。爲了消除穩態誤差,在控制器中必須引入“積分項”。積分項對誤差的影響取決於時間的積分,隨着時間的增加,積分項會增大。這樣,即便誤差很小,積分項也會隨着時間的增加而加大,它推動控制器的輸出增大,從而使穩態誤差進一步減小,直到等於0。因此,比例+積分(PI)控制器可以使系統在進入穩態後無穩態誤差。
3)PD微分控制,就是按照一定的步伐走到一百零幾步後,再慢慢地走向110步的位置靠近,如果最後能精確停在110步的位置,就是無靜差控制;如果停在110步附近(如109步或111步位置),就是有靜差控制。
說明:在微分控制D中,控制器的輸出與輸入誤差信號的微分(即誤差的變化率)成正比關係。
      自動控制系統在克服誤差的調節過程中可能會出現振盪甚至失穩,原因是存在較大慣性組件(環節)或滯後組件,具有抑制誤差的作用,其變化總是落後於誤差的變化。解決的辦法是使抑制誤差作用的變化“超前”,即在誤差接近於零時,抑制誤差的作用就應該是零。這就是說,在控制器中僅引入“比例P”項往往是不夠的,比例項的作用僅是放大誤差的幅值,而目前需要增加的是“微分項”,它能預測誤差變化的趨勢。這樣,具有比例+微分的控制器就能夠提前使抑制誤差的控制作用等於零,甚至爲負值,從而避免了被控量的嚴重超調。所以對有較大慣性或滯後的被控對象,比例P+微分D(PD)控制器能改善系統在調節過程中的動態特性。


五、用小明來說明PID:
       小明接到這樣一個任務:有一個水缸有點漏水(而且漏水的速度還不一定固定不變),要求水面高度維持在某個位置,一旦發現水面高度低於要求位置,就要往水缸里加水。 小明接到任務後就一直守在水缸旁邊,時間長就覺得無聊,就跑到房裏看小說了,每30分鐘來檢查一次水面高度。水漏得太快,每次小明來檢查時,水都快漏完了,離要求的高度相差很遠,小明改爲每3分鐘來檢查一次,結果每次來水都沒怎麼漏,不需要加水,來得太頻繁做的是無用功。幾次試驗後,確定每10分鐘來檢查一次。這個檢查時間就稱爲採樣週期。 開始小明用瓢加水,水龍頭離水缸有十幾米的距離,經常要跑好幾趟才加夠水,於是小明又改爲用桶加,一加就是一桶,跑的次數少了,加水的速度也快了,但好幾次將缸給加溢出了,不小心弄溼了幾次鞋,小明又動腦筋,我不用瓢也不用桶,老子用盆,幾次下來,發現剛剛好,不用跑太多次,也不會讓水溢出。這個加水工具的大小就稱爲比例係數。 
       小明又發現水雖然不會加過量溢出了,有時會高過要求位置比較多,還是有打溼鞋的危險。他又想了個辦法,在水缸上裝一個漏斗,每次加水不直接倒進水缸,而是倒進漏斗讓它慢慢加。這樣溢出的問題解決了,但加水的速度又慢了,有時還趕不上漏水的速度。於是他試着變換不同大小口徑的漏斗來控制加水的速度,最後終於找到了滿意的漏斗。漏斗的時間就稱爲積分時間。 
       小明終於喘了一口,但任務的要求突然嚴了,水位控制的及時性要求大大提高,一旦水位過低,必須立即將水加到要求位置,而且不能高出太多,否則不給工錢。小明又爲難了!於是他又開努腦筋,終於讓它想到一個辦法,常放一盆備用水在旁邊,一發現水位低了,不經過漏斗就是一盆水下去,這樣及時性是保證了,但水位有時會高多了。他又在要求水面位置上面一點將水缸要求的水平面處鑿一孔,再接一根管子到下面的備用桶裏這樣多出的水會從上面的孔裏漏出來。這個水漏出的快慢就稱爲微分時間。

六、在代碼中理解PID:(好好看註釋,很好理解的。注意結合下面PID的公式)
首先看PID的增量型公式:
PID=Uk+KP*【E(k)-E(k-1)】+KI*E(k)+KD*【E(k)-2E(k-1)+E(k-2)】
在單片機中運用PID,出於速度和RAM的考慮,一般不用浮點數,這裏以整型變量爲例來講述PID在單片機中的運用。由於是用整型來做的,所以不是很精確。但是對於一般的場合來說,這個精度也夠了,關於係數和溫度在程序中都放大了10倍,所以精度不是很高,但是大部分的場合都夠了,若不夠,可以再放大10倍或者100倍處理,不超出整個數據類型的範圍就可以了。一下程序包括PID計算和輸出兩部分。當偏差>10度時全速加熱,偏差在10度以內時爲PID計算輸出。
程序說明:下面的程序,先看main函數。可知在對定時器0初始化後就一直在執行PID_Output()函數。在PID_Output()函數中先用iTemp變量來得到PID運算的結果,來決定是啓動加熱絲加熱還是不啓動加熱絲。下面的if語句結合定時器來決定PID算法多久執行一次。PID_Operation()函數看似很複雜,其實就一直在做一件事:根據提供的數據,用PID公式把最終的PID值算出來。

[C] 純文本查看 複製代碼
代碼
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <reg52.h>
typedef unsigned char      uChar8;     
typedef unsigned int       uInt16;
typedef unsigned long int  uInt32;
 
sbit ConOut = P1^1;     //加熱絲接到P1.1口
 
typedef struct PID_Value
{
    uInt32 liEkVal[3];          //差值保存,給定和反饋的差值
    uChar8 uEkFlag[3];          //符號,1則對應的爲負數,0爲對應的爲正數   
    uChar8 uKP_Coe;             //比例係數
    uChar8 uKI_Coe;             //積分常數
    uChar8 uKD_Coe;             //微分常數
    uInt16 iPriVal;             //上一時刻值
    uInt16 iSetVal;             //設定值
    uInt16 iCurVal;             //實際值
}PID_ValueStr;
 
PID_ValueStr PID;               //定義一個結構體,這個結構體用來存算法中要用到的各種數據
bit g_bPIDRunFlag = 0;          //PID運行標誌位,PID算法不是一直在運算。而是每隔一定時間,算一次。
/* ********************************************************
/* 函數名稱:PID_Operation()                                 
/* 函數功能:PID運算                   
/* 入口參數:無(隱形輸入,係數、設定值等)                     
/* 出口參數:無(隱形輸出,U(k))
/* 函數說明:U(k)+KP*[E(k)-E(k-1)]+KI*E(k)+KD*[E(k)-2E(k-1)+E(k-2)]                                     
******************************************************** */
void PID_Operation(void)
{
    uInt32 Temp[3] = {0};   //中間臨時變量
    uInt32 PostSum = 0;     //正數和
    uInt32 NegSum = 0;      //負數和
    if(PID.iSetVal > PID.iCurVal)                //設定值大於實際值否?
    {
        if(PID.iSetVal - PID.iCurVal > 10)      //偏差大於10否?
            PID.iPriVal = 100;                  //偏差大於10爲上限幅值輸出(全速加熱)
        else                                    //否則慢慢來
        {
            Temp[0] = PID.iSetVal - PID.iCurVal;    //偏差<=10,計算E(k)
            PID.uEkFlag[1] = 0;                     //E(k)爲正數,因爲設定值大於實際值
            /* 數值進行移位,注意順序,否則會覆蓋掉前面的數值 */
            PID.liEkVal[2] = PID.liEkVal[1];
            PID.liEkVal[1] = PID.liEkVal[0];
            PID.liEkVal[0] = Temp[0];
            /* =================================================================== */
            if(PID.liEkVal[0] > PID.liEkVal[1])              //E(k)>E(k-1)否?
            {
                Temp[0] = PID.liEkVal[0] - PID.liEkVal[1];  //E(k)>E(k-1)
                PID.uEkFlag[0] = 0;                         //E(k)-E(k-1)爲正數
            }                                      
            else
            {
                Temp[0] = PID.liEkVal[1] - PID.liEkVal[0];  //E(k)<E(k-1)
                PID.uEkFlag[0] = 1;                         //E(k)-E(k-1)爲負數
            }                       
            /* =================================================================== */
            Temp[2] = PID.liEkVal[1] * 2;                   //2E(k-1)
            if((PID.liEkVal[0] + PID.liEkVal[2]) > Temp[2]) //E(k-2)+E(k)>2E(k-1)否?
            {
                Temp[2] = (PID.liEkVal[0] + PID.liEkVal[2]) - Temp[2];
                PID.uEkFlag[2]=0;                           //E(k-2)+E(k)-2E(k-1)爲正數
            }                                              
            else                                            //E(k-2)+E(k)<2E(k-1)
            {
                Temp[2] = Temp[2] - (PID.liEkVal[0] + PID.liEkVal[2]);
                PID.uEkFlag[2] = 1;                         //E(k-2)+E(k)-2E(k-1)爲負數
            }                                  
            /* =================================================================== */
            Temp[0] = (uInt32)PID.uKP_Coe * Temp[0];        //KP*[E(k)-E(k-1)]
            Temp[1] = (uInt32)PID.uKI_Coe * PID.liEkVal[0]; //KI*E(k)
            Temp[2] = (uInt32)PID.uKD_Coe * Temp[2];        //KD*[E(k-2)+E(k)-2E(k-1)]
            /* 以下部分代碼是講所有的正數項疊加,負數項疊加 */
            /* ========= 計算KP*[E(k)-E(k-1)]的值 ========= */
            if(PID.uEkFlag[0] == 0)
                PostSum += Temp[0];                         //正數和
            else                                            
                NegSum += Temp[0];                          //負數和
            /* ========= 計算KI*E(k)的值 ========= */
            if(PID.uEkFlag[1] == 0)    
                PostSum += Temp[1];                         //正數和
            else
                ;   /* 空操作。就是因爲PID.iSetVal > PID.iCurVal(即E(K)>0)才進入if的,
                    那麼就沒可能爲負,所以打個轉回去就是了 */
            /* ========= 計算KD*[E(k-2)+E(k)-2E(k-1)]的值 ========= */
            if(PID.uEkFlag[2]==0)
                PostSum += Temp[2];             //正數和
            else
                NegSum += Temp[2];              //負數和
            /* ========= 計算U(k) ========= */                        
            PostSum += (uInt32)PID.iPriVal;        
            if(PostSum > NegSum)                 //是否控制量爲正數
            {
                Temp[0] = PostSum - NegSum;
                if(Temp[0] < 100 )               //小於上限幅值則爲計算值輸出
                    PID.iPriVal = (uInt16)Temp[0];
                else PID.iPriVal = 100;         //否則爲上限幅值輸出
            }
            else                                //控制量輸出爲負數,則輸出0(下限幅值輸出)
                PID.iPriVal = 0;
        }
    }
    else PID.iPriVal = 0;                       //同上,嘿嘿
}
/* ********************************************************
/* 函數名稱:PID_Output()                                    
/* 函數功能:PID輸出控制                 
/* 入口參數:無(隱形輸入,U(k))                        
/* 出口參數:無(控制端)                                     
******************************************************** */
void PID_Output(void)
{
    static uInt16 iTemp;
    static uChar8 uCounter;
    iTemp = PID.iPriVal;
    if(iTemp == 0)
        ConOut = 1;     //不加熱
    else ConOut = 0;    //加熱
    if(g_bPIDRunFlag)   //定時中斷爲100ms(0.1S),加熱週期10S(100份*0.1S)
    {
        g_bPIDRunFlag = 0;
        if(iTemp) iTemp--;      //只有iTemp>0,纔有必要減“1”
        uCounter++;
        if(100 == uCounter)
        {
            PID_Operation();    //每過0.1*100S調用一次PID運算。
            uCounter = 0;  
        }
    }
}
/* ********************************************************
/* 函數名稱:PID_Output()                                    
/* 函數功能:PID輸出控制                 
/* 入口參數:無(隱形輸入,U(k))                        
/* 出口參數:無(控制端)                                     
******************************************************** */
void Timer0Init(void)
{
    TMOD |= 0x01;   // 設置定時器0工作在模式1下
    TH0 = 0xDC;
    TL0 = 0x00;     // 賦初始值
    TR0 = 1;        // 開定時器0
    EA = 1;         // 開總中斷
    ET0 = 1;        // 開定時器中斷
}
 
void main(void)
{
    Timer0Init();
    while(1)
    {
        PID_Output();
    }
}
 
void Timer0_ISR(void) interrupt 1
{
    static uInt16 uiCounter = 0;
    TH0 = 0xDC;
    TL0 = 0x00;
    uiCounter++;
    if(100 == uiCounter)
    {
        g_bPIDRunFlag = 1;
    }
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章