根據設備有所不一樣,比例帶通常爲2~10%(溫度控制)。
可是,僅僅是P控制的話,會產生下面將提到的offset (穩態偏差),因此通常加上積分控制(I),以消除穩態偏差。
小明接到這樣一個任務:php
有一個水缸點漏水(並且漏水的速度還不必定固定不變),要求水面高度維持在某個位置,一旦發現水面高度低於要求位置,就要往水缸里加水。
小明接到任務後就一直守在水缸旁邊,時間長就以爲無聊,就跑到房裏看小說了,每30分鐘來檢查一次水面高度。水漏得太快,
每次小明來檢查時,水都快漏完了,離要求的高度相差很遠,小明改成每3分鐘來檢查一次,結果每次來水都沒怎麼漏,不須要加水,來得太頻繁作的是無用功。
幾回試驗後,肯定每10分鐘來檢查一次。這個檢查時間就稱爲採樣週期。
開始小明用瓢加水,水龍頭離水缸有十幾米的距離,常常要跑好幾趟才加夠水,因而小明又改成用桶加,一加就是一桶,跑的次數少了,加水的速度也快了,
但好幾回將缸給加溢出了,不當心弄溼了幾回鞋,小明又動腦筋,我不用瓢也不用桶,老子用盆,幾回下來,
發現剛恰好,不用跑太屢次,也不會讓水溢出。這個加水工具的大小就稱爲比例係數。
小明又發現水雖然不會加過量溢出了,有時會高過要求位置比較多,仍是有打溼鞋的危險。他又想了個辦法,在水缸上裝一個漏斗,
每次加水不直接倒進水缸,而是倒進漏斗讓它慢慢加。這樣溢出的問題解決了,但加水的速度又慢了,有時還趕不上漏水的速度。
因而他試着變換不一樣大小口徑的漏斗來控制加水的速度,最後終於找到了滿意的漏斗。漏斗的時間就稱爲積分時間 。
小明終於喘了一口,但任務的要求忽然嚴了,水位控制的及時性要求大大提升,一旦水位太低,必須當即將水加到要求位置,並且不能高出太多,不然不給工錢。
小明又爲難了!因而他又開努腦筋,終於讓它想到一個辦法,常放一盆備用水在旁邊,一發現水位低了,不通過漏斗就是一盆水下去,這樣及時性是保證了,但水位有時會高多了。
他又在要求水面位置上面一點將水鑿一孔,再接一根管子到下面的備用桶裏這樣多出的水會從上面的孔裏漏出來。這個水漏出的快慢就稱爲微分時間。html
拿一個水池水位來講,咱們 能夠制定一個規則,
把水位分爲超高、高、較高、中、較低、低、超低幾個區段;
再把水位波動的趨勢分爲甚快、快、較快、慢、停幾個區段,並區分趨勢的正負;
把輸出分爲超大幅 度、大幅度、較大幅度、微小几個區段。node
當水位處於中值、趨勢處於停頓的時候,不調節;
當水位處於中值、趨勢緩慢變化的時候,也能夠暫不調節;
當水位處於較高、趨勢緩慢變化 的時候,輸出一個微小調節兩就夠了;
當水位處於中值、趨勢較快變化的時候,輸出進行叫 大幅度調節……。 react
如上所述,咱們須要制定一個控制規則表,而後制定參數判斷水位區段的界值、波動趨 勢的界值、輸出幅度的界值。git
所謂積分控制(I),就是在出現穩態偏差時自動的改變輸出量,使其與手動復位動做的輸出量相同,達到消除穩態偏差的目的。
當系統存在偏差時,進行積分控制,根據積分時間的大小調節器的輸出會以必定的速度變化,只要偏差還存在,就會不斷的進行輸出。github
積分時間的定義:
當積分項和比例項對於控制器的輸出的貢獻相同,即積分做用重複了一次比例做用時所花費的時間,就是積分時間。算法
微分控制(D)的功能是經過偏差的變化率預報偏差信號的將來變化趨勢。
經過提供超前控制做用,微分控制能使被控過程趨於穩定。
所以,它常常用來抵消積分控制產生的不穩定趨勢。shell
微分時間的定義:
當輸入量持續的以必定速率變化時,微分項和比例項對於控制器的輸出的貢獻相同,
即微分做用重複了一次比例做用時所花費的時間,就是微分時間。api
PID stands for Proportional, Integral, Derivative.app
PID control provides a continuous variation of output within a control loop feedback mechanism
to accurately control the process, removing oscillation and increasing efficiency.
Where proportional control is used to minimize the oscillation characteristic of on/off control,
PID control goes that bit further to reduce errors and provide accuracy and stability in a process.
It does this by using the integral action and derivative actions to eliminate control deviation errors
and to manage rapid process movements.
All three PID terms need to tuned appropriately for the application requirements to achieve the best control.
For a high level of control, digital PID controllers are often used.
These typically come in the form of PID Temperature Controllers or
PID Process Controllers and can be single, dual or multi-loop instruments.
PID control is used for a variety of process variables such as; Temperature, Flow and Pressure.
Typically, challenging applications such as industrial heat treatment processes,
ovens and furnaces use PID controllers as well as in the scientific and
lab sector where precision and reliability is essential to the quality of a control application.
For a detailed insight into how PID Controllers work please refer to the PID Controller Wiki page.
To achieve the best levels of control it is necessary to tune PID controllers, this can be done in a number of ways.
Controllers will enable manual PID tuning meaning the P, I and D variables must
be manually calculated and set using the controller menu.
This requires a reasonable level of knowledge and understanding from the user to be able to carry out the calculation.
Often this may still require some trial and error testing.
Manual tuning can be extremely time consuming compared to the alternatives.
These days, most products will support auto-tuning (also known as self-tuning) of PID settings.
Typically the way this works will depend on which manufacturer’s product you are using,
but commonly they use a rule based calculation in the same way that an experienced engineer would.
Auto-tuning can either take place at the set-point or with some controllers it occurs as the load is being heated up from the ambient temperature.
More recently, controllers have introduced a number of options for auto-tuning PID settings.
These allow the settings to be more closely aligned to a specific application’s requirements
for example prioritising the minimisation of overshoot over the time it takes to reach the set-point.
連續一時間PID控制系統如圖所示。
D(s)爲控制器。在PID控制系統中,D(s)完成PID控制規律,稱爲PID控制器。
PID控制器是一種線性控制器,用輸出量y(t)和給定量r(t)之間的偏差的時間函數e(t)=r(t)-y(t)。
圖中的比例,積分,微分的線性組合,構成控制量u(t)稱爲
比例(Proportional),積分(Integrating),微分(Differentiation)控制,簡稱PID控制。
增量算法
當執行機構須要的不是控制量的絕對值,而是控制量的增量(例如去驅動步進電動機)時,須要用PID的「增量算法」。
比例,積分,微分
咱們先來了解下正式的定義,P,I,D分別表明控制算法中的三個組成部分:比例,積分,微分。
比例
閥門驅動的準則之一是比例誤差,指的是系統讀數與設定值間的差別,該誤差乘以P值後的數值被賦予求和地址位。
因而可知,若是實際讀數與設定值之間的差別很大,控制器就會快速驅動閥門工做以試圖到達設定值,能夠把它看做踩油門。
積分
由微積分曲線圖可知,積分是某兩點區間內曲線下方包圍的面積,一般即起始時間點與結束時間點。
說得更實際點,就是從零點開始的讀數總和或偏差總和。
調節P值和D值僅着力於當下或前一刻的測量狀態,I值則利用更多較早的數據將當前系統讀數修正至設定值。
艾裏卡特大多數將質量流量控制器、壓力控制器的I值設爲零,P值與D值被計入以每秒1000次的速度更新的求和地址位,
免除用戶輸入I值的必要。求和地址位的數值即用來按比例地控制閥門開度。
微分
根據微積分函數表達式y=T(dx/dt),微分是變量(x)對時間(t)的求導。
就質量流量控制器而言,流量值就是這個變量(x)。
PID控制算法會將求和地址位數值,減去dx/dt乘以D值後的值,做爲阻尼項,於是能夠將D值理解爲剎車。
PID控制器是一種線性調節器,這種調節器是將系統的給定r與實際輸出值y構成的控制誤差
e =y- r的比例、積分、微分,經過線性組合構成控制量輸入給控制對象。
PID控制器各校訂環節的做用
比例環節:快速減少誤差,加快響應速度,可是過大會使系統超調量增大,影響系統穩定性。
積分環節:主要用於減少穩態偏差,但會使系統響應速度減慢。
微分環節:在系統中誤差信號變得太大以前引入一個修正信號,從而加快系統的動做速度,減少了調節時間,減小了系統的超調量。
PID控制器(比例-積分-微分控制器),由比例單元P、積分單元I和微分單元D組成。
經過Kp,Ki和Kd三個參數的設定。PID控制器主要適用於基本上線性,且動態特性不隨時間變化的系統。
PID控制器是一個在工業控制應用中常見的反饋迴路部件。
這個控制器把收集到的數據和一個參考值進行比較,而後把這個差異用於計算新的輸入值,
這個新的輸入值的目的是可讓系統的數據達到或者保持在參考值。
PID控制器能夠根據歷史數據和差異的出現率來調整輸入值,使系統更加準確而穩定。
PID控制器的比例單元P、積分單元I和微分單元D分別對應目前偏差、過去累計偏差及將來偏差。
如果不知道受控系統的特性,通常認爲PID控制器是最適用的控制器。
藉由調整PID控制器的三個參數,能夠調整控制系統,設法知足設計需求。
控制器的響應能夠用控制器對偏差的反應快慢、控制器過沖的程度及系統震盪的程度來表示。
不過使用PID控制器不必定保證可達到系統的最佳控制,也不保證系統穩定性。
有些應用只須要PID控制器的部分單元,能夠將不須要單元的參數設爲零便可。
所以PID控制器能夠變成PI控制器、PD控制器、P控制器或I控制器。
其中又以PI控制器比較經常使用,由於D控制器對回授噪聲十分敏感,
而若沒有I控制器的話,系統不會回到參考值,會存在一個偏差量。
PID是以它的三種糾正算法而命名。
受控變數是三種算法(比例、積分、微分)相加後的結果,即爲其輸出,
其輸入爲偏差值(設定值減去測量值後的結果)或是由偏差值衍生的信號。
若定義 u(t) 爲控制輸出,PID算法能夠用下式表示:
比例控制考慮當前偏差,偏差值和一個正值的常數Kp(表示比例)相乘。
Kp只是在控制器的輸出和系統的偏差成比例的時候成立。
好比說,一個電熱器的控制器的比例尺範圍是10°C,它的預約值是20°C。
那麼它在10°C的時候會輸出100%,在15°C的時候會輸出50%,在19°C的時候輸出10%,
注意在偏差是0的時候,控制器的輸出也是0。
比例控制的輸出以下:
若比例增益大,在相同偏差量下,會有較大的輸出,
但若比例增益太大,會使系統不穩定。相反的,
若比例增益小,若在相同偏差量下,其輸出較小,所以控制器會較不敏感的。
若比例增益過小,當有干擾出現時,其控制信號可能不夠大,沒法修正干擾的影響。
The proportional term changes the output according to the error.
A simple proportional controller has only one control parameter, the Kp.
By changing this parameter, the controller becomes more or less reactive in changes.
The higher the Kp parameter, the faster the system will try to reach the set-point,
but also high proportional gain turns the system unstable
and it oscillates around the set-point.
On the other hand, if the Kp is low, then the the system will stabilize with a constant error bellow (or above) the set-point,
because the output will not provide sufficient power for the system to reach the final position.
Here is a graph to better understand this parameter:
The red line shows the response of the system with a high Kp.
The system will try to reach the set-point faster, but the overshoot is high as well as the error.
This drives the system into an unstable state in which it oscillates around the set-point.
The blue line shows the response with a low Kp.
The system will reach the set-point slower and it will cause a much smaller overshoot.
But after a few oscillations, the controller output does not provide enough power to change the system,
thus it will remain stable a little bellow the set-point.
比例控制在偏差爲0時,其輸出也會爲0。
若要讓受控輸出爲非零的數值,就須要有一個穩態偏差或偏移量。
穩態偏差和比例增益成正比,和受控系統自己的增益成反比。
若加入一偏置,或是加入積分控制,能夠消除穩態偏差。
不一樣比例增益Kp下,受控變數對時間的變化(Ki和Kd維持定值)
積分控制考慮過去偏差,將偏差值過去一段時間和(偏差和)乘以一個正值的常數Ki。
Ki從過去的平均偏差值來找到系統的輸出結果和預約值的平均偏差。
一個簡單的比例系統會震盪,會在預約值的附近來回變化,由於系統沒法消除多餘的糾正。
經過加上負的平均偏差值,平均系統偏差值就會漸漸減小。
因此,最終這個PID迴路系統會在設定值穩定下來。積分控制的輸出以下:
積分控制會加速系統趨近設定值的過程,而且消除純比例控制器會出現的穩態偏差。
積分增益越大,趨近設定值的速度越快,不過過由於積分控制會累計過去全部的偏差,可能會使回授值出現過沖的情形。
A proportional system is usually not sufficient to eliminate the error.
The system must not only change its output according to the current error,
but it must also be able to watch and change the output according to the past errors.
The integral is the sum of the errors over time.
What this means is that, if the error is big, then the integral builds up as time passes
and the output changes rapidly to eliminate the error.
Now suppose that we have the same example as before, a proportional system that has a low Kp.
First of all, the system will respond much faster at the beginning, because the error will build up a big integral.
Then, when the system is stabilized little bellow the set-point, the integral will take over.
This small error is added over time to increase the integral, and finally it will change the system's output.
Finally, the system will stabilize onto the set-point.
Now, suppose that we have a proportional system with low Kp and an integral term.
Such a system is named PI, and here is how the system reacts as Ki changes:
A high integral gain (Ki) (red line) causes a faster system response, but also causes overshoot.
Unlike the pure proportional system, despite the overshoot, the system is not driven into an unstable state.
After a few oscillations the system will stabilize on the set-point.
As the integral gain gets smaller (blue line), the system responses slower,
but it stabilizes much faster with less oscillations.
The optimal integral gain (purple line) causes the system to respond very slow
but it stabilizes on the set-point with almost no oscillations.
微分控制考慮未來偏差,計算偏差的一階導,並和一個正值的常數Kd相乘。
這個導數的控制會對系統的改變做出反應。
導數的結果越大,那麼控制系統就對輸出結果做出更快速的反應。
這個Kd參數也是PID被稱爲可預測的控制器的緣由。
Kd參數對減小控制器短時間的改變頗有幫助。
一些實際中的速度緩慢的系統能夠不須要Kd參數。
微分控制的輸出以下:
微分控制能夠提高整定時間及系統穩定性。
不過由於純微分器不是因果系統,所以在PID系統實現時,通常會爲微分控制加上一個低通濾波器以限制高頻增益及噪聲。
實務上較少用到微分控制,估計PID控制器中只有約20%有用到微分控制。
The third and last term is the Derivative.
This term calculates the rate of change of the error and adds to the output accordingly.
If the error changes slow, then the derivative is increased in order to make the PID system respond faster.
On the other hand, if the error is changes rapidly, the derivative is decreased
in order to make the system more stable and avoid oscillations.
The process of setting the P, I and D parameters to obtain an ideal response of the system is called tuning.
Over the years, several tuning methods have been described,
but we will discuss only the Ziegler-Nichols method and of course the manual tuning.
The tuning time is relative to the system type and speed of change.
If a system has a rapid response, then the tuning time is very short.
On the other hand, a slow response system has a very long tuning time.
A fast-response system is for example a robot positioning system.
An example of a slow response system could be a large water tank heater.
It is obvious that a parameter change in the robot positioning system will have an immediate response at the robot's behavior,
while a parameter change in the water tank heater system may need several minutes for the systems to react.
Manual PID Tuning
If someone wants to tune a PID system with this method, then he first must be sure
that he understands what each parameter does and how each one affects the system,
otherwise this can be a headache. These are the steps for this method:
Tuning a PID system with the Ziegler-Nichols method
The process of setting the optimal gains for P, I and D to get an ideal response from a control system is called tuning.
There are different methods of tuning of which the 「guess and check」 method and the Ziegler Nichols method will be discussed.
The gains of a PID controller can be obtained by trial and error method.
Once an engineer understands the significance of each gain parameter, this method becomes relatively easy.
In this method, the I and D terms are set to zero first and the proportional gain is increased until the output of the loop oscillates.
As one increases the proportional gain, the system becomes faster, but care must be taken not make the system unstable.
Once P has been set to obtain a desired fast response, the integral term is increased to stop the oscillations.
The integral term reduces the steady state error, but increases overshoot.
Some amount of overshoot is always necessary for a fast system so that it could respond to changes immediately.
The integral term is tweaked to achieve a minimal steady state error.
Once the P and I have been set to get the desired fast control system with minimal steady state error,
the derivative term is increased until the loop is acceptably quick to its set point.
Increasing derivative term decreases overshoot and yields higher gain with stability
but would cause the system to be highly sensitive to noise.
Often times, engineers need to tradeoff one characteristic of a control system for another to better meet their requirements.
The Ziegler-Nichols method is another popular method of tuning a PID controller.
It is very similar to the trial and error method wherein I and D are set to zero and P is increased until the loop starts to oscillate.
Once oscillation starts, the critical gain Kc and the period of oscillations Pc are noted.
The P, I and D are then adjusted as per the tabular column shown below.
The Ziegler-Nichols method was introduced by John G. Ziegler and Nathaniel B. Nichols in the 1940s.
To tune a system with the Ziegler-Nichols method,
the engineer must first define the critical P gain, which we will name Kc.
This is how its done:
Once the Kc is defined, then the engineer must measure the oscillation period.
It is the same like measuring the period of an AC signal.
This will be the Pc.
Finally, the user can calculate the values for P,I and D parameters using the following table:
If you're wondering what the Ti and Td terms are, these are common parameters in a PID controller.
A typical PID controller has three parameters, K, Ti and Td, but there are many different kinds of PID controllers.
For example, a PID controller with set-point weighting and derivative filter has six parameters K, Ti, Td, Tf , b and c.
Parameters b and c are called set-point weights.
They have no influence on the response to disturbances but they have a significant influence on the response to set-point changes,
so you do not need to worry about them when tuning a PID system.
Example
Here is an example of a system that we want to tune with the Ziegler-Nichols method.
First, we need to find the critical P gain.
So, we make the I and D parameters zero and we let the controller operate as a simple proportional controller (only P).
Then, we start to increase the parameter P and write down the system's response.
Here are 4 typical characteristics of a system's response in relation to the P change:
Suppose that the setting for P is 20. So, Kc is 20.
Now we can take a closer look at the last chart:
The horizontal axis shows the time in seconds.
As you can see, the oscillation has a period of about 15 seconds (45 to 60 seconds).
So, the second parameter that we need, the Pc, is 15' seconds.
To get the PID parameters for our controller, we need to use these values with the Ziegler-Nichols table:
P = 0.60 x Kc => P = 0.6 x 20 => P = 12
Ti = 0.5 x Pc => Ti = 0.5 x 15 => Ti = 7.5
Td = Pc / 8 => Td = 15 / 8 => Td = 1.87
After 2 pages of theory and mathematical hocus-pocus,
there will be people that still are not able to write software for a PID controller.
First is that i found the best documentation for making a PID controller with a PIC microcontroller.
It is the Application Note 937 from Microchip (who else):
Second good news: A PID controller example explained in simple words
I understand that not all people feel comfortable with mathematics,
but many would like to make a PID controller for number of reasons.
The integral term is by far the hardest to decipher.
Instead of just explaining the integral term, i will explain a complete (and simple) PID controller with an example.
I bet that after this, everyone will be able to make one (as long as he know how to program a microcontroller of course).
So, it goes like this: We want to control the temperature of a water heater.
The heater resistor is controlled by a TRIAC, and the microcontroller controls the gate of that TRIAC (it could operate as a dimmer for example).
A temperature sensor is placed in the water and gives feedback to the microcontroller in degrees Celsius (oC).
The system has these parameters which you must define as RAM variables,
and also it is good to save them in an EEPROM memory because you do not want to define them every time the system restarts:
You will also need to define these additional RAM variables:
This is the simplest PID controller.
The output of this system is described by this function (do not be scared if you know little about mathematics)
The microcontroller will run an infinite loop, during which it will calculate the PID terms and control the output.
I call this routine "MainLoop". During start-up, the microcontroller will run the "Initialization" routine.
Here are the flow chart of the Initialization and the MainLoop routines:
Initialization
MainLoop
Finally, there are 3 more subroutines,
Calculate_P_Term, Calculate_I_Term andCalculate_D_Term.
Here are the flow charts for those:
First of all, i want you to agree that, behind the mask of the hard math,
there is only a simple program with additions and subtractions (ok, and some multiplications and divisions maybe).
I assume that you do know how to read a flowchart and i will not go into details.
Regarding the symbols that i use:
An ellipse indicates the start and the name of the (sub)routine that follows,
a rectangle has the parts of a routine that have to be translated directly into code,
a parallelogram defines a call to another sub-routine and
a diamond defines an IF case, with a YES or NO branch.
Looking into the MainLoop, you can see that the system reads the temperature and calculates the error in every loop.
The error is calculated by subtracting the PV from the SP (Err=SP-PV).
If for example we want the water to have 80oC temperature (SP) and the current temperature is 25oC (PV), the error (Err) is 80-25=55oC.
Using this error, the program calculates the P_Term (Calculate_P_Term subroutine),
simply by multiplying the error by the P_Gain (P_Term=Err*P_Gain).
Until now, this is the typical operation of a simple P-controller.
Now to add the I term.
Looking again into the MainLoop, we see that the Calculate_I_Term subroutine is NOT called in every loop.
Instead, a counter is used (Ti_Counter). The initial value of this counter is taken from the I_Time system parameter (Ti).
The counter is decreased on every loop.
When the counter becomes zero, the Calculate_I_Term subroutine is called.
So, this is where the Ti parameter is used.
Unlike the P_Term, the I_Term is calculated only once every Ti loop cycles.
To calculate the I_Term the system adds the current error (Err) to a variable named Acc_Err.
This variable accumulates all past errors and therefore is called Accumulated Error.
The I_Term is then calculated by multiplying this variable by the I_Gain (I_Term=Acc_Err*I_Gain).
Now you understand the purpose of the Ti parameter.
If the system did not have this parameter, it would accumulate the current errors constantly,
and therefore the I_Term would become huge and it would overshadow the P_Term and D_Term.
Finally, the D_Term.
Similar to the I_Term, the D_Term has a different countdown timer (Td_Counter),
so the Calculate_D_Term subroutine is called once every D_Time (Td) cycles.
To calculate the D_Term, the system must know the previous error.
The previous error is saved in the D_Err variable.
So, the system can calculate the error difference by subtracting the previous error from the current error (Err - D_Err).
Finally, the D_Term is calculated by multiplying the error difference by the D_Gain (D_Term = (Err - D_Err) * D_Gain).
The output of the system is calculated in every loop, and it is the sum of the 3 terms:
P_Out = P_Term + I_Term + D_Term
There are some problems that may come out from the following example.
First of all, you have to decide if you will use 8-bit registers of longer for the variables.
This is something that you need to decide before you start designing the system.
You have to take into account the system's range and accuracy.
For example, a temperature controller that is used for a system which measures temperatures from 0 to 100 degrees
with 1oC accuracy can be implemented with 8-bit registers, But if the system measures from 0 to 600oC then the 8-bits are just not enough.
Moreover, extreme caution must be taken with the P_Out and Acc_Err registers.
Both of them are subject to overflow when big numbers are added.
If for example an 8-bit registered is selected for the Acc_Err variable,
but an error of 100 degrees is added 3 times, then the register will overflow and the result will be wrong.
Same can happen to the P_out register if the result of the P and D term addition is larger than the register can handle.
Therefore, make sure that you put some limits to these registers
and check them every time to see if they exceed these limits.
There are several elements within a feedback system; for discussion purposes,
we will use a home heating temperature control system as our model in the descriptions below.
Disturbances – These are unwanted inputs to the Plant, which can be common.
A disturbance would be an open entry door allowing a gust of cold air to blow in, quickly dropping the temperature and causing the heat to come on.
Controller – Intentionally left for last, this is the most significant element of a control system.
The Controller is responsible for several tasks and is the link that connects together all of the physical and nonphysical elements.
It measures the output signal of the Plant’s Sensors, processes the signal and then derives an error based on the signal measurement and the Setpoint.
Once the sensor data has been collected and processed, the result must be used to find PID values, which then must be sent out to the Plant for error correction.
The rate at which all of this happens is dependent upon the Controller’s processing power.
This may or may not be an issue depending on the response characteristic of the Plant.
A temperature control system is much more forgiving on a Controller’s processing capabilities than a motor control system.
Figure 1 shows a basic block diagram of a feedback control system.
PID means Proportional, Integral and Derivative.
A PID control loop is designed to eliminate the need for continued monitoring of an operation by the operators.
Example:
A very simple example that illustrates the basic functionality of a PID is when a person enters a shower.
The person initially opens the hot water tap to increase the temperature to an acceptable value (also called "Setpoint").
The problem is that there may come a time when the water temperature exceeds this value
so the person has to crack the cold water tap to counter the heat and keep the balance.
Cold water is adjusted to reach the desired temperature.
In this case, the human is the one who is exercising control over the control loop,
and is the decision maker to open or close one of the keys.
Wouldn't it be great if instead of us there was a machine that made decisions and kept the water at our ideal temperature?
This is the reason why PID loops were invented, to simplify the work of operators and have better control over operations.
Some of the most common applications are:
Proportional Response
The proportional component depends only on the difference between the set point and the process variable.
This difference is referred to as the Error term. The proportional gain (Kc) determines the ratio of output response to the error signal.
For instance, if the error term has a magnitude of 10, a proportional gain of 5 would produce a proportional response of 50.
In general, increasing the proportional gain will increase the speed of the control system response.
However, if the proportional gain is too large, the process variable will begin to oscillate.
If Kc is increased further, the oscillations will become larger and the system will become unstable and may even oscillate out of control.
Integral Response
The integral component sums the error term over time.
The result is that even a small error term will cause the integral component to increase slowly.
The integral response will continually increase over time unless the error is zero,
so the effect is to drive the Steady-State error to zero. Steady-State error is the final difference between the process variable and set point.
A phenomenon called integral windup results when integral action saturates a controller without the controller driving the error signal toward zero.
Derivative Response
The derivative component causes the output to decrease if the process variable is increasing rapidly.
The derivative response is proportional to the rate of change of the process variable.
Increasing the derivative time (Td) parameter will cause the control system to react more strongly to changes in the error term
and will increase the speed of the overall control system response.
Most practical control systems use very small derivative time (Td),
because the Derivative Response is highly sensitive to noise in the process variable signal.
If the sensor feedback signal is noisy or if the control loop rate is too slow,
the derivative response can make the control system unstable
Programmable controllers or PLCs execute this control automatically to keep the "Process Variable" PV
(eg, temperature measurement or a tank level) close to the setpoint. SP
Setpoint is where the action should be.
The "Error" is defined as the difference between the setpoint and process variable.
The controller tries to minimize this error by adjusting the control inputs.
Error = (Setpoint) - (Process Variable)
The variable that is set is known as "control variable" which is usually the controller output.
The PID controller output changes in response to changes introduced in the measurement of the process variable.
The algorithm involved in the PID loop consists of three main parameters:
Proportional, Integral and Derivative.
I've been reading up on PID controllers and they actually make sense now.
It is a nice Saturday in St. Louis and I'm inside researching PID controllers.
Yes, I'm a nerd. A very large nerd. A nerd who's toy isn't playing nicely with him.
So here is the break down of a PID controller as I see it along with how I'll be implementing it.
P = Short term corrections
I = Adds long-term precision
D = This gives you a rough estimate of the velocity (delta position/sample time),
which predicts where the position will be in a while.
The D directly relates to my Gyro. It measures change in degrees per second.
typedef struct { double dState; // Last position input double iState; // Integrator state double iMax, iMin; // Maximum and minimum allowable integrator state double iGain, // integral gain pGain, // proportional gain dGain; // derivative gain } SPid; double UpdatePID(SPid * pid, double error, double position) { double pTerm, dTerm, iTerm; pTerm = pid->pGain * error; // calculate the proportional term // calculate the integral state with appropriate limiting pid->iState += error; if (pid->iState > pid->iMax) pid->iState = pid->iMax; else if (pid->iState < pid->iMin) pid->iState = pid->iMin; iTerm = pid->iGain * iState; // calculate the integral term dTerm = pid->dGain * (position - pid->dState); pid->dState = position; return pTerm + iTerm - dTerm; }
In conjunction with the release of the new Arduino PID Library
I’ve decided to release this series of posts.
The last library, while solid, didn’t really come with any code explanation.
This time around the plan is to explain in great detail why the code is the way it is.
I’m hoping this will be of use to two groups of people:
It’s going to be a tough slog, but I think I found a not-too-painful way to explain my code.
I’m going to start with what I call 「The Beginner’s PID.」
I’ll then improve it step-by-step until we’re left with an efficient, robust pid algorithm.
Here’s the PID equation as everyone first learns it:
This leads pretty much everyone to write the following PID controller:
/*working variables*/ unsigned long lastTime; double Input, Output, Setpoint; double errSum, lastErr; double kp, ki, kd; void Compute() { /*How long since we last calculated*/ unsigned long now = millis(); double timeChange = (double)(now - lastTime); /*Compute all the working error variables*/ double error = Setpoint - Input; errSum += (error * timeChange);
double dErr = (error - lastErr) / timeChange; /*Compute PID Output*/ Output = kp * error + ki * errSum + kd * dErr; /*Remember some variables for next time*/ lastErr = error; lastTime = now; } void SetTunings(double Kp, double Ki, double Kd) { kp = Kp; ki = Ki; kd = Kd; }
Compute() is called either regularly or irregularly, and it works pretty well.
This series isn’t about 「works pretty well」 though.
If we’re going to turn this code into something on par with industrial PID controllers, we’ll have to address a few things:
Once we’ve addressed all these issues, we’ll have a solid PID algorithm.
We’ll also, not coincidentally, have the code that’s being used in the lastest version of the Arduino PID Library.
So whether you’re trying to write your own algorithm, or trying to understand what’s going on inside the PID library,
I hope this helps you out. Let’s get started.
UPDATE: In all the code examples I’m using doubles.
On the Arduino, a double is the same as a float (single precision.)
True double precision is WAY overkill for PID.
If the language you’re using does true double precision, I’d recommend changing all doubles to floats.
The Beginner’s PID is designed to be called irregularly. This causes 2 issues:
Ensure that the PID is called at a regular interval.
The way I’ve decided to do this is to specify that the compute function get called every cycle.
based on a pre-determined Sample Time, the PID decides if it should compute or return immediately.
Once we know that the PID is being evaluated at a constant interval,
the derivative and integral calculations can also be simplified. Bonus!
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double errSum, lastErr; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 void Compute() 8 { 9 unsigned long now = millis(); 10 int timeChange = (now - lastTime); 11 if(timeChange>=SampleTime) 12 {
------------------------------------------------------------------------- 13 /*Compute all the working error variables*/ 14 double error = Setpoint - Input; 15 errSum += error; 16 double dErr = (error - lastErr); 17 18 /*Compute PID Output*/ 19 Output = kp * error + ki * errSum + kd * dErr; 20 21 /*Remember some variables for next time*/ 22 lastErr = error;
------------------------------------------------------------------------ 23 lastTime = now; 24 } 25 } 26 27 void SetTunings(double Kp, double Ki, double Kd) 28 { 29 double SampleTimeInSec = ((double)SampleTime)/1000; 30 kp = Kp; 31 ki = Ki * SampleTimeInSec; 32 kd = Kd / SampleTimeInSec; 33 } 34 35 void SetSampleTime(int NewSampleTime) 36 { 37 if (NewSampleTime > 0) 38 { 39 double ratio = (double)NewSampleTime 40 / (double)SampleTime; 41 ki *= ratio; 42 kd /= ratio; 43 SampleTime = (unsigned long)NewSampleTime; 44 } 45 }
On lines 10&11, the algorithm now decides for itself if it’s time to calculate.
Also, because we now KNOW that it’s going to be the same time between samples,
we don’t need to constantly multiply by time change.
We can merely adjust the Ki and Kd appropriately (lines 31 & 32)
and result is mathematically equivalent, but more efficient.
one little wrinkle with doing it this way though though.
if the user decides to change the sample time during operation,
the Ki and Kd will need to be re-tweaked to reflect this new change. that’s what lines 39-42 are all about.
Also Note that I convert the sample time to Seconds on line 29.
Strictly speaking this isn’t necessary, but allows the user to enter Ki and Kd in units of 1/sec and s, rather than 1/mS and mS.
the changes above do 3 things for us
If this PID is going into a microcontroller, a very good argument can be made for using an interrupt.
SetSampleTime sets the interrupt frequency, then Compute gets called when it’s time.
There would be no need, in that case, for lines 9-12, 23, and 24.
If you plan on doing this with your PID implentation, go for it!
Keep reading this series though.
You’ll hopefully still get some benefit from the modifications that follow.
There are three reasons I didn’t use interrupts
This modification is going to tweak the derivative term a bit.
The goal is to eliminate a phenomenon known as 「Derivative Kick」.
The image above illustrates the problem.
Since error=Setpoint-Input, any change in Setpoint causes an instantaneous change in error.
The derivative of this change is infinity (in practice, since dt isn’t 0 it just winds up being a really big number.)
This number gets fed into the pid equation, which results in an undesirable spike in the output.
Luckily there is an easy way to get rid of this.
It turns out that the derivative of the Error is equal to negative derivative of Input, EXCEPT when the Setpoint is changing.
This winds up being a perfect solution.
Instead of adding (Kd * derivative of Error), we subtract (Kd * derivative of Input).
This is known as using 「Derivative on Measurement」
/*working variables*/ unsigned long lastTime; double Input, Output, Setpoint; double errSum, lastInput; double kp, ki, kd; int SampleTime = 1000; //1 sec void Compute() { unsigned long now = millis(); int timeChange = (now - lastTime); if(timeChange>=SampleTime) { /*Compute all the working error variables*/ double error = Setpoint - Input; errSum += error; double dInput = (Input - lastInput); /*Compute PID Output*/ Output = kp * error + ki * errSum - kd * dInput; /*Remember some variables for next time*/ lastInput = Input; lastTime = now; } } void SetTunings(double Kp, double Ki, double Kd) { double SampleTimeInSec = ((double)SampleTime)/1000; kp = Kp; ki = Ki * SampleTimeInSec; kd = Kd / SampleTimeInSec; } void SetSampleTime(int NewSampleTime) { if (NewSampleTime > 0) { double ratio = (double)NewSampleTime / (double)SampleTime; ki *= ratio; kd /= ratio; SampleTime = (unsigned long)NewSampleTime; } }
The modifications here are pretty easy.
We’re replacing +dError with -dInput.
Instead of remembering the lastError, we now remember the lastInput
Here’s what those modifications get us. Notice that the input still looks about the same.
So we get the same performance, but we don’t send out a huge Output spike every time the Setpoint changes.
This may or may not be a big deal.
It all depends on how sensitive your application is to output spikes.
The way I see it though, it doesn’t take any more work to do it without kicking so why not do things right?
The ability to change tuning parameters while the system is running is a must for any respectable PID algorithm.
The Beginner’s PID acts a little crazy if you try to change the tunings while it’s running.
Let’s see why.
Here is the state of the beginner’s PID before and after the parameter change above:
So we can immediately blame this bump on the Integral Term (or 「I Term」).
It’s the only thing that changes drastically when the parameters change.
Why did this happen? It has to do with the beginner’s interpretation of the Integral:
This interpretation works fine until the Ki is changed.
Then, all of a sudden, you multiply this new Ki times the entire error sum that you have accumulated.
That’s not what we wanted! We only wanted to affect things moving forward!
There are a couple ways I know of to deal with this problem.
The method I used in the last library was to rescale errSum.
Ki doubled?
Cut errSum in Half.
That keeps the I Term from bumping, and it works.
It’s kind of clunky though, and I’ve come up with something more elegant.
(There’s no way I’m the first to have thought of this, but I did think of it on my own. That counts damnit!)
The solution requires a little basic algebra (or is it calculus?)
Instead of having the Ki live outside the integral, we bring it inside.
It looks like we haven’t done anything, but we’ll see that in practice this makes a big difference.
Now, we take the error and multiply it by whatever the Ki is at that time.
We then store the sum of THAT.
When the Ki changes, there’s no bump because all the old Ki’s are already 「in the bank」 so to speak.
We get a smooth transfer with no additional math operations.
It may make me a geek but I think that’s pretty sexy.
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double ITerm, lastInput; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 void Compute() 8 { 9 unsigned long now = millis(); 10 int timeChange = (now - lastTime); 11 if(timeChange>=SampleTime) 12 { 13 /*Compute all the working error variables*/ 14 double error = Setpoint - Input; 15 ITerm += (ki * error); 16 double dInput = (Input - lastInput); 17 18 /*Compute PID Output*/ 19 Output = kp * error + ITerm - kd * dInput; 20 21 /*Remember some variables for next time*/ 22 lastInput = Input; 23 lastTime = now; 24 } 25 } 26 27 void SetTunings(double Kp, double Ki, double Kd) 28 { 29 double SampleTimeInSec = ((double)SampleTime)/1000; 30 kp = Kp; 31 ki = Ki * SampleTimeInSec; 32 kd = Kd / SampleTimeInSec; 33 } 34 35 void SetSampleTime(int NewSampleTime) 36 { 37 if (NewSampleTime > 0) 38 { 39 double ratio = (double)NewSampleTime 40 / (double)SampleTime; 41 ki *= ratio; 42 kd /= ratio; 43 SampleTime = (unsigned long)NewSampleTime; 44 } 45 }
So we replaced the errSum variable with a composite ITerm variable [Line 4].
It sums Ki*error, rather than just error [Line 15].
Also, because Ki is now buried in ITerm, it’s removed from the main PID calculation [Line 19].
So how does this fix things.
Before when ki was changed, it rescaled the entire sum of the error; every error value we had seen.
With this code, the previous error remains untouched,
and the new ki only affects things moving forward, which is exactly what we want.
Reset windup is a trap that probably claims more beginners than any other.
It occurs when the PID thinks it can do something that it can’t.
For example, the PWM output on an Arduino accepts values from 0-255.
By default the PID doesn’t know this.
If it thinks that 300-400-500 will work, it’s going to try those values expecting to get what it needs.
Since in reality the value is clamped at 255 it’s just going to keep trying higher
and higher numbers without getting anywhere.
The problem reveals itself in the form of weird lags.
Above we can see that the output gets 「wound up」 WAY above the external limit.
When the setpoint is dropped the output has to wind down before getting below that 255-line.
There are several ways that windup can be mitigated, but the one that I chose was as follows:
tell the PID what the output limits are.
In the code below you’ll see there’s now a SetOuputLimits function.
Once either limit is reached, the pid stops summing (integrating.)
It knows there’s nothing to be done;
Since the output doesn’t wind-up, we get an immediate response
when the setpoint drops into a range where we can do something.
Notice in the graph above though, that while we got rid that windup lag, we’re not all the way there.
There’s still a difference between what the pid thinks it’s sending, and what’s being sent.
Why? the Proportional Term and (to a lesser extent) the Derivative Term.
Even though the Integral Term has been safely clamped, P and D are still adding their two cents,
yielding a result higher than the output limit.
To my mind this is unacceptable.
If the user calls a function called 「SetOutputLimits」 they’ve got to assume that that means
「the output will stay within these values.」
So for Step 2, we make that a valid assumption.
In addition to clamping the I-Term, we clamp the Output value so that it stays where we’d expect it.
(Note: You might ask why we need to clamp both.
If we’re going to do the output anyway, why clamp the Integral separately?
If all we did was clamp the output, the Integral term would go back to growing and growing.
Though the output would look nice during the step up, we’d see that telltale lag on the step down.)
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double ITerm, lastInput; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 double outMin, outMax; 8 void Compute() 9 { 10 unsigned long now = millis(); 11 int timeChange = (now - lastTime); 12 if(timeChange>=SampleTime) 13 { 14 /*Compute all the working error variables*/ 15 double error = Setpoint - Input; 16 ITerm+= (ki * error); 17 if(ITerm> outMax) ITerm= outMax; 18 else if(ITerm< outMin) ITerm= outMin; 19 double dInput = (Input - lastInput); 20 21 /*Compute PID Output*/ 22 Output = kp * error + ITerm- kd * dInput; 23 if(Output > outMax) Output = outMax; 24 else if(Output < outMin) Output = outMin; 25 26 /*Remember some variables for next time*/ 27 lastInput = Input; 28 lastTime = now; 29 } 30 } 31 32 void SetTunings(double Kp, double Ki, double Kd) 33 { 34 double SampleTimeInSec = ((double)SampleTime)/1000; 35 kp = Kp; 36 ki = Ki * SampleTimeInSec; 37 kd = Kd / SampleTimeInSec; 38 } 39 40 void SetSampleTime(int NewSampleTime) 41 { 42 if (NewSampleTime > 0) 43 { 44 double ratio = (double)NewSampleTime 45 / (double)SampleTime; 46 ki *= ratio; 47 kd /= ratio; 48 SampleTime = (unsigned long)NewSampleTime; 49 } 50 } 51 52 void SetOutputLimits(double Min, double Max) 53 { 54 if(Min > Max) return; 55 outMin = Min; 56 outMax = Max; 57 58 if(Output > outMax) Output = outMax; 59 else if(Output < outMin) Output = outMin; 60 61 if(ITerm> outMax) ITerm= outMax; 62 else if(ITerm< outMin) ITerm= outMin; 63 }
A new function was added to allow the user to specify the output limits [lines 52-63].
And these limits are used to clamp both the I-Term [17-18] and the Output [23-24]
As we can see, windup is eliminated.
in addition, the output stays where we want it to.
this means there’s no need for external clamping of the output.
if you want it to range from 23 to 167, you can set those as the Output Limits.
As nice as it is to have a PID controller, sometimes you don’t care what it has to say.
Let’s say at some point in your program you want to force the output to a certain value (0 for example)
you could certainly do this in the calling routine:
void loop() { Compute(); Output=0; }
This way, no matter what the PID says, you just overwrite its value.
This is a terrible idea in practice however.
The PID will become very confused:
「I keep moving the output, and nothing’s happening! What gives?! Let me move it some more.」
As a result, when you stop over-writing the output and switch back to the PID,
you will likely get a huge and immediate change in the output value.
The solution to this problem is to have a means to turn the PID off and on.
The common terms for these states are 「Manual」 (I will adjust the value by hand)
and 「Automatic」 (the PID will automatically adjust the output).
Let’s see how this is done in code:
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double ITerm, lastInput; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 double outMin, outMax; 8 bool inAuto = false; 9 10 #define MANUAL 0 11 #define AUTOMATIC 1 12 13 void Compute() 14 { 15 if(!inAuto) return; 16 unsigned long now = millis(); 17 int timeChange = (now - lastTime); 18 if(timeChange>=SampleTime) 19 { 20 /*Compute all the working error variables*/ 21 double error = Setpoint - Input; 22 ITerm+= (ki * error); 23 if(ITerm> outMax) ITerm= outMax; 24 else if(ITerm< outMin) ITerm= outMin; 25 double dInput = (Input - lastInput); 26 27 /*Compute PID Output*/ 28 Output = kp * error + ITerm- kd * dInput; 29 if(Output > outMax) Output = outMax; 30 else if(Output < outMin) Output = outMin; 31 32 /*Remember some variables for next time*/ 33 lastInput = Input; 34 lastTime = now; 35 } 36 } 37 38 void SetTunings(double Kp, double Ki, double Kd) 39 { 40 double SampleTimeInSec = ((double)SampleTime)/1000; 41 kp = Kp; 42 ki = Ki * SampleTimeInSec; 43 kd = Kd / SampleTimeInSec; 44 } 45 46 void SetSampleTime(int NewSampleTime) 47 { 48 if (NewSampleTime > 0) 49 { 50 double ratio = (double)NewSampleTime 51 / (double)SampleTime; 52 ki *= ratio; 53 kd /= ratio; 54 SampleTime = (unsigned long)NewSampleTime; 55 } 56 } 57 58 void SetOutputLimits(double Min, double Max) 59 { 60 if(Min > Max) return; 61 outMin = Min; 62 outMax = Max; 63 64 if(Output > outMax) Output = outMax; 65 else if(Output < outMin) Output = outMin; 66 67 if(ITerm> outMax) ITerm= outMax; 68 else if(ITerm< outMin) ITerm= outMin; 69 } 70 71 void SetMode(int Mode) 72 { 73 inAuto = (Mode == AUTOMATIC); 74 }
A fairly simple solution. If you’re not in automatic mode,
immediately leave the Compute function without adjusting the Output or any internal variables.
It’s true that you could achieve a similar effect by just not calling Compute from the calling routine,
but this solution keeps the workings of the PID contained, which is kind of what we need.
By keeping things internal we can keep track of which mode were in,
and more importantly it let’s us know when we change modes. That leads us to the next issue…
In the last section we implemented the ability to turn the PID off and on.
We turned it off, but now let’s look at what happens when we turn it back on:
Yikes! The PID jumps back to the last Output value it sent, then starts adjusting from there.
This results in an Input bump that we’d rather not have.
This one is pretty easy to fix. Since we now know when we’re turning on (going from Manual to Automatic,) we just have to initialize things for a smooth transition. That means massaging the 2 stored working variables (ITerm & lastInput) to keep the output from jumping.
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double ITerm, lastInput; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 double outMin, outMax; 8 bool inAuto = false; 9 10 #define MANUAL 0 11 #define AUTOMATIC 1 12 13 void Compute() 14 { 15 if(!inAuto) return; 16 unsigned long now = millis(); 17 int timeChange = (now - lastTime); 18 if(timeChange>=SampleTime) 19 { 20 /*Compute all the working error variables*/ 21 double error = Setpoint - Input; 22 ITerm+= (ki * error); 23 if(ITerm> outMax) ITerm= outMax; 24 else if(ITerm< outMin) ITerm= outMin; 25 double dInput = (Input - lastInput); 26 27 /*Compute PID Output*/ 28 Output = kp * error + ITerm- kd * dInput; 29 if(Output> outMax) Output = outMax; 30 else if(Output < outMin) Output = outMin; 31 32 /*Remember some variables for next time*/ 33 lastInput = Input; 34 lastTime = now; 35 } 36 } 37 38 void SetTunings(double Kp, double Ki, double Kd) 39 { 40 double SampleTimeInSec = ((double)SampleTime)/1000; 41 kp = Kp; 42 ki = Ki * SampleTimeInSec; 43 kd = Kd / SampleTimeInSec; 44 } 45 46 void SetSampleTime(int NewSampleTime) 47 { 48 if (NewSampleTime > 0) 49 { 50 double ratio = (double)NewSampleTime 51 / (double)SampleTime; 52 ki *= ratio; 53 kd /= ratio; 54 SampleTime = (unsigned long)NewSampleTime; 55 } 56 } 57 58 void SetOutputLimits(double Min, double Max) 59 { 60 if(Min > Max) return; 61 outMin = Min; 62 outMax = Max; 63 64 if(Output > outMax) Output = outMax; 65 else if(Output < outMin) Output = outMin; 66 67 if(ITerm> outMax) ITerm= outMax; 68 else if(ITerm< outMin) ITerm= outMin; 69 } 70 71 void SetMode(int Mode) 72 { 73 bool newAuto = (Mode == AUTOMATIC); 74 if(newAuto && !inAuto) 75 { /*we just went from manual to auto*/ 76 Initialize(); 77 } 78 inAuto = newAuto; 79 } 80 81 void Initialize() 82 { 83 lastInput = Input; 84 ITerm = Output; 85 if(ITerm> outMax) ITerm= outMax; 86 else if(ITerm< outMin) ITerm= outMin; 87 }
We modified SetMode(…) to detect the transition from manual to automatic,
and we added our initialization function.
It sets ITerm=Output to take care of the integral term, and lastInput = Input to keep the derivative from spiking.
The proportional term doesn’t rely on any information from the past, so it doesn’t need any initialization.
We see from the above graph that proper initialization results in a bumpless transfer from manual to automatic: exactly what we were after
I have been getting a lot of questions recently asking why I don’t set ITerm=0 upon intialization.
As an answer, I’d ask you to consider the following scenario:
The pid is in manual, and the user has set the output to 50.
After a time, the process steadies out to an input of 75.2.
The user makes the Setpoint 75.2 and turns on the pid. What should happen?
I contend that after switching to automatic the output value should stay at 50.
since the P and D terms will be zero, the only way this will happen is if ITerm is initialized to the value of Output.
If you are in a situation where you need the output to initialize to zero, there is no need alter the code above.
Just set Output=0 in your calling routine before turning the PID from Manual to Automatic.
The processes the PID will be connected to fall into two groups: direct acting and reverse acting. All the examples I’ve shown so far have been direct acting. That is, an increase in the output causes an increase in the input. For reverse acting processes the opposite is true. In a refrigerator for example, an increase in cooling causes the temperature to go down. To make the beginner PID work with a reverse process, the signs of kp, ki, and kd all must be negative.
This isn’t a problem per se, but the user must choose the correct sign, and make sure that all the parameters have the same sign.
To make the process a little simpler, I require that kp, ki, and kd all be >=0. If the user is connected to a reverse process, they specify that separately using the SetControllerDirection function. this ensures that the parameters all have the same sign, and hopefully makes things more intuitive.
1 /*working variables*/ 2 unsigned long lastTime; 3 double Input, Output, Setpoint; 4 double ITerm, lastInput; 5 double kp, ki, kd; 6 int SampleTime = 1000; //1 sec 7 double outMin, outMax; 8 bool inAuto = false; 9 10 #define MANUAL 0 11 #define AUTOMATIC 1 12 13 #define DIRECT 0 14 #define REVERSE 1 15 int controllerDirection = DIRECT; 16 17 void Compute() 18 { 19 if(!inAuto) return; 20 unsigned long now = millis(); 21 int timeChange = (now - lastTime); 22 if(timeChange>=SampleTime) 23 { 24 /*Compute all the working error variables*/ 25 double error = Setpoint - Input; 26 ITerm+= (ki * error); 27 if(ITerm > outMax) ITerm= outMax; 28 else if(ITerm < outMin) ITerm= outMin; 29 double dInput = (Input - lastInput); 30 31 /*Compute PID Output*/ 32 Output = kp * error + ITerm- kd * dInput; 33 if(Output > outMax) Output = outMax; 34 else if(Output < outMin) Output = outMin; 35 36 /*Remember some variables for next time*/ 37 lastInput = Input; 38 lastTime = now; 39 } 40 } 41 42 void SetTunings(double Kp, double Ki, double Kd) 43 { 44 if (Kp<0 || Ki<0|| Kd<0) return; 45 46 double SampleTimeInSec = ((double)SampleTime)/1000; 47 kp = Kp; 48 ki = Ki * SampleTimeInSec; 49 kd = Kd / SampleTimeInSec; 50 51 if(controllerDirection ==REVERSE) 52 { 53 kp = (0 - kp); 54 ki = (0 - ki); 55 kd = (0 - kd); 56 } 57 } 58 59 void SetSampleTime(int NewSampleTime) 60 { 61 if (NewSampleTime > 0) 62 { 63 double ratio = (double)NewSampleTime 64 / (double)SampleTime; 65 ki *= ratio; 66 kd /= ratio; 67 SampleTime = (unsigned long)NewSampleTime; 68 } 69 } 70 71 void SetOutputLimits(double Min, double Max) 72 { 73 if(Min > Max) return; 74 outMin = Min; 75 outMax = Max; 76 77 if(Output > outMax) Output = outMax; 78 else if(Output < outMin) Output = outMin; 79 80 if(ITerm > outMax) ITerm= outMax; 81 else if(ITerm < outMin) ITerm= outMin; 82 } 83 84 void SetMode(int Mode) 85 { 86 bool newAuto = (Mode == AUTOMATIC); 87 if(newAuto == !inAuto) 88 { /*we just went from manual to auto*/ 89 Initialize(); 90 } 91 inAuto = newAuto; 92 } 93 94 void Initialize() 95 { 96 lastInput = Input; 97 ITerm = Output; 98 if(ITerm > outMax) ITerm= outMax; 99 else if(ITerm < outMin) ITerm= outMin; 100 } 101 102 void SetControllerDirection(int Direction) 103 { 104 controllerDirection = Direction; 105 }
And that about wraps it up.
We’ve turned 「The Beginner’s PID」 into the most robust controller I know how to make at this time.
For those readers that were looking for a detailed explanation of the PID Library,
I hope you got what you came for. For those of you writing your own PID,
I hope you were able to glean a few ideas that save you some cycles down the road.
Two Final Notes:
PID Library
PID Front-End using Processing.org
Older Versions can be viewed / downloaded here
From Wikipedia: "A PID controller calculates an 'error' value as the difference between a measured [Input] and a desired setpoint. The controller attempts to minimize the error by adjusting [an Output]."
So, you tell the PID what to measure (the "Input",) Where you want that measurement to be (the "Setpoint",) and the variable to adjust that can make that happen (the "Output".) The PID then adjusts the output trying to make the input equal the setpoint.
For reference, in a car, the Input, Setpoint, and Output would be the speed, desired speed, and gas pedal angle respectively.
The black magic of PID comes in when we talk about HOW it adjusts the Output to drive the Input towards Setpoint. There are 3 Tuning Parameters (or "Tunings"): Kp, Ki & Kd. Adjusting these values will change the way the output is adjusted. Fast? Slow? God-awful? All of these can be achieved depending on the values of Kp, Ki, and Kd.
So what are the "right" tuning values to use? There isn't one right answer. The values that work for one application may not work for another, just as the driving style that works for a truck may not work for a race car. With each new application you will need to try Several Tuning values until you find a set that gives you what you want.
Note: there is also now a PID Autotune Library that can help you determine tuning parameters.
PID is a pretty impressive control algorithm, but it's not magic. You should at least be able to answer "yes" to these questions:
(This list will probably grow as I think of more criteria. These are the big ones though)
Even though a PID controller is designed to work with an analog output, it is possible to connect to a discrete output such as a relay. Be sure to look at the RelayOutput example below.
Try the newly created Google group.
Using The PID Library has two benefits in my mind
***************************************************************
* Arduino PID Library - Version 1.1.1
* by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com
*
* This Library is licensed under a GPLv3 License
***************************************************************
- For an ultra-detailed explanation of why the code is the way it is, please visit:
http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
- For function documentation see: http://playground.arduino.cc/Code/PIDLibrary
#ifndef PID_v1_h #define PID_v1_h #define LIBRARY_VERSION 1.1.1 class PID { public: //Constants used in some of the functions below #define AUTOMATIC 1 #define MANUAL 0 #define DIRECT 0 #define REVERSE 1 //commonly used functions ************************************************************************** PID(double*, double*, double*, // * constructor. links the PID to the Input, Output, and double, double, double, int); // Setpoint. Initial tuning parameters are also set here void SetMode(int Mode); // * sets PID to either Manual (0) or Auto (non-0) bool Compute(); // * performs the PID calculation. it should be // called every time loop() cycles. ON/OFF and // calculation frequency can be set using SetMode // SetSampleTime respectively void SetOutputLimits(double, double); //clamps the output to a specific range. 0-255 by default, but //it's likely the user will want to change this depending on //the application //available but not commonly used functions ******************************************************** void SetTunings(double, double, // * While most users will set the tunings once in the double); // constructor, this function gives the user the option // of changing tunings during runtime for Adaptive control void SetControllerDirection(int); // * Sets the Direction, or "Action" of the controller. DIRECT // means the output will increase when error is positive. REVERSE // means the opposite. it's very unlikely that this will be needed // once it is set in the constructor. void SetSampleTime(int); // * sets the frequency, in Milliseconds, with which // the PID calculation is performed. default is 100 //Display functions **************************************************************** double GetKp(); // These functions query the pid for interal values. double GetKi(); // they were created mainly for the pid front-end, double GetKd(); // where it's important to know what is actually int GetMode(); // inside the PID. int GetDirection(); // private: void Initialize(); double dispKp; // * we'll hold on to the tuning parameters in user-entered double dispKi; // format for display purposes double dispKd; // double kp; // * (P)roportional Tuning Parameter double ki; // * (I)ntegral Tuning Parameter double kd; // * (D)erivative Tuning Parameter int controllerDirection; double *myInput; // * Pointers to the Input, Output, and Setpoint variables double *myOutput; // This creates a hard link between the variables and the double *mySetpoint; // PID, freeing the user from having to constantly tell us // what these values are. with pointers we'll just know. unsigned long lastTime; double ITerm, lastInput; unsigned long SampleTime; double outMin, outMax; bool inAuto; }; #endif
/********************************************************************************************** * Arduino PID Library - Version 1.1.1 * by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com * * This Library is licensed under a GPLv3 License **********************************************************************************************/ #if ARDUINO >= 100 #include "Arduino.h" #else #include "WProgram.h" #endif #include <PID_v1.h> /*Constructor (...)********************************************************* * The parameters specified here are those for for which we can't set up * reliable defaults, so we need to have the user set them. ***************************************************************************/ PID::PID(double* Input, double* Output, double* Setpoint, double Kp, double Ki, double Kd, int ControllerDirection) { myOutput = Output; myInput = Input; mySetpoint = Setpoint; inAuto = false; PID::SetOutputLimits(0, 255); //default output limit corresponds to //the arduino pwm limits SampleTime = 100; //default Controller Sample Time is 0.1 seconds PID::SetControllerDirection(ControllerDirection); PID::SetTunings(Kp, Ki, Kd); lastTime = millis()-SampleTime; } /* Compute() ********************************************************************** * This, as they say, is where the magic happens. this function should be called * every time "void loop()" executes. the function will decide for itself whether a new * pid Output needs to be computed. returns true when the output is computed, * false when nothing has been done. **********************************************************************************/ bool PID::Compute() { if(!inAuto) return false; unsigned long now = millis(); unsigned long timeChange = (now - lastTime); if(timeChange>=SampleTime) { /*Compute all the working error variables*/ double input = *myInput; double error = *mySetpoint - input; ITerm+= (ki * error); if(ITerm > outMax) ITerm= outMax; else if(ITerm < outMin) ITerm= outMin; double dInput = (input - lastInput); /*Compute PID Output*/ double output = kp * error + ITerm- kd * dInput; if(output > outMax) output = outMax; else if(output < outMin) output = outMin; *myOutput = output; /*Remember some variables for next time*/ lastInput = input; lastTime = now; return true; } else return false; } /* SetTunings(...)************************************************************* * This function allows the controller's dynamic performance to be adjusted. * it's called automatically from the constructor, but tunings can also * be adjusted on the fly during normal operation ******************************************************************************/ void PID::SetTunings(double Kp, double Ki, double Kd) { if (Kp<0 || Ki<0 || Kd<0) return; dispKp = Kp; dispKi = Ki; dispKd = Kd; double SampleTimeInSec = ((double)SampleTime)/1000; kp = Kp; ki = Ki * SampleTimeInSec; kd = Kd / SampleTimeInSec; if(controllerDirection ==REVERSE) { kp = (0 - kp); ki = (0 - ki); kd = (0 - kd); } } /* SetSampleTime(...) ********************************************************* * sets the period, in Milliseconds, at which the calculation is performed ******************************************************************************/ void PID::SetSampleTime(int NewSampleTime) { if (NewSampleTime > 0) { double ratio = (double)NewSampleTime / (double)SampleTime; ki *= ratio; kd /= ratio; SampleTime = (unsigned long)NewSampleTime; } } /* SetOutputLimits(...)**************************************************** * This function will be used far more often than SetInputLimits. while * the input to the controller will generally be in the 0-1023 range (which is * the default already,) the output will be a little different. maybe they'll * be doing a time window and will need 0-8000 or something. or maybe they'll * want to clamp it from 0-125. who knows. at any rate, that can all be done * here. **************************************************************************/ void PID::SetOutputLimits(double Min, double Max) { if(Min >= Max) return; outMin = Min; outMax = Max; if(inAuto) { if(*myOutput > outMax) *myOutput = outMax; else if(*myOutput < outMin) *myOutput = outMin; if(ITerm > outMax) ITerm= outMax; else if(ITerm < outMin) ITerm= outMin; } } /* SetMode(...)**************************************************************** * Allows the controller Mode to be set to manual (0) or Automatic (non-zero) * when the transition from manual to auto occurs, the controller is * automatically initialized ******************************************************************************/ void PID::SetMode(int Mode) { bool newAuto = (Mode == AUTOMATIC); if(newAuto == !inAuto) { /*we just went from manual to auto*/ PID::Initialize(); } inAuto = newAuto; } /* Initialize()**************************************************************** * does all the things that need to happen to ensure a bumpless transfer * from manual to automatic mode. ******************************************************************************/ void PID::Initialize() { ITerm = *myOutput; lastInput = *myInput; if(ITerm > outMax) ITerm = outMax; else if(ITerm < outMin) ITerm = outMin; } /* SetControllerDirection(...)************************************************* * The PID will either be connected to a DIRECT acting process (+Output leads * to +Input) or a REVERSE acting process(+Output leads to -Input.) we need to * know which one, because otherwise we may increase the output when we should * be decreasing. This is called from the constructor. ******************************************************************************/ void PID::SetControllerDirection(int Direction) { if(inAuto && Direction !=controllerDirection) { kp = (0 - kp); ki = (0 - ki); kd = (0 - kd); } controllerDirection = Direction; } /* Status Funcions************************************************************* * Just because you set the Kp=-1 doesn't mean it actually happened. these * functions query the internal state of the PID. they're here for display * purposes. this are the functions the PID Front-end uses for example ******************************************************************************/ double PID::GetKp(){ return dispKp; } double PID::GetKi(){ return dispKi;} double PID::GetKd(){ return dispKd;} int PID::GetMode(){ return inAuto ? AUTOMATIC : MANUAL;} int PID::GetDirection(){ return controllerDirection;}
Example
/******************************************************** * PID Basic Example * Reading analog input 0 to control analog PWM output 3 ********************************************************/ #include <PID_v1.h> #define PIN_INPUT 0 #define PIN_OUTPUT 3 //Define Variables we'll be connecting to double Setpoint, Input, Output; //Specify the links and initial tuning parameters double Kp=2, Ki=5, Kd=1; PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); void setup() { //initialize the variables we're linked to Input = analogRead(PIN_INPUT); Setpoint = 100; //turn the PID on myPID.SetMode(AUTOMATIC); } void loop() { Input = analogRead(PIN_INPUT); myPID.Compute(); analogWrite(PIN_OUTPUT, Output); }
/******************************************************** * PID Adaptive Tuning Example * One of the benefits of the PID library is that you can * change the tuning parameters at any time. this can be * helpful if we want the controller to be agressive at some * times, and conservative at others. in the example below * we set the controller to use Conservative Tuning Parameters * when we're near setpoint and more agressive Tuning * Parameters when we're farther away. ********************************************************/ #include <PID_v1.h> #define PIN_INPUT 0 #define PIN_OUTPUT 3 //Define Variables we'll be connecting to double Setpoint, Input, Output; //Define the aggressive and conservative Tuning Parameters double aggKp=4, aggKi=0.2, aggKd=1; double consKp=1, consKi=0.05, consKd=0.25; //Specify the links and initial tuning parameters PID myPID(&Input, &Output, &Setpoint, consKp, consKi, consKd, DIRECT); void setup() { //initialize the variables we're linked to Input = analogRead(PIN_INPUT); Setpoint = 100; //turn the PID on myPID.SetMode(AUTOMATIC); } void loop() { Input = analogRead(PIN_INPUT); double gap = abs(Setpoint-Input); //distance away from setpoint if (gap < 10) { //we're close to setpoint, use conservative tuning parameters myPID.SetTunings(consKp, consKi, consKd); } else { //we're far from setpoint, use aggressive tuning parameters myPID.SetTunings(aggKp, aggKi, aggKd); } myPID.Compute(); analogWrite(PIN_OUTPUT, Output); }
/******************************************************** * PID RelayOutput Example * Same as basic example, except that this time, the output * is going to a digital pin which (we presume) is controlling * a relay. the pid is designed to Output an analog value, * but the relay can only be On/Off. * * to connect them together we use "time proportioning * control" it's essentially a really slow version of PWM. * first we decide on a window size (5000mS say.) we then * set the pid to adjust its output between 0 and that window * size. lastly, we add some logic that translates the PID * output into "Relay On Time" with the remainder of the * window being "Relay Off Time" ********************************************************/ #include <PID_v1.h> #define PIN_INPUT 0 #define RELAY_PIN 6 //Define Variables we'll be connecting to double Setpoint, Input, Output; //Specify the links and initial tuning parameters double Kp=2, Ki=5, Kd=1; PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); int WindowSize = 5000; unsigned long windowStartTime; void setup() { windowStartTime = millis(); //initialize the variables we're linked to Setpoint = 100; //tell the PID to range between 0 and the full window size myPID.SetOutputLimits(0, WindowSize); //turn the PID on myPID.SetMode(AUTOMATIC); } void loop() { Input = analogRead(PIN_INPUT); myPID.Compute(); /************************************************ * turn the output pin on/off based on pid output ************************************************/ if (millis() - windowStartTime > WindowSize) { //time to shift the Relay Window windowStartTime += WindowSize; } if (Output < millis() - windowStartTime) digitalWrite(RELAY_PIN, HIGH); else digitalWrite(RELAY_PIN, LOW); }