PID算法详解

控制算法

所谓控制就是把当前所控制的对象的状态控制为我们设定的目标值,或者尽可能的接近,例如:一个温度控制系统中,我想控制水温在100摄氏度,100摄氏度就是目标值,我们需要把当前温度不断地接近目标值100摄氏度。

传统的控制算法

SV是我们设定的值,PV是对象当前的值,将SV和PV同时送入特定电路或者算法中,利用控制算法对SV和PV进行分析、判断、处理,从而得出一个输出信号OUT,将OUT加到执行机构中,去产生预期的控制效果。

位式控制算法

这种控制算法输出的控制量只有高低电平两种状态,即通电和断电,当当前值PV低于目标值SV时,控制对象就全额工作,反之就停止工作,例如说控制对象是一个500W的加热棒,温度不到目标值就满功率500W工作,如果到达目标值就直接断电。

这种算法存在很大缺陷,如果当前值PV在目标值SV附近,输出信号OUT就会不断地通断,导致执行部件的开关的寿命大大减少,并且由于惯性因素,控制效果往往会在目标值SV上下产生很大的波动。

PID算法

位置式PID算法

PID算法是一种具有预测未来状态的算法,其核心思想是

  1. PID算法不但考虑控制对象的当前状态值,而且还考虑控制对象过去,和最近一段状态的状态值变化,并由这3面共同决定当前的输出控制状态
  2. PID控制算法的运算结果是一个数,利用这个数可以控制被控对象的工作状态,一般输出形式是PWM

算法分析

某控制系统中,用户的设定值是SV

  1. 从系统开始运行后,控制算法每隔一段时间对被控对象的状态值进行采样。得到从开机以来被控对象每个采样点被控制对象的状态值所形成的数据序列:

    X1,X2,X3,X4,....Xk−2,Xk−1,Xk
    X 1 :开机以来的第一次采样值 Xk:开机以来的第一次采样值

  2. 我们从这些序列中提取出来三个信息

1)当前采样值Xk与用户设定值SV之间的差值:Ek

Ek=Sv−Xk

​ Ek >0:说明当前状态值未达标

​ Ek =0:说明当前控制状态值正好满足要求

​ Ek <0:说明当前状态值已经超标

明显可以看出Ek就是控制对象当前值和设定值的偏差程度,我们可以根据Ek的大小对输出信号OUT进行调整,即偏差程度大OUT增大,偏差程度小OUT减小。即输出信号的强弱和当前的偏差程度有着某种关系,单纯根据Ek的大小来给出控制信号OUT的值的算法被称为比例控制,即

POUT=KpEk+Out0

Kp:称为比例系数,可以理解成一个放大器,选取不同的Kp可以将OUT的增益放大或者缩小,并提高控制算法的响应速度

Out0:是一个常数,为了在Ek为0时,确保输出信号不为0,防止PV = SV时OUT = 0,系统就处于失控状态

2)如果把开机运行以来的各个采样值都与设定值相减,就能得到开机以来每个采样时刻的偏差序列数据

E1E2E3.....Ek2,Ek1,Ek

E1:开机后第一个采样时刻的偏差值
E1=SV−X1;E2=SV−X2;......Ek−2=SV−Xk−2;Ek−1=SV−Xk−1;


Ek: 当前的采样值与设定值的偏差

分析这个序列可以得出:

每个偏差值可以是:>0,<0,=0这三种数值,这是因为开机后控制算法不断作用,导致过去的这段时间有的时候超标(Ex<0),有些时候未达标(Ex>0),有时候正好满足要求(Ex=0),将这些偏差值进行累加得到Sk,即:

Sk=E1+E2+E3+.........+Ek2+Ek1+Ek

​ Sk >0: 过去大多数时候未达标

​ Sk =0:过去控制效果较理想

​ Sk <0: 过去大多数时候已经超标

