PID控制
引言:
在工业应用中PID及其衍生算法是应用最广泛的算法之一,是当之无愧的万能算法,如果能够熟练掌握PID算法的设计与实现过程,对于一般的研发人员来讲,应该是足够应对一般研发问题了,而难能可贵的是,在我所接触的控制算法当中,PID控制算法又是最简单,最能体现反馈思想的控制算法,可谓经典中的经典。经典的未必是复杂的,经典的东西常常是简单的,而且是最简单的。
PID控制算法的C语言实现一 PID算法原理
先看看PID算法的一般形式:
PID的流程简单到了不能再简单的程度,通过误差信号控制被控量,而控制器本身就是比例、积分、微分三个环节的加和。这里我们规定(在t时刻):
1.输入量为rin(t);(设定值)
2.输出量为rout(t);(实际值)
3.偏差量为err(t)=rin(t)-rout(t);
理解一下这个公式,主要从下面几个问题着手,为了便于理解,把控制环境具体一下:
1.规定这个流程是用来为直流电机调速的;
2.输入量rin(t)为电机转速预定值;
3.输出量rout(t)为电机转速实际值;
4.执行器为直流电机;
5.传感器为光电码盘,假设码盘为10线;
6.直流电机采用PWM调速 转速用单位 转/min 表示;
不难看出以下结论:
1.输入量rin(t)为电机转速预定值(转/min);
输出量rout(t)为电机转速实际值(转/min);
3.偏差量为预定值和实际值之差(转/min);那么以下几个问题需要弄清楚:
1.通过PID环节之后的U(t)是什么值呢?
2.控制执行器(直流电机)转动转速应该为电压值(也就是PWM占空比)。
3.那么U(t)与PWM之间存在怎样的联系呢?
http://blog.21ic.com/user1/3407/archives/2006/33541.html(见附录1)
这篇文章上给出了一种方法,即,每个电压对应一个转速,电压和转速之间呈现线性关系。但是我考虑这种方法的前提是把直流电机的特性理解为线性了,而实际情况下,直流电机的特性绝对不是线性的,或者说在局部上是趋于线性的,这就是为什么说PID调速有个范围的问题。具体看一下http://articles.e-works.net.cn/component/article90249.htm(见附录2)
这篇文章就可以了解了。所以在正式进行调速设计之前,需要现有开环系统,测试电机和转速之间的特性曲线(或者查阅电机的资料说明),然后再进行闭环参数整定。这篇先写到这,下一篇说明连续系统的离散化问题。并根据离散化后的特点讲述位置型PID和增量型PID的用法和C语言实现过程。
PID控制算法的C语言实现二 PID算法的离散化
上一节中,我论述了PID算法的基本形式,并对其控制过程的实现有了一个简要的说明,通过上一节的总结,基本已经可以明白PID控制的过程。这一节中先继续上一节内容补充说明一下。
1.PID控制其实是对偏差的控制过程
2.如果偏差为0,则比例环节不起作用,只有存在偏差时,比例环节才起作用。
3.积分环节主要是用来消除静差——静差,是系统稳定后输出值和设定值之间的差值,积分环节实际上就是偏差累计的过程,把累计的误差加到原有系统上以抵消系统造成的静差。
这里我的理解是,如果在静差持续存在的时候,说明u(t)仍不够大/小,通过对该偏差的持续累积增大/减小u(t)的值,来使实际值向设定值靠近。
4.而微分信号则反应了偏差信号的变化趋势,根据偏差信号的变化趋势来进行超前调节,从而增加了系统的快速性。这里我的理解是,进行预测未来,当增长量过大时du(t)/dt也会反向很大,起到来拒去留,稳定系统的效果。
这是PID的数学表述:
下面将对PID连续系统离散化,从而方便在处理器上实现:
假设采样间隔为T,则在第K T时刻:
偏差err(K)=rin(K)-rout(K);
积分环节用加和的形式表示,即err(K)+err(K-1)+……;
微分环节用斜率的形式表示,即[err(K)-err(K-1)]/T;(T为采样时间)
从而形成如下PID离散表示形式:
则u(K)可表示成为:ActualSpeed=Kperr+Kiintegral+Kd*(err-err_last)
用离散型数学表达即为:
这种表述形式称为位置型PID。
且由连续型易得Ki=KpT/Ti,Kd=KpTd/T,其中T为采样时间可知
Ti称为积分时间,Td称为积分时间。他们可以替代Ki,Kd,两者地位相同(参数变种)
从这个公式可以知道Ki,Kd均受到Kp的影响
另外一种表述方式为增量式PID:
incrementSpeed=Kp(err-err_next)+Kierr+Kd(err-2err_next+err_last);
ActualSpeed+=incrementSpeed
这就是离散化PID的增量式表示方式,增量式的表达结果和最近三次的偏差有关,这样就大大提高了系统的稳定性。需要注意最终的输出结果应该是增量式PID的累加。
PID的离散化过程基本思路就是这样,下面是将离散化的公式转换成为C语言。
PID控制算法的C语言实现三 位置型PID的C语言实现
第一步:定义PID变量结构体,代码如下:1
2
3
4
5
6
7
8
9struct _pid{
float SetSpeed; //定义设定值
float ActualSpeed; //定义实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float voltage; //定义电压值(控制执行器的变量)(有的地方也用电流)
float integral; //定义积分值
}pid;
控制算法中所需要用到的参数在一个结构体中统一定义,方便后面的使用。
第二部:初始化变量,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13void PID_init(){
printf("PID_init begin \n");
pid.SetSpeed=0.0;
pid.ActualSpeed=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.voltage=0.0;
pid.integral=0.0;
pid.Kp=0.2;
pid.Ki=0.015;
pid.Kd=0.2;
printf("PID_init end \n");
}
统一初始化变量,尤其是Kp,Ki,Kd三个参数,调试过程当中,对于要求的控制效果,可以通过调节这三个量直接进行调节。
第三步:编写控制算法,代码如下:1
2
3
4
5
6
7
8
9
10
11float PID_realize(float speed){ //传入的参数用作设定值
pid.SetSpeed=speed;
pid.err=pid.SetSpeed-pid.ActualSpeed; //偏差值
pid.integral+=pid.err; //偏差值的积分累加,用作I
//通过PID算法得到pid下的电压,通过电压控制实际速度
//这里没有dt是因为dt由系统程序(时钟)决定,因为会把这段语句放入循环
pid.voltage=pid.Kp*pid.err+pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
pid.err_last=pid.err; //上次的偏差值,用作D运算
pid.ActualSpeed=pid.voltage*1.0; //电机电压与实际速度比例为1
return pid.ActualSpeed;
}
注意:这里用了最基本的算法实现形式,没有考虑死区问题,没有设定上下限,只是对公式的一种直接的实现,后面的介绍当中还会逐渐的对此改进。
下面是测试代码:1
2
3
4
5
6
7
8
9
10
11
12int main(){
printf("System begin \n");
PID_init();
int count=0;
while(count<1000)
{
float speed=PID_realize(200.0); //循环PID逼近200
printf("%f\n",speed);
count++;
}
return 0;
}
PID控制算法的C语言实现四 增量型PID的C语言实现
上一节中介绍了最简单的位置型PID的实现手段,这一节主要讲解增量式PID的实现方法,实现过程仍然是分为定义变量、初始化变量、实现控制算法函数、算法测试四个部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47#include<stdio.h>
#include<stdlib.h>
struct _pid{
float SetSpeed; //定义设定值
float ActualSpeed; //定义实际值
float err; //定义偏差值
float err_next; //定义上一个偏差值
float err_last; //定义上上个的偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
}pid;
void PID_init(){
pid.SetSpeed=0.0;
pid.ActualSpeed=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.err_next=0.0;
pid.Kp=0.2;
pid.Ki=0.015;
pid.Kd=0.2;
}
float PID_realize(float speed){
pid.SetSpeed=speed;
pid.err=pid.SetSpeed-pid.ActualSpeed;
//这里与位置式不同的是,增量式得到的是一个delta而不是可以直接用的实际值
//最后需要将delta累加起来才能得到实际速度
float incrementSpeed=pid.Kp*(pid.err-pid.err_next)+pid.Ki*pid.err+pid.Kd*(pid.err-2*pid.err_next+pid.err_last);
pid.ActualSpeed+=incrementSpeed; //累加delta得实际速度
pid.err_last=pid.err_next;
pid.err_next=pid.err;
return pid.ActualSpeed;
}
int main(){
PID_init();
int count=0;
while(count<1000)
{
float speed=PID_realize(200.0);
printf("%f\n",speed);
count++;
}
return 0;
}
PID控制算法的C语言实现五 积分分离的PID控制算法C语言实现
通过三、四两篇文章,基本上已经弄清楚了PID控制算法的最常规的表达方法。在普通PID控制中,引入积分环节的目的,主要是为了消除静差,提高控制精度。但是在启动、结束或大幅度增减设定时,短时间内系统输出有很大的偏差(详见pid数据开头部分)会造成PID运算的积分积累,导致控制量超过执行机构可能允许的最大动作范围对应极限控制量,从而引起较大的超调,甚至是震荡,这是绝对不允许的。
为了克服这一问题,引入了积分分离的概念,基本思路是当被控量与设定值偏差较大时,取消积分作用; 当被控量接近给定值时,引入积分控制,以消除静差,提高精度。其具体实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12pid.Kp=0.2;
pid.Ki=0.04;
pid.Kd=0.2; //初始化过程
if(abs(pid.err)>200) //200太极限了,总之就是当偏差值过大时不引入积分(index=0且不累计)
index=0;
else
{
index=1;
pid.integral+=pid.err;
}
pid.voltage=pid.Kp*pid.err+index*pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
其他代码相同,不做赘述
PID控制算法的C语言实现六 抗积分饱和的PID控制算法C语言实现
所谓的积分饱和现象是指如果系统存在一个方向的偏差,PID控制器的输出由于积分作用的不断累加而加大,从而导致执行机构达到极限位置,若控制器输出U(k)继续增大,执行器开度不可能再增大,此时计算机输出控制量超出了正常运行范围而进入饱和区。一旦系统出现反向偏差,u(k)逐渐从饱和区退出。进入饱和区越深则退出饱和区时间越长。在这段时间里,执行机构仍然停留在极限位置而不随偏差反向而立即做出相应的改变,这时系统就像失控一样,造成控制性能恶化,这种现象称为积分饱和现象或积分失控现象。
防止积分饱和的方法之一就是抗积分饱和法,该方法的思路是在计算u(k)时,首先判断上一时刻的控制量u(k-1)是否已经超出了极限范围: 如果u(k-1)>umax,则只累加负偏差; 如果u(k-1)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65struct _pid{
float SetSpeed; //定义设定值
float ActualSpeed; //定义实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float voltage; //定义电压值(控制执行器的变量)
float integral; //定义积分值
float umax;
float umin;
}pid;
void PID_init(){
printf("PID_init begin \n");
pid.SetSpeed=0.0;
pid.ActualSpeed=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.voltage=0.0;
pid.integral=0.0;
pid.Kp=0.2;
pid.Ki=0.1; //注意,和上几次相比,这里加大了积分环节的值
pid.Kd=0.2;
pid.umax=400; //阈值[-200,400]
pid.umin=-200;
printf("PID_init end \n");
}
float PID_realize(float speed){
int index;
pid.SetSpeed=speed;
pid.err=pid.SetSpeed-pid.ActualSpeed;
if(pid.ActualSpeed>pid.umax) //超过正阈值
{
if(abs(pid.err)>200) //积分分离
index=0;
else
{
index=1;
if(pid.err<0) pid.integral+=pid.err; //超过正阈值了,只累积负偏差
}
}
else if(pid.ActualSpeed<pid.umin){ //超过负阈值
if(abs(pid.err)>200)
index=0;
else
{
index=1;
if(pid.err>0) pid.integral+=pid.err; //超过负阈值了,只累积正偏差
}
}
else{ //未超过阈值,正常计算
if(abs(pid.err)>200) //积分分离过程
index=0;
else{
index=1;
pid.integral+=pid.err; //正常累积
}
}
pid.voltage=pid.Kp*pid.err+index*pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
pid.err_last=pid.err;
pid.ActualSpeed=pid.voltage*1.0;
return pid.ActualSpeed;
}
PID控制算法的C语言实现七 梯形积分的PID控制算法C语言实现
先看一下梯形算法的积分环节公式(?)
作为PID控制律的积分项,其作用是消除余差,为了尽量减小余差,应提高积分项运算精度,为此可以将矩形积分改为梯形积分,具体实现的语句为:
pid.voltage=pid.Kppid.err+indexpid.Kipid.integral/2+pid.Kd(pid.err-pid.err_last);
最后运算的稳定数据为:199.999878,较教程六中的199.9999390而言,精度进一步提高。
PID控制算法的C语言实现八 变积分的PID控制算法C语言实现
变积分PID可以看成是积分分离的PID算法的更一般的形式。在普通的PID控制算法中,由于积分系数ki是常数,所以在整个控制过程中,积分增量是不变的。但是,系统对于积分项的要求是,系统偏差大时,积分作用应该减弱甚至是全无,而在偏差小时,则应该加强。积分系数取大了会产生超调,甚至积分饱和,取小了又不能短时间内消除静差。因此,根据系统的偏差大小改变积分速度是有必要的。
变积分PID的基本思想是设法改变积分项的累加速度,使其与偏差大小相对应:偏差越大,积分越慢; 偏差越小,积分越快。
这里给积分系数前加上一个比例值index:(相比于积分分离更加精确了)
当abs(err)<180时,index=1;
当180
最终的比例环节的比例系数值为ki*index;
具体PID实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 pid.Kp=0.4;
pid.Ki=0.2; //增加了积分系数
pid.Kd=0.2;
float PID_realize(float speed){
float index;
pid.SetSpeed=speed;
pid.err=pid.SetSpeed-pid.ActualSpeed;
if(abs(pid.err)>200) //变积分过程,调整I在pid不同阶段中的重要性
{
index=0.0;
}else if(abs(pid.err)<180){
index=1.0;
pid.integral+=pid.err;
}else{
index=(200-abs(pid.err))/20;
pid.integral+=pid.err;
}
pid.voltage=pid.Kp*pid.err+index*pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
pid.err_last=pid.err;
pid.ActualSpeed=pid.voltage*1.0;
return pid.ActualSpeed;
}
PID参数整定方法:
PID的整定方法很多,其效果在不同的系统里各有千秋,可以多尝试不同的方法整定,来达到最佳的整定结果。
1.知识储备:
比例增益Kp是指输出变化对偏差变化之比。而比例度δ则是指调节器的偏差值占输出值变化的百分比。这两种表示方法互为倒数关系,即:KP=1/δ。
Ki与Ti,Kd与Td的转化:Ki=KpT/Ti,Kd=KpTd/T
2.衰减曲线法:
本方法实际是临界比例度法一种变形,本方法操作简便,凑试时间较短。首先把Ki,Kd置零,用一个较大的Kp来纯比例作用系统,在比例度逐步减少的过程中,就会出现右图所示的过渡过程。
这时控制过程的比例度Kp称为n:1衰减比例度δs;两个波峰之间的距离称为n:1衰减周期Ts。而衰减曲线法就是在纯比例作用的控制系统中,求得衰减比例度δs和衰减周期Ts,并依据这两个数据和经验表格来得到PID的系数。具体地分为4:1和10:1衰减曲线法。
4:1衰减曲线法整定步骤:首先把Ki,Kd置零,Kp放至较大的适当值,使控制系统按纯比例作用的方式投入运行。然后慢慢地减少比例度,观察调节器的输出及控制过程的波动情况,直到找出4:1的衰减过程为止,并记下此时的δs(Kp)和Ts。
在部分调节系统中,由于采用4:1衰减比仍嫌振荡比较厉害,则可采用10:1的衰减过程。10:1衰减曲线法整定步骤:和4:1衰减曲线法在过程上完全一致,只是采用的整定参数和经验公式不同。此时所需的参数为衰减比例度δs和首次峰值加速时间Tr。
根据n:1选择衰减比例度s和衰减周期Ts、首次峰值加速时间Tr的经验表格计算
注意:网上给出了不同的经验表格,可以多次尝试选最优。表格3中的Ti,Td改成Ts。
3.临界比例度法
4.经验公式:
Kp调整为0.00100-0.01000之间
Ki调整在0.00050左右
Kd调整在0.00005-0.00020之间
RPM=15000:0.006 0.0001 0.00025
减小超调通过增大Kp,加快回落通过增大Ki,减少震荡通过增大Kd
④先将比例度放在一个比计算值大的数值上,然后加上积分时间Ti,再慢慢加上微分时间Td。操作时一定要按“PID序加参数”,即先P次I最后D,不要破坏了这个次序。
⑤把比例度降到计算值上,通过观察曲线,再作适当的调整各参数。即“观看运行细调整”,直到找出最佳值。
PID整定经验:
1.纯Kp情况下的曲线大致如此,此时可以看到所谓的衰减曲线法的衰减率。如果不是纯Kp下发生类似曲线,也可能是Kp过大或Kd过小
2.PID整定时出现这种状况(超调)通常是Ki过大导致的,此时可以调小Ki解决。此时也可以调大Kd,但可能对后续阶段产生影响。通常见于微小超调情况下,调小Ki有很好的效果。但由于Ki过小会导致静差,实在不行也可以调大Kp(因为Kp调大会间接增强Kd在上升段的影响,这是可以接受的。不要误以为Kp过大导致超调而调小Kp,实际上这是Kd来拒去留效果的表现)同理当反向超调时可以调大Ki等。
3.当长久情况下PID一直回不到预设值,此时是Ki过小无法控制静差导致的,应该调大Ki。此外,若是稳定后误差±过大,则应该调大Kd。
4.此时通常是因为Kp过小或是Kd过大导致的,体现在上升过程过慢且会有一个大而光滑的弧。此时需要调大Kp或调小Kd。
5.良好的PID曲线。快速上升,准确停止,稳态误差不大。