惯性环节:PID控制以及各种控制环节-C语言实现

1 典型环节的微分方程、传递函数及C语言实现方法

无论多么复杂的系统,总是可以由简单的子系统构成,分析典型环节的特点,其目的是为了通过典型环节的特点分析更为复杂的系统,实际工程应用中,真正完全通过理论的方式建立模型是非常困难的,实际的模型建立过程是一个复杂的过程,需要通过假设、验证、参数实验给定等多种手段分析完善模型内容,利用实验获取模型的方法又称作系统辨识技术,在下一节中将重点讲解。假设的过程,其实就是根据系统特点,综合典型环节的过程。将不同的环节通过四则运算给予不同的参数进行运算,从而接近真实的模型。从这个角度上讲,透彻理解典型环节的特点,无论对于理论分析系统,还是对于实际建立系统模型都具有重要的意义。

1.1 比例环节

比例环节是自然界普遍存在的一个环节,几乎所有的系统必定存在比例环节。比例环节的特点在于输入输出量成正比例关系,没有失真与延时。其微分方程可表示为:c(t) = kr(t)。其传递函数可表示为:G(s)=c(s)/R(s)。
对于比例环节而言,C语言实现相对比较简单。下面进行C语言实现过程的说明。

float ProElement(float K, float GiveValue)
{
float result;
result = K*GiveValue;
return result;
}

以上代码用C语言实现了比例环节的处理。使用时直接定义一全局变量,例如float ResultValue; 然后直接调用该函数即可,例如 ResultValue = ProElement(0.1, 10);返回的ResultValue = 1.0。

1.2 惯性环节

惯性环节是自然界普遍存在的另一个环节,其存在的广泛性不亚于比例环节,任何系统,只有时间精度足够高,都必然存在一定的惯性性能。惯性环节的特点是对变化的输入量,输出量不能立刻复现,或多或少的存在一定的延时,在延时的时间内,输出量会逐渐接近输入的给定值。其微分方程可表示为:
T ( d c ( t ) / d t ) + c ( t ) = r ( t ) T(dc(t)/dt)+c(t) = r(t) T(dc(t)/dt)+c(t)=r(t)其中T为惯性时间常数,T越大,惯性越大,当延时时间约为3-4倍的T时,输出接近输入给定值。其传递函数可表示为: G ( s ) = 1 / ( T s + 1 ) G(s)=1/(Ts+1) G(s)=1/(Ts+1)下面讲解用C语言实现惯性环节的过程。这里注意系统的输出为c(t),输入为r(t)。首先将微分方程转化为差分方程的形式 T ( c ( t ) − c ( t − 1 ) / t − ( t − 1 ) ) + c ( t ) = r ( t ) T(c(t)-c(t-1)/t-(t-1))+c(t)=r(t) T(c(t)−c(t−1)/t−(t−1))+c(t)=r(t)化简该公式可表示为: c ( t ) = ( r ( t ) + T c ( t − 1 ) ) / ( T + 1 ) c(t)=(r(t)+Tc(t-1))/(T+1) c(t)=(r(t)+Tc(t−1))/(T+1)上述公式表明,当前时刻输出量c(t)与上一时刻输出量c(t-1)相关。利用该原理,可使用C语言通过迭代函数实现。首先定于全局变量,存放上一时刻输出量的值float ResultValueBack。初值设定为0,然后可通过下面函数实现迭代过程

float InertialElement(float T, float GiveValue)
{
float result;
result = (T*ResultValueBack + GiveValue)/(1+T);
ResultValueBack = result;
return result;
}

1.3积分环节