通过分析Sk,可以对控制算法过去的控制效果进行评估,控制算法按照曾经的方式输出的控制信号导致了现在的结果,可以利用这个值对当前要输出的控制信号OUT进行修正,来保证控制对象会尽快的达到用户的设定值

Sk实际上就是对过去每个时间点的误差相加,相等于高等数学中的定积分,所以根据Sk对输出信号进行调节的算法称为积分算法,即

Kp是一常数,其目的类似硬件上的放大器,用于放大或者缩小

Out0是一常数,为了在历史积分偏差值为0时确保系统有一个输出值,避免失控

Ti叫做积分时间常数,取值越大会导致输出OUT越小,可以理解为历史上已经很久的误差值都影响了现在的输出信号,取值越小,输出OUT会更大,相当于积分只考虑了最近一段时间的误差。

但是在实际上,如果系统已经运行很长时间,早期的偏差值的影响是可以忽略的,所以如果选取合适的TI值会有意想不到的控制效果

3)Dk是最近两次的偏差Dk=EkEk1
Ek: 当前的偏差

Ek-1: 基于当前的前一个采样时刻的偏差值(即上一次的偏差值);

​ Dk >0:说明从上一采样时刻到当前误差有增大趋势

​ Dk =0:说明从上一采样时刻到当前误差平稳

​ Dk <0:说明从上一采样时刻到当前误差有减小趋

Dk可以反映出上次采样到当前采样的这段时间被控对象的状态变化趋势,这种趋势可能会延续到下一个采样时间点,可以根据这个变化趋势Dk对输出信号OUT进行调整,通过预测“未来”,达到提前控制的目的

Dk很像高等数学中的微分运算,即变化率,反映控制对象在一段时间内的变化趋势和变化量,所以利用Dk对控制器输出信号进行调节的算法称为微分算法,即:

DOUT=Kp(Td(de/dt))+Out0

Kp:为一常数,可理解为硬件上的放大器或衰减器,用于对输出信号OUT的增益进行调整Out0:为一常数,为了在

Dk:为0时确保OUT都有一个稳定的控制值,避免失控

Td:叫微分时间常数,Td越大会导致输出OUT增大,对输出信号产生很大影响

PID算法的组成部分

他们的优缺点分析
  • 比例算法:只考虑当前的误差,当前有误差才输出控制信号,当前没有误差就不输出控制信号,也就是说只有误差产生后比例算法才采取措施进行调整,单独的比例算法不可能将控制对象的状态值控制在设定值上,始终在设定值上下波动,但是比例控制反应非常灵敏,有误差就立刻反映到输出。
  • 积分算法:只考虑的是控制对象的历史误差情况,过去的误差状况参与了当前的输出控制,但是在系统没有到达目标期间,这些历史误差会对当前的输出产生干扰,如果使用不到,可能扰乱当前的输出。但是在系统进入稳定状态后,特别是当前值和设定值没有误差时,积分算法就会根据过去的误差值输出一个相对稳定的控制信号,防止偏离目标值
  • 微分算法:只考虑了最近一段时间的变化率,当系统懂得偏差趋近于某一个固定值时(变化率为0),微分算法不输出控制信号对其偏差进行调整,所以微分算法也不能单独使用,它只关心偏差的变化速度,而不考虑是否存在偏差。但是微分算法可以知道控制对象最近的变化趋势,可以尽早的抑制控制对象的变化,在将要发生剧烈变化时,就大幅度调整输出信号进行抑制,避免控制对象的大幅度变化。

这三种算法综合起来,相互的优缺点互补,就形成了PID算法
OUT=POUT+IOUT+DOUT

或者

将所有的Out0归并到OUT0

说了这么多接下来说说在单片机中的PID算法

T是采样周期,即多久采样一次当前值PV,进行一次PID计算

Ti:是积分算法在Ti时间内和比例算法的控制信号是相同的

Td:是微分算法在Td时间内和比例算法产生的控制信号是相同的

再经过一次变换

就得到了

