单片机之PID算法
简介
1.直接计算法和增量算法
这里的所谓增量算法就是相对于标准算法的相邻两次运算之差,得到的结果是增量,也就是说,在上一次的控制量的基础上需要增加(负值意味着减少)控制量,例如对于可控硅电机调速系统,就是可控硅的触发相位还需要提前(或迟后)的量,对于温度控制就是需要增加(或减少)加热比例,根据具体的应用适当选择采用哪一种算法,但基本的控制方法、原理是完全一样的,直接计算法得到的是当前需要的控制量,相邻两次控制量的差就是增量;
2.基本偏差e(t)
表示当前测量值与设定目标间的差,设定目标是被减数,结果可以是正或负,正数表示还没有达到,负数表示已经超过了设定值。这是面向比例项用的变动数据。
3.累计偏差
这是我们每一次测量到的偏差值的总和,这是代数和,考虑到他的正负符号的运算,这是面向积分项目的一个变动数据。
4.基本偏差的相对偏差:e(t)-e(t-1)
用本次的基本偏差减去上一次的基本偏差,用于考察当前控制的对象的趋势,作为快速反应的重要依据,这是面向微分项的一个变动数据。
5.三个基本参数:Kp,Ki,Kd.
这是做好一个控制器的关键常数,分别成为比例常数、积分常数和微分常数,不同的控制对象他们需要选择不同的数值,还需要经过现场调试才能获得较好的效果。
6.标准的直接计算法公式
上一次的计算值:
两式相减得到增量法计算公式:
*这里我们对∑项的表示应该是对e(i)从1到t全部总和,但为了打字的简便就记作∑e(t)。
三个基本参数Kp,Ki,Kd.在实际控制中的作用
比例调节作用: 是按比例反应系统的偏差,系统一旦出现了偏差,比例调节立即产生调节作用以减少偏差。比例作用大,可以加快调节,减少误差,但是过大的比例,使系统的稳定性下降,甚至造成系统的不稳定。
积分调节作用: 是使系统消除稳态误差,提高无差度。因为有误差,积分调节就进行,直至无差,积分调节停止,积分调节输出一常值。积分作用的强弱取决于积分时间常数Ti,Ti越小,积分作用就越强。反之Ti大则积分作用弱,加入积分调节可使系统稳定性下降,动态响应变慢。积分作用常与另两种调节规律结合,组成PI调节器或PID调节器。
微分调节作用: 微分作用反映系统偏差信号的变化率,具有预见性,能预见偏差变化的趋势,因此能产生超前的控制作用,在偏差还没有形成之前,已被微分调节作用消除。因此,可以改善系统的动态性能。在微分时间选择合适情况下,可以减少超调,减少调节时间。微分作用对噪声干扰有放大作用,因此过强的加微分调节,对系统抗干扰不利。此外,微分反应的是变化率,而当输入没有变化时,微分作用输出为零。微分作用不能单独使用,需要与另外两种调节规律相结合,组成PD或PID控制器。
参数的设定与调整
这是PID最困难的部分,编程时只设定他们的大概数值,然后通过反复的调试才能找到相对比较理想的参数值。面向不同的控制对象参数都不同,所以我们无法提供参考数值,但是我们可以根据这些参数在整个PID过程中的作用原理,来讨论我们的对策。
1.加温很迅速就达到了目标值,但是温度过冲很大:
a)比例系数太大,致使在未达到设定温度前加温比例过高;
b)微分系数过小,致使对对象反应不敏感;
2.加温经常达不到目标值,小于目标值的时间较多:
a)比例系数过小,加温比例不够;
b)积分系数过小,对恒偏差补偿不足;
3.基本上能够在控制目标上,但上下偏差偏大,经常波动:
a)微分系数过小,对即时变化反应不够快,反映措施不力;
b)积分系数过大,使微分反应被淹没钝化;
c)设定的基本定时周期过短,加热没有来得及传到测温点;
4.受工作环境影响较大,在稍有变动时就会引起温度的波动:
a)微分系数过小,对即时变化反应不够快,不能及时反映;
b)设定的基本定时周期过长,不能及时得到修正;
选择一个合适的时间常数很重要,要根据我们的输出单元采用什么器件来确定,如果是采用可控硅的,则可设定时间常数的范围很自由,如果采用继电器的则过于频繁的开关会影响继电器的使用寿命,所以就不太适合采用较短周期。一般的周期设定范围为1-10分钟较为合适。
为了调试方便,起码在调试阶段您必须编制一个可以对参数进行随时修改和记忆的接口,否则你会很辛苦,老是在现场与办公室之间来回跑。
关于自整定问题
在通用仪表行业用的比较多,因为他们的工作对象是不确定的,而不同的对象所使用的参数是千变万化的,所以无法为用户设定参数。这就引入了自整定的概念,用户首次使用的时候,启动自整定功能专门为新的工作对象寻找一套参数,并把他们记忆下来,作为今后工作的依据。其实自整定也就是作N次的测定,根据实际变化反应反复对参数进行修正,当达到一定要求后宣告自整定完成。这是一个多元素的优化问题,可以采用很多种优化方法,例如0.618黄金分割法就是一种很不错的算法,可以参考有关优化算法的书籍,我们这里就不做讨论了,需要说明的是,我们是对全部参数进行同时优化的,根据出现的不同情况去修正不同参数,我们的修正量将逐步缩小,当我们的修正量达到一定的范围以后,或者实测温度变化波动已经达到一定范围以后就可以认为自整定过程结束,记录最后结果返回正常控制状态。由于优化过程需要进行大量的试探,所以一次自整定过程往往需要花费很长时间,一般在半小时到两个小时左右。
修正的对策基本上就是上一节我们讨论的几种,对具体每一项参数的修正量,我们还可以赋以不同的比例参数,根据您认为的影响重要程度来确定,在0-1之间选取。
实际应用
说到PID算法,想必大部人并不陌生,PID算法在很多方面都有重要应用,比如电机的速度控制,恒温槽的温度控制,四轴飞行器的平衡控制等等,作为闭环控制系统中的一种重要算法,其优点和可实现性都成为人们的首选。下面简单来讲解一下PID算法:
首先PID算法是有比例,积分,微分三部分组成,先说下比例部分,所谓比例部分,就是呈线性关系,举个例子,一个电热丝加热水,开始的时候温度很低,离50℃很大,这时应该加大功率,离目标温度越大,其功率应该越大,反之越小,这就是比例部分。
乍一看,既然比例部分已经可以控制温度了为啥还需要积分和微分部分呢,难道是多此一举么?其实不然,在实际中会出现这种情况,当加热到50℃时,系统很难停止下来,而是会持续一段时间,这样就会超过预设值,所以仅有比例控制并不完美,这是就需要积分部分和微分部分。积分部分就是把之前的误差全部累加起来,这样起始时由于误差很大加热功率就大,随着接近预设值后功率开始减少,微分部分就是起始时温度增加很快,表示此时需要很大的功率,随着温度接近预设值,其斜率开始减小最后为零,意味着功率也减少,当然很难为零,一般在一定的范围内波动。
现在开始用C语言来实现PID算法:
位置式:
比例部分:
Kp:比例系数 SetValue:预设值 FactValue:当前实际值 Error_1:当前误差
则比例部分为:
Sp = Kp*(SetValue - FactValue)
或者
Sp = Kp*Error_1
注解:Sp大小反应需要控制的量大小,比如Sp越大,功率越大。当Sp为负值时,表示要超过预设值,如果是电机,则需要反转
积分部分:
Ki:积分系数 Error_1:当前误差 Error_2:上一次误差 Error_3:上上一次误差 …Error_n:开始时的误差
则积分部分为:
Si = Ki*(Error_1+Error_2+Error_3+…+Error_n)
注解:因为整个是一个过程,所以上一次误差其实就是上一次的当前误差
微分部分:
Kd:微分系数 Error_1:当前误差 Error_2:上一次误差
则微分部分为:
Sd = Kd*(Error_1-Error_2)
综上部分的PID得:
PID=Sp + Si + Sd = KpError_1 + Ki(Error_1+Error_2+Error_3+…+Error_n) + Kd*(Error_1-Error_2)
增量式:
将上述推导的PID记作时间为k时刻的PID控制量,则
PID(k) =Sp + Si + Sd = KpError_1(k) + Ki(Error_1(k)+Error_2(k-1)+Error_3(k-2)+…+Error_n(0)) + Kd*(Error_1(k)-Error_2(k-1)) 1
将上式k=k-1代入得:
PID(k-1) =Sp + Si + Sd = KpError_1(k-1) + Ki(Error_1(k-1)+Error_2(k-2)+Error_3(k-3)+…+Error_n(0)) + Kd*(Error_1(k-1)-Error_2(k-2)) 2
1-2得:
PID(k) - PID(k-1) = Kp*(Error_1(k)-Error_1(k-1)) + Ki*(Error_1(k)) + Kd*(Error_1(k)-2*Error_2(k-1)+Error_2(k-2))
将PID(k) - PID(k-1)记作detPID
detPID = Kp*(Error_1(k)-Error_1(k-1)) + Ki*(Error_1(k)) + Kd*(Error_1(k)-2*Error_2(k-1)+Error_2(k-2))
这样就得到了增量式的PID算法,其计算的结果为增加的控制量
增量式的PID有个好处就是只与当前三个误差量有关系,与其他无关,这样就简化的处理过程,而且提高了精度,下面是PID源码:
/*文件名:PID.h*/
#ifndef _PID_H_
#define _PID_H_
extern float Kp,Ki,Kd; //系数(全局变量)
extern float AclValue; //实际值
extern float SetValue;
int PID(void);
#endif
/*########################################################################
文件名:PID.c
时间: 2018.9.7
备注:无
#########################################################################*/
#include "PID.h"
float Kp=10,Ki=0.8,Kd=0.5; //系数
float SetValue=2000; //设定值
float AclValue=0; //实际
float Error1=0,Error2=0,Error3=0; //误差
/* 下面为增量式PID算法 */
/**********************************************************************************
函数名:PID
返回值:输出增量
参数:无
备注:当输出大于0表示小于预设值,当输出小于0表示大于预设值
***********************************************************************************/
int PID(void)
{
float OutValue =0;
Error3 = SetValue - AclValue;
OutValue = Kp*(Error3-Error2)+Ki*(Error3)+Kd*(Error3-2*Error2+Error1);
Error1=Error2; //这部分是迭代,因为上次的误差就是上次的当前误差
Error2=Error3;
if(OutValue>3000) //这部分是规定最大输出增量
OutValue=3000;
if(OutValue<-3000)
OutValue=-3000;
return OutValue;
}
下面给出计算机模拟代码;
#include "stdio.h"
float Kp=10,Ki=2,Kd=0.5; //系数
float SetValue=1256; //设定值
float AclValue=0; //实际
float Error1=0,Error2=0,Error3=0; //误差
/* 下面为增量式PID算法 */
/**********************************************************************************
函数名:PID
返回值:输出增量
参数:无
备注:当输出大于0表示小于预设值,当输出小于0表示大于预设值
***********************************************************************************/
int PID(void)
{
float OutValue =0;
Error3 = SetValue - AclValue;
OutValue = Kp*(Error3-Error2)+Ki*(Error3)+Kd*(Error3-2*Error2+Error1);
Error1=Error2;
Error2=Error3;
return OutValue;
}
int main(void)
{
unsigned int i=1000;
while(i)
{
PID(); //特别注意这里:必须要运行,因为需要执行这一步:Error1=Error2; Error2=Error3; printf("当前实际值为:%f \n",AclValue); AclValue += PID(); i--);
} return 0;}
参考代码
PID算法(c 语言)(来自老外),位置式PID
#include <stdio.h>
#include<math.h>
//定义PID 的结构体
struct _pid
{
int pv; // integer that contains the process value 过程量
int sp; // integer that contains the set point 设定值
float integral; // 积分值 -- 偏差累计值
float pgain;
float igain;
float dgain;
int deadband; //死区
int last_error;
};
struct _pid warm, *pid;
int process_point, set_point, dead_band;
float p_gain, i_gain, d_gain, integral_val, new_integ;
/*
pid_init --- This function initializes the
pointers in the _pid structure to the process variable
and the setpoint. *pv and *sp are integer pointers.
*/
void pid_init( struct _pid *warm, int process_point, int set_point )
{
struct _pid *pid;
pid = warm;
pid->pv = process_point;
pid->sp = set_point;
}
/*pid_tune --- Sets the proportional gain (p_gain), integral gain (i_gain),
derivitive gain (d_gain), and the dead band (dead_band)
of a pid control structure _pid.
设定PID参数 ---- P,I,D,死区
*/
void pid_tune( struct _pid *pid,
float p_gain,
float i_gain,
float d_gain,
int dead_band )
{
pid->pgain = p_gain;
pid->igain = i_gain;
pid->dgain = d_gain;
pid->deadband = dead_band;
pid->integral = integral_val;
pid->last_error = 0;
}
/*
pid_setinteg --- Set a new value for the integral term of the pid equation.
This is useful for setting the initial output of the pid controller at start up.
设定输出初始值
*/
void pid_setinteg( struct _pid *pid, float new_integ )
{
pid->integral = new_integ;
pid->last_error = 0;
}
/*
pid_bumpless --- Bumpless transfer algorithim.
When suddenly changing setpoints, or when restarting the PID equation
after an extended pause, the derivative of the equation can cause a bump
in the controller output. This function will help smooth out that bump.
The process value in *pv should be the updated just before this function is used.
pid_bumpless 实现无扰切换
当突然改变设定值时,或重新启动后,将引起扰动输出。这
个函数将能实现平顺扰动, 在调用该函数之前需要先更新 PV值
*/
void pid_bumpless( struct _pid *pid )
{
pid->last_error = ( pid->sp ) - ( pid->pv ); //设定值与反馈值偏差
}
/*
pid_calc --- Performs PID calculations for the _pid structure * a.
This function uses the positional form of the pid
equation, and incorporates an integral windup prevention algorithim.
Rectangular integration is used, so this function must
be repeated on a consistent time basis for accurate control.
RETURN VALUE The new output value for the pid loop. USAGE #include "control.h"
本函数使用位置式PID计算方式,并且采取了积分饱和限制运算
PID计算
*/
float pid_calc( struct _pid *pid )
{
int err;
float pterm, dterm, result, ferror;
// 计算偏差
err = ( pid->sp ) - ( pid->pv );
// 判断是否大于死区
if ( abs( err ) > pid->deadband )
{
ferror = (float) err; //do integer to float conversion only once 数据类型转换
// 比例项
pterm = pid->pgain * ferror;
if ( pterm > 100 || pterm < -100 )
{
pid->integral = 0.0;
}
else
{
// 积分项
pid->integral += pid->igain * ferror;
// 输出为0--100%
// 如果计算结果大于100,则等于100
if ( pid->integral > 100.0 )
{
pid->integral = 100.0;
}
// 如果计算结果小于0.0,则等于0
else if ( pid->integral < 0.0 )
pid->integral = 0.0;
}
// 微分项
dterm = ( (float) ( err - pid->last_error ) ) * pid->dgain;
result = pterm + pid->integral + dterm;
}
else
result = pid->integral; // 在死区范围内,保持现有输出
// 保存上次偏差
pid->last_error = err;
// 输出PID值(0-100)
return ( result );
}
void main( void )
{
float display_value;
int count = 0;
pid = &warm;
// printf("Enter the values of Process point, Set point, P gain, I gain, D gain \n");
// scanf("%d%d%f%f%f", &process_point, &set_point,&p_gain, &i_gain, &d_gain);
// 初始化参数
process_point = 30;
set_point = 40;
p_gain = (float) ( 5.2 );
i_gain = (float) ( 0.77 );
d_gain = (float) ( 0.18 );
dead_band = 2;
integral_val = (float) ( 0.01 );
printf( "The values of Process point, Set point, P gain, I gain, D gain \n" );
printf( " %6d %6d %4f %4f %4f\n", process_point, set_point, p_gain, i_gain, d_gain );
printf( "Enter the values of Process point\n" );
while ( count <= 20 )
{
scanf( "%d", &process_point );
// 设定PV,SP 值
pid_init( &warm, process_point, set_point );
// 初始化PID 参数值
pid_tune( &warm, p_gain, i_gain, d_gain, dead_band );
// 初始化PID 输出值
pid_setinteg( &warm, 0.0 );
//pid_setinteg(&warm,30.0);
//Get input value for process point
pid_bumpless( &warm );
// how to display output
display_value = pid_calc( &warm );
printf( "%f\n", display_value );
//printf("\n%f%f%f%f",warm.pv,warm.sp,warm.igain,warm.dgain);
count++;
}
}
增量式PID公式.如下:
△u=Kp*(e0-e1) + Kie0 + Kd(e0 - 2*e1 + e2);
u += △u
e0为本次偏差; e1是上次偏差; e2是上上次偏差。u为本次输出。
其他参考
下面给出公式直接体现的C语言源代码(请结合项目修改源代码):
位置式PID
typedef struct
{
float Kp; //比例系数Proportional
float Ki; //积分系数Integral
float Kd; //微分系数Derivative
float Ek; //当前误差
float Ek1; //前一次误差 e(k-1)
float Ek2; //再前一次误差 e(k-2)
float LocSum; //累计积分位置
}PID_LocTypeDef;
/************************************************
函数名称 : PID_Loc
功 能 : PID位置(Location)计算
参 数 : SetValue ------ 设置值(期望值)
ActualValue --- 实际值(反馈值)
PID ----------- PID数据结构
返 回 值 : PIDLoc -------- PID位置
作 者 : strongerHuang
*************************************************/
float PID_Loc(float SetValue, float ActualValue, PID_LocTypeDef *PID)
{
float PIDLoc; //位置
PID->Ek = SetValue - ActualValue;
PID->LocSum += PID->Ek; //累计误差
PIDLoc = PID->Kp * PID->Ek + (PID->Ki * PID->LocSum) + PID->Kd * (PID->Ek1 - PID->Ek);
PID->Ek1 = PID->Ek; return PIDLoc;
}
增量式PID
△u(k) = u(k) – u(k-1)
= Kp[ e(k) – e(k-1)] + Ki*e(k) + Kd[e(k) – 2e(k-1) + e(k-2)]
typedef struct
{
float Kp; //比例系数Proportional
float Ki; //积分系数Integral
float Kd; //微分系数Derivative
float Ek; //当前误差
float Ek1; //前一次误差 e(k-1)
float Ek2; //再前一次误差 e(k-2)
}PID_IncTypeDef;
/************************************************
函数名称 : PID_Inc
功 能 : PID增量(Increment)计算
参 数 : SetValue ------ 设置值(期望值)
ActualValue --- 实际值(反馈值)
PID ----------- PID数据结构
返 回 值 : PIDInc -------- 本次PID增量(+/-)
作 者 : strongerHuang
*************************************************/
float PID_Inc(float SetValue, float ActualValue, PID_IncTypeDef *PID)
{
float PIDInc; //增量
PID->Ek = SetValue - ActualValue;
PIDInc = (PID->Kp * PID->Ek) - (PID->Ki * PID->Ek1) + (PID->Kd * PID->Ek2);
PID->Ek2 = PID->Ek1;
PID->Ek1 = PID->Ek; return PIDInc;
}
资料来源:
https://www.cnblogs.com/listenscience/p/9648374.html
https://www.sohu.com/a/133343611_488169
https://blog.csdn.net/weixin_30425949/article/details/96938793