积分环节是设计校正系统是常用的一个环节,经典的PID算法就分别用到了比例、积分、微分三个环节。积分环节的显著特点是,输出量与输入量的积分成正比例关系,当输入量消失后,输出量具有记忆功能,能够储存部分能量。其微分方程可表示为: c ( t ) = I n t e g r a l ( r ( d ) d t ) c(t)=Integral(r(d)dt) c(t)=Integral(r(d)dt)传递函数可以表示为: G ( s ) = 1 / s G(s) = 1/s G(s)=1/s对于离散系统而言,积分过程实质上是系统输入量的累加和,用C语言实现积分过程可表示为:
定义全局变量ResultValue

float IntegralElement(float GiveValue)
{
float result;
ResultValue = ResultValue + GiveValue;
result = ResultValue;
return result;
}

1.4微分环节

微分环节也经常运用到校正环节中,如果被控系统存在较大的惯性环节,可考虑校正环节中加入微分环节,微分环节关注的是给定变化率的特性,所以在一定程度上具有系统预测能力。微分环节根据微分阶数的不同分为一阶微分方程、二阶微分方程及多阶微分方程。这里我们只用C语言实现一阶和二阶微分方程,多阶微分方程可根据实际需要,读者利用本书提到的基本方法自行实现。微分环节的微分方程可表示为,一阶: c ( t ) = d r ( t ) d t c(t)=dr(t)dt c(t)=dr(t)dt二阶: c ( t ) = T d r ( t ) / d t + r ( t ) c(t)=Tdr(t)/dt + r(t) c(t)=Tdr(t)/dt+r(t)其传递函数可表示为,一阶: G ( s ) = s G(s)=s G(s)=s二阶: G ( s ) = T s + 1 G(s)=Ts+1 G(s)=Ts+1
下面是C语言实现一阶微分方程与二阶微分方程的过程。定义全局变量GiveValueBack

float DervativeElementOne(float GiveValue) //一阶微分实现
{
float result;
result = GiveValue - GiveValueBack;
GiveValueBack = GiveValue;
return result;
}
float DervativeElementTwo(float T, float GiveValue)//二阶微分实现
{
float result;
result = (T+1) GiveValue - GiveValueBack;
GiveValueBack = GiveValue;
return result;
}

1.5 滞后环节

滞后环节不同与惯性环节,滞后环节的特点是输入量给定经过一段延时后,输出信号完全复现输入信号。惯性环节则是从输入量给定开始输出量便逐渐趋近于输入量。读者要理解清楚这两个环节的不同。滞后环节的微分方程不易表示,但是C语言实现相对简单,只需要延时赋值即可。

float DelayElement(float Time, float GiveValue) //Time表示延时时间
{
float result;
if( T > Time)
{
result = GiveValue;
}else{
result = 0;
}
return result;
}

震荡环节在平常的控制设计中并不常用,这里暂不做详细介绍

1.6 小结

以上5小节,详细论述了典型环节的微分方程,传递函数,及C语言实现的方法步骤。需要反复强调的是,用C语言实现系统模型,首先将系统模型的传递函数转化为微分方程的形式,然后再将微分方程转化为差分方程的形式,最后根据差分方程表达式用C语言函数实现。这是控制方法用C语言实现的核心所在。为什么要讲述典型环节的,因为无论多么复杂的系统,也无论多么困难的系统,其无非是几种典型环节的组合与排列的过程。只有弄清楚基本的典型环节,才能在后续的问题分析与解决中游刃有余。


PID控制及其C语言实现

PID校正控制方式是工程应用中使用最为广泛的一种线性系统控制方法。其核心思想是利用比例、积分、微分三个环节作为校正环节,提高系统的响应速度、稳定性与准确性。其中比例环节能够成比例的反映误差信号,误差信号一旦产生,比例环节立即产生控制作用,以减少变差;积分环节能够有效的消除系统的静态误差,在系统控制过程中,只要存在误差,则积分一直持续,直至误差完全消除,积分环节的强弱直接决定了系统消除误差时间的快慢;微分环节反映了偏差信号的变化趋势,具有一定的预测功能,能够在偏差信号变化太大之前引入一个有效的早期修正信号,加快系统的动作速度,减少调节时间。