利用C语言可以方便实现这个计算公式。OUT即为本次运算的结果,利用OUT可以去驱动执行机构输出对应的控制信号,例如温度控制就可以控制PWM的宽度

这种PID算法计算出的结果(OUT值)表示当前控制器应该输出的控制量,所以称为位置式(直接输出了执行机构应该达到的状态值)。

增量式PID算法

位置式PID算法计算量较大,比较消耗处理器的资源。在有些控制系统中,执行机构本身没有记忆功能,比如MOS管是否导通完全取决于控制极电压,可控硅是否导通取决于触发信号,继电器是否接通取决于线圈电流等,只要控制信号丢失,执行机构就停止,在这些应用中应该采用位置式PID。

也有一些执行机构本身具有记忆功能,比如步进电机,即使控制信号丢失,由于其自身的机械结构会保持在原来的位置等,在这些控制系统中,PID算法没有必要输出本次应该到达的真实位置,只需要说明应该在上次的基础上对输出信号做多大的修正(可正可负)即可,这就是增量式PID算法。

增量式PID计算出的是应该在当前控制信号上的调整值,如果计算出为正,则增强输出信号;如果计算出为负则减弱输出信号。

如果用OUTK-1表示上次的输出控制信号值,那么当前的输出值应该为OUTk,这两者之间的关系为:

OUTK=OUTk1+OUT

变化后得到
OUT=OUTK−OUTk−1

本次的位置式算法输出:

两个式子相减

kp(EKEK1)+((KpT)/Ti)Ek+(((KpTD)/T)(Ek2Ek1+Ek2))

Ki=Kp(T/Ti);KD=(Kp(TD/T)

得到

kp(EKEK1)+((KpT)/Ti)Ek+(((KpTD)/T)(Ek2Ek1+Ek2))

EK: 本次的偏差;

Ek-1:上次的偏差

Ek-2:上上次的偏差

Kp: 算法增益调节

Ti : 积分时间

TD: 微分时间常数

增量式PID的计算只需要最近3次的偏差(本次偏差,上次偏差,上上次偏差),不需要处理器存储大量的历史偏差值,计算量也相对较少,容易实现。

关于Ti和TD的理解:

在PID控制算法中,当前的输出信号由比例项,积分项,微分项共同作用形成,当比例项输出不为0时,如果积分项对运算输出的贡献作用与比例项对运算对输出的贡献一样时(即同为正或同为负时),积分项相当于重复了一次比例项产生作用的时间,这个时间就可以理解为积分时间。

当比例项不为0时,如果微分项在一段时间里计算的结果与比例项对输出的贡献相同(即同为正或同为负)时,微分项相当于在一段时间里重复了比例项的作用,这段时间可理解为就是微分时间。

实际应用中应该合理选择Kp,Ti,Td以确保三者对输出的贡献平衡,从而使控制对象在设定值的附近。

程序示例

这是一个温控项目,位置式PID

pid.h

#ifndef _pid_
#define _pid_
#include "stm32f10x_conf.h"

typedef struct
{
 float Sv;//用户设定值
 float Pv;//当前值
 
 float Kp;//比例系数
 float T;  //PID计算周期--采样周期
 float Ti;//积分常数
 float Td; //微分常数
	
	
	
 float Ek;  //本次偏差
 float Ek_1;//上次偏差
 float SEk; //历史偏差之和
	
  float Pout;//比例项输出
	float Iout;//积分项输出
	float Dout;//微分项输出
	
 float OUT0;//基础输出

 float OUT;//最终输出
	
	
 u16 C10ms;//10ms
	
 u16 pwmcycle;//pwm周期
	
}PID;

extern PID pid; //存放PID算法所需要的数据
void PID_Calc(void); //pid计算

#endif

pid.c

#include "pid.h"

PID pid; //存放PID算法所需要的数据


void PID_Calc()  //pid计算
{
 float DelEk;
	float ti,ki;
	float td;
	float kd;
	float out;
 if(pid.C10ms<(pid.T))  //计算周期未到
 {
    return ;
 }
 
 pid.Ek=pid.Sv-pid.Pv;   //得到当前的偏差值
 pid.Pout=pid.Kp*pid.Ek;      //比例输出
 
 pid.SEk+=pid.Ek;        //历史偏差总和
 
 DelEk=pid.Ek-pid.Ek_1;  //最近两次偏差之差
 
 ti=pid.T/pid.Ti;//积分时间常数
 ki=ti*pid.Kp;//积分项系数
 
  pid.Iout=ki*pid.SEk*pid.Kp;  //积分输出

 td=pid.Td/pid.T;//微分时间常数
 
 kd=pid.Kp*td;//微分项系数
 
  pid.Dout=kd*DelEk;    //微分输出
 
 out= pid.Pout+ pid.Iout+ pid.Dout;//最终输出
 
 
 if(out>pid.pwmcycle)//大于最大pwm周期
 {
  pid.OUT=pid.pwmcycle;//满占空比
 }
 else if(out<0)//小于最小
 {
  pid.OUT=pid.OUT0; //占空比为0
 }
 else 
 {
  pid.OUT=out;//其他情况
 }
 pid.Ek_1=pid.Ek;  //更新偏差
 
 pid.C10ms=0;//一个PID计算时间归0
}

pidout.c

#include "pidout.h"
#include "pid.h"
void PIDOUT_init()//pid输出脚初始化
{
	GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);  //开B口时钟  
  GPIO_InitStructure.GPIO_Pin =GPIO_Pin_8;	
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
}