2.1基本PID控制原理及实现

在工业应用中,最常用的控制方式是PID控制,PID控制框图如图所示。


PID是一种线性控制方法,其运用给定值R(t)与实际输出值C(t)的偏差e(t)作为控制量进行控制。其控制规律的微分方程可表示为
U ( t ) = K p e ( t ) + K i ∫ e ( t ) d t + K d d e ( t ) d t U(t)=K_pe(t)+K_i\int e(t)dt + K_d\frac{de(t)}{dt} U(t)=Kp​e(t)+Ki​∫e(t)dt+Kd​dtde(t)​
其中Kp为比例系数、Ki为积分系数、Kd为微分系数。这里需要注意的是,并非系统最终输出,而是被控对象之前节点的值。举个例子说明,假如控制电机以某一转速运行,通过PID运算得到的值可能是驱动桥电路的PWM开通值,而并非电机的转速值,PWM开通后作用到电机上才能最终转化到转速上。下面将微分方程转化为差分方程,再用C语言实现。
这里我们采用纯惯性环节作为被控对象,用C语言设计最一般的PID算法,并进行仿真验证。PID的C语言实现过程如下

float PID(float Kp, float Ki, float Kd, float GiveValue, float ActualValue)
{
float result;
float Err,KpWork, KiWork, KdWork;
Err = GiveValue - ActualValue;
KpWork = KpErr;
KiWork = KiPIDErrADD;
KdWork = Kd*(Err-ErrBack);
result = KpWork+KiWork+KdWork;
PIDErrADD = PIDErrADD + Err;
ErrBack = Err;
return result;
}
取Kp = 4.5,Ki = 0.5,Kd = 0.1,给定值为阶跃信号100.2,惯性环节的惯性系数取3。最终输出的数据取前100个点波形如图所示

一般PID惯性环节仿真数据图

积分分离PID控制实现

控制环节中引入积分环节的目的,主要是为了消除静态误差,提高控制的精度。但是,在实际应用中,当系统处于启动、结束或大幅增减设定的过程中时,短时间内系统输出有很大的偏差,如此便会在短时间内产生较大的积分累计,导致控制量超过执行机构可能允许的最大动作范围对应的极限控制量,进而引起系统较大超调,甚至造成系统的震荡。
引入积分分离的目的在于解决上述问题,其基本思想是,当控制量接近给定值时,引入积分控制,消除静态误差;当控制量与给定值相差较大时,取消积分作用,避免积分累加和过大造成的系统不稳定因素增加。
这里我们采用纯惯性环节作为被控对象,用C语言设计积分分离的PID算法,并进行仿真验证。PID的C语言实现过程如下

float SeqIntPID(float Kp, float Ki, float Kd, float GiveValue, float ActualValue)
{
float result;
float Err,KpWork, KiWork, KdWork;
Err = GiveValue - ActualValue;
KpWork = KpErr;
KiWork = KiSeqIntPIDErrADD;
KdWork = Kd*(Err-SeqIntErrBack);
if(fabs(Err) > 100)
{
result = KpWork+KdWork;
}else{
result = KpWork+KiWork+KdWork;
}
SeqIntPIDErrADD = SeqIntPIDErrADD + Err;
SeqIntErrBack = Err;
return result;
}

取Kp = 4.5,Ki = 0.5,Kd = 0.1,给定值为阶跃信号100.2,惯性环节的惯性系数取30。最终输出的数据取前100个点波形如图所示


积分分离PID惯性环节仿真数据图

2.3抗积分饱和PID控制实现