void PID_out()  //输出PID运算结果到负载---每1ms被调用1次
{
   static u16 pw;
	 pw++;
	 if(pw>=pid.pwmcycle)  //
	 {
	   pw=0;
	 }
	  //0  ~  pid.pwmcycle-1
	 
	 if(pw<pid.OUT)
	 {
	   pwmout_0;//加热
	 }
	 else
	 {
	   pwmout_1;//停止加热
	 }
	 
}

time_init.c

 #include "time_init.h"

#include"stm32f10x_conf.h"

  
void Timer4_init()	//T4 10ms时钟
{	 //72000000/7200=100us
//
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM4,ENABLE);

 

TIM_TimeBaseStructure.TIM_Period = 10000-1; //计数个数     //100us*10=1000us=10ms
TIM_TimeBaseStructure.TIM_Prescaler =72-1;//分频值   	    
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0; 	//分割时钟			
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
 TIM_DeInit(TIM4);
TIM_TimeBaseInit(TIM4, & TIM_TimeBaseStructure); 
TIM_Cmd(TIM4, ENABLE); 	 //使能定时器2

 /*以下定时器4中断初始化*/
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE); //向上计数溢出产生中断


}
  
void Timer3_init()	//T3 1ms时钟
{	 //72000000/7200=100us
//
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM3,ENABLE);

 

TIM_TimeBaseStructure.TIM_Period = 1000-1; //计数个数     //10ms
TIM_TimeBaseStructure.TIM_Prescaler =72-1;//分频值   	    
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0; 	//分割时钟			
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
 TIM_DeInit(TIM3);
TIM_TimeBaseInit(TIM3, & TIM_TimeBaseStructure); 
TIM_Cmd(TIM3, ENABLE); 	 //使能定时器2

 /*以下定时器4中断初始化*/
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //向上计数溢出产生中断


}


max6675.c

#include "max6675.h"
#include "pid.h"

float temper;
extern u16 Kms10;


#define cs_1   GPIO_SetBits(GPIOC, GPIO_Pin_11)
#define cs_0   GPIO_ResetBits(GPIOC, GPIO_Pin_11)

#define sck_1  GPIO_SetBits(GPIOC, GPIO_Pin_12)
#define sck_0   GPIO_ResetBits(GPIOC, GPIO_Pin_12)

#define so GPIO_ReadInputDataBit(GPIOD,GPIO_Pin_2) 

u16 read_max6675()  //????
{
  u16 d,i; 
  //so_1;
  cs_0;
  delay_us(2);
  for(i=0;i<16;i++)
  {
    sck_1;
    delay_us(2);
    sck_0;
    delay_us(2);
    d<<=1;
    if(so)
     d++;
  }
  cs_1;
  return d;
}

void read_temper()//??????
{
  u16 d;
	if(Kms10<20)  return ;
	
  d=read_max6675();//读取MAX6675当前的温度值
  pid.Pv=((d>>4)&0x0fff)*0.25;//????  
  Kms10=0;	
}


void Max6675_Init()
{GPIO_InitTypeDef GPIO_InitStructure;
	 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);  //开C口时钟
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);  //开D口时钟
	
	 
   GPIO_InitStructure.GPIO_Pin =GPIO_Pin_11;	
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOC, &GPIO_InitStructure);
	
	   GPIO_InitStructure.GPIO_Pin =GPIO_Pin_2;	
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOD, &GPIO_InitStructure);
	
	
	   GPIO_InitStructure.GPIO_Pin =GPIO_Pin_12;	
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOC, &GPIO_InitStructure);
}


main.c

#include "led.h"
#include "12864.h"
#include "time_init.h"
#include "max6675.h"
#include "pid.h"
#include "pidout.h"

u16 Kms10;
void delay(u32 x)
{
  while(x--);
}

void delay_us(u16 us)
{
	 u8 i;
  while(us--)
	{
	  for(i=0;i<6;i++)
		{
		
		}
	}
}

void Isr_Init()
{
	NVIC_InitTypeDef NVIC_InitStructure; 
	
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel =TIM4_IRQn;// TIM4_IRQChannel; 
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 
NVIC_Init (&NVIC_InitStructure); 


NVIC_InitStructure.NVIC_IRQChannel =TIM3_IRQn;// TIM3_IRQChannel; 
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; 
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 
NVIC_Init (&NVIC_InitStructure);
	
}

 void TIM4_IRQHandler() //10ms 1次
{					  
  
	static u8 tsec; u8 st;
	st=TIM_GetFlagStatus(TIM4, TIM_IT_Update);
	if(st!=0)
	{  TIM_ClearFlag(TIM4, TIM_IT_Update); 
		Kms10++;
//    pid.C10ms++;
		if(tsec++>=100)
		{
		  tsec=0;

		}
	}
}

 void TIM3_IRQHandler() //1ms 1次
{					  
   u8 st;
	st=TIM_GetFlagStatus(TIM3, TIM_IT_Update);
	if(st!=0)
	{  pid.C10ms++;
		 TIM_ClearFlag(TIM3, TIM_IT_Update); 
     PID_out(); //输出PID运算结果到负载
	}
}

void PID_Init()
{
  pid.Sv=120;//用户设定温度
	pid.Kp=30;
	pid.T=500;//PID计算周期
  pid.Ti=5000000;//积分时间
	pid.Td=1000;//微分时间
	pid.pwmcycle=200;//pwm周期1000
	pid.OUT0=1;
}


void Display()
{
	Show_string1(0x90,"设定值:");
	Show_number(0X94,pid.Sv);	
	
	Show_string1(0x88,"当前值:");
	Show_number(0X8c,pid.Pv);
	
	Show_string1(0x98,"PIDOUT:");
	Show_number(0X9c,pid.OUT);	
}


int main()
{
   LCD_Init();
	 Show_string1(0x80,"位置式PID");
	 Timer4_init();	//T4 10ms时钟	
	 Isr_Init();
	 Max6675_Init();
	 PID_Init();
	 PIDOUT_init();
	 Timer3_init();	//T3 1ms时钟
   while(1)
   {
    read_temper();//读取当前温度
		PID_Calc(); //pid计算 
		Display(); 
   }
}