实际系统的积分项饱和是指,在实际应用系统中,由于误差在一段较长的时间内在一个方向上积累,造成积分的累计误差和过大,从而造成整个积分环节的值过大,当系统趋于稳定或者超调过冲的时候,比例项与微分项的调节作用相对减弱,从而减慢调节速度,甚至长时间不能够到达给定值。
比如对于一个较大的惯性系统,给定系统一个值后,当控制系统极限输出时,惯性系统仍然需要一段时间消除误差,那么在这段时间内容,正常误差一直在累加,等到输出值大于给定值时,由于前面一段时间累积的误差和太大,从而不能很快的通过误差调整过来。图2-4说明了积分饱和的概念。


积分饱和说明图
u为控制器输出,X为执行机构的开度,若系统存在一个方向的偏差,PID控制器的输出由于积分作用累积,导致执行机构达到极限位置,如果此时控制器输出u继续增大,执行器的实际开度已经不可能再增大,此时计算机输出的控制量超出了正常运行的范围而进入饱和状态,一旦系统出现反向偏差,控制器的输出u会逐渐的退出饱和区。但是进入饱和区越深,退出所需时间越久,从而造成系统的失控。为了防止积分项饱和,需要对积分项进行限制。实际当中常用的限制方法方法有两种:
方法一:直接限制积分累加和最大值。所谓直接限制积分累加和是指通过限制积分累加和最大值的方式限制积分项的值使其不能过于深入的进入饱和状态。比如稳定状态下积分累加和的值是100,那么可以将积分累加和的上下限分别限定到+110与-110,但是此值的选择需要斟酌,此值选取太小,容易造成系统调整速度慢,甚至不能调整到给定值,此值限制的太大,起不到抗饱和积分的作用。在实际应用中,此种方法以其操作简单被广泛用到。
方法二:动态限制积分累加和。该方法的思路是在计算本次输出u(k)时,首先判断上一时刻的控制量u(k-1)是否已经超出限制范围;若超出最大限值范围,则只累加负偏差;反之则只累加正偏差。这种算法可以避免控制量长时间停留在饱和区。
以上两种方法,对于方法一用C语言实现较为简便,不再赘述,本处着重对第二种方法进行C语言实现。

float OverIntPID(float Kp, float Ki, float Kd, float GiveValue, float ActualValue)
{
float result;
float Err,KpWork, KiWork, KdWork;
Err = GiveValue - ActualValue;
if(OverIntResultBack > 120)
{
if(Err < 0)
{
OverIntPIDErrADD = OverIntPIDErrADD + Err;
}
}else if(OverIntResultBack < 120){
if(Err > 0)
{
OverIntPIDErrADD = OverIntPIDErrADD + Err;
}
}else{
OverIntPIDErrADD = OverIntPIDErrADD + Err;
}
KpWork = KpErr;
KiWork = KiOverIntPIDErrADD;
KdWork = Kd*(Err-OverIntErrBack);
result = KpWork+KiWork+KdWork;
OverIntErrBack = Err;
OverIntResultBack = result;
return result;
}

取Kp = 4.5,Ki = 0.5,Kd = 0.1,给定值为阶跃信号100.2,惯性环节的惯性系数取30。最终输出的数据取前100个点波形如图所示:

抗饱和积分PID惯性环节仿真数据图

2.6 其他PID控制实现

PID的在工业上广泛的应用,具体使用过程中会针对不同的控制系统研究选择不同的PID控制形式,PID实际应用中多种多样,上述五个小节中讲述了五种PID的实现形式,实际的工业应用中常用的PID还有带前馈的PID、基于滤波器的PID以及智能PID等其他使用形式,读者需掌握系统与控制校正环节的分析方法,灵活运用,实现适合自身系统的PID控制器。

/*积分分离的pid控制算法c语言实现系统所用时间是原来时间的一半系统的快速性得到了提高*/ #include<stdio.h>#include<stdlib.h> struct _pid{float SetSpeed; //定义设定值//24V 1100-1900float ActualSpeed; //定义实际值float err; //定义偏差值float err_last; //定义上一个偏差值float Kp,Ki,Kd; //定义比例、积分、微分系数float voltage; //定义电压值(控制执行器的变量)float integral; //定义积分值}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.04;//自己设定pid.Kd=0.2;//自己设定printf("PID_init end \n");} float PID_realize(float speed){pid.SetSpeed=speed;//设定值pid.err=pid.SetSpeed-pid.ActualSpeed;//设定值-实际值int index;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;//返回} int main(){PID_init();int count=0;while(count<1000){float speed=PID_realize(200.0);printf("%f\n",speed);count++;}return 0;} /*抗击分饱和的pid控制算法*/ #include<stdio.h>#include<stdlib.h>struct _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;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.voltage=pid.Kp*pid.err+index*pid.Ki*pid.integral/2+pid.Kd*(pid.err-pid.err_last);//梯形积分pid.err_last=pid.err;pid.ActualSpeed=pid.voltage*1.0;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;}

温度控制的C语言实现

/*功能:用软件模拟PID控制温度模型一:夏天要求室内的温度为23摄氏度左右,假定房间受室外高温影响,1分钟上升0.1摄氏度(上限为室外温度37摄氏度)。释放一包制冷剂可降低5摄氏度,一包制热剂,可上升五度,温度变化由传感器采集,但是我用软件模拟温度变化,假设一切处于理想状态。*/ #include "SoftSimulation_PID_Header.h" int main(){init_PID(); time_t timep;struct tm *tp;while (1){if (pid.ActualTemperature < outdoorTemperature){// 获取时间time(&timep);tp = gmtime(&timep); AcquireTemperature(tp->tm_hour,tp->tm_min,tp->tm_sec);} PID_controlTemperature(); /* 当温度处于人体舒适的温度时,则不用制冷或加热 */while (fabs(pid.SetTemperature - pid.ActualTemperature) < ComfortTemperature){// 获取时间time(&timep);tp = gmtime(&timep);// 获取计算机时间,10秒钟打印一次printf("%d/%d/%d %d/%d/%d\n",1900+tp->tm_year,(1+tp->tm_mon),tp->tm_mday,tp->tm_hour + 8, tp->tm_min,tp->tm_sec);printf("Display:设定温度为:%.2f\t室内温度为:%f\t\n\n", pid.SetTemperature, pid.ActualTemperature);// 获取实时温度AcquireTemperature(tp->tm_hour, tp->tm_min, tp->tm_sec);Sleep(500);}}} /* Acquire indoor Temperature */void AcquireTemperature(int hour,int minute,int second){static int lastTime[3] = { 0 };static float Minute = 0;if (!lastTimeFlag ){lastTime[0] = hour;lastTime[1] = minute;lastTime[2] = second;lastTimeFlag = 1;}else{if (pid.ActualTemperature <= outdoorTemperature){Minute = (hour - lastTime[0]) * 60 + (minute - lastTime[1]) + float(second - lastTime[2]) / 60.0;pid.ActualTemperature = pid.ActualTemperature + Minute * IncrementTemperature;lastTime[0] = hour;lastTime[1] = minute;lastTime[2] = second;//printf("Test:设定温度为:%.2f\t室内温度为:%f\t\n\n", pid.SetTemperature, pid.ActualTemperature);}else{// 温度升高过快,只记录时间,程序会在PID_controlTemperature函数中处理lastTime[0] = hour;lastTime[1] = minute;lastTime[2] = second;}}} /* release the refrigeration */void Refrigeration(){pid.ActualTemperature = pid.ActualTemperature - 5;}/* release the heating */void Heating(){pid.ActualTemperature = pid.ActualTemperature + 5;} /* the PID arithmetic control the temperature of indoor */void PID_controlTemperature(){if (pid.SetTemperature >= pid.ActualTemperature + TemperatureErr){// 室内温度偏低,调用制热模块Heating();}// 室内温度处于23度左右,使用PID精调else if (fabs(pid.SetTemperature - pid.ActualTemperature) < TemperatureErr){IncrementPID_realize(); // 增量式PID控制//PositionPID_realize(); // 位置式PID控制,两者只要一个就可以了,之所以都贴出来,为了试试两种方式有什么不同}else if (pid.SetTemperature + TemperatureErr <= pid.ActualTemperature) {// 室内温度偏高,调用制冷模块Refrigeration();}else{printf("区间考虑不周全\n");} } /* initialize PID */void init_PID(){pid.Kp = 0.2;pid.Ki = 0.015;pid.Kd = 0.2;pid.SetTemperature = 23.0;pid.ActualTemperature = outdoorTemperature;lastTimeFlag = 0;pid.ErrVal[0] = 0;pid.ErrVal[1] = 0;pid.ErrVal[2] = 0;pid.Integral = 0; printf("accomplish initialize PID \n");} /* 功能:使用增量式PID调节 公式:U(k)+Kp*[E(k)-E(k-1)]+Ki*E(k)+Kd*[E(k)-2E(k-1)+E(k-2)] 备注:网上的公式Kp乘以的东西有两种,第一种是Kp*[E(k)-E(k-1)],第二种是Kp*([E(k)-E(k-1)]+Ki*E(k)+Kd*[E(k)-2E(k-1)+E(k-2)]) 这两种我都测试过,都能实现功能,前者更快一些,后者慢,但是我个人喜欢第二种*/void IncrementPID_realize(){pid.ErrVal[0] = pid.SetTemperature - pid.ActualTemperature;// 计算设定值与实际值直接的误差float Temp0 = pid.Kp * (pid.ErrVal[0] - pid.ErrVal[1]);//float Temp0 = (pid.ErrVal[0] - pid.ErrVal[1]);float Temp1 = pid.Ki * pid.ErrVal[0];float Temp2 = pid.Kd * (pid.ErrVal[0] - 2 * pid.ErrVal[1] + pid.ErrVal[2]);float Increment = Temp0 + Temp1 + Temp2;// 通过公式计算增量// float Increment = pid.Kp * (Temp0 + Temp1 + Temp2);// 通过公式计算增量pid.ActualTemperature += Increment;// 计算实际温度pid.ErrVal[1] = pid.ErrVal[0];pid.ErrVal[2] = pid.ErrVal[1];//printf("Realize:设定温度为:%.2f\t室内温度为:%.2f\t\n\n", pid.SetTemperature, pid.ActualTemperature);} /*功能:使用位置式PID调节公式:Kp*(E(k)+Ki*∑E(j)+Kd*[E(k)-E(k-1)]) 备注:参考增量式的备注*/void PositionPID_realize(){pid.ErrVal[0] = pid.SetTemperature - pid.ActualTemperature;// 计算设定值与实际值直接的误差pid.Integral += pid.ErrVal[0];pid.ActualTemperature = pid.Kp * pid.ErrVal[0] + pid.Ki * pid.Integral + pid.Kd * (pid.ErrVal[0] - pid.ErrVal[1]);pid.ErrVal[1] = pid.ErrVal[0];}

头文件如下:

#include <stdio.h>#include<stdlib.h>#include<math.h>#include<time.h>#include<windows.h> typedef struct PID_Value{float ErrVal[3];// 三次误差值float Kp;// 比例系数float Ki;// integral 系数float Kd;// derivative 系数float Integral;// 积分float SetTemperature;float ActualTemperature;}PID_ValueStr,*PID_ValueT; PID_Value pid; #defineoutdoorTemperature36.2#defineTemperatureErr3#defineComfortTemperature0.5#defineIncrementTemperature1// 每分钟房间上升的温度 int lastTimeFlag = 0; void AcquireTemperature(int hour, int minute, int second);void PID_controlTemperature();void Refrigeration();void init_PID();void IncrementPID_realize();void PositionPID_realize();

相关推荐

相关文章