PID控制的理解与具体实现

PID控制的理解与具体实现

摘要

        比例积分微分控制,简称PID控制,由于其 算法简单、***鲁棒性好***和 可靠性高,被广泛应用于工业过程控制,至今仍有 90% 左右的控制回路具有PID结构。
简单的说,根据给定值和实际输出值构成控制偏差,将偏差按比例积分微分通过线性组合构成控制量,对被控对象进行控制。
        所谓串级控制,就是采用两个控制器串联工作,外环控制器的输出作为内环控制器的设定值,由内环控制器的输出去操纵控制阀,从而对外环被控量具有更好的控制效果。
        万能的PID控制听起来很陌生,但是在我们的生活中随处可见。比如空调的温度控制,无人机的精准悬停,机械手臂的运动系统,人脸跟踪,甚至飞机和火箭的姿态调整等等,都要到这这个最基础的自动控制算法。

一定要认真理解PID控制算法是一种闭环系统!!!

一、概念理解与数学原理

        首先我们先不引入过多的复杂概念,先结合一个具体的生活实例来形象生动的讲述PID的各个环节。
        众所周知,某校西区宿舍洗澡是要调控冷热水比例才能达到令人舒服的水温。我有个师兄,现在这位师兄已经放出了冷水,只需要再加入50个单位的热水就可以肆无忌惮的洗澡了。一般地,我们会先设置一个达成目标的时间。比如说5秒,那么这位师兄操纵机械臂旋转热水阀的平均速度是10单位/秒。按照这个速度匀速转动开关5s就可以完成目标了。而这种控制方法叫做***开放回路控制系统***,模式图如下:

在这里插入图片描述

这种控制系统的可以理解成是一种流水线的结构,输入是恒定的值,输出也是对应得到的恒定值,且输出值对输入值是没有影响的

然而要知道,机械臂有可能因为沾上了水而容易发生偏移,又或者机械臂转动速度并不是那么精准,是无法达到他洗澡要求的50单位热水。况且这位老同志突然想改成40单位热水或者60单位热水,那么我们又需要重新给这个系统赋予新的输入值,这是件很麻烦的事情,总不能老同志洗澡的时候,我们闯进去计算输入值给机械臂吧!
        在这里我们更需要一种***闭环回路控制系统***。在这样一个系统中,输入值不在是一个恒定的值,而是会受输出值的影响。根据当前的热水总量来重新调整机械臂的转动力,模式图如下:

在这里插入图片描述

这就是PID控制的基本模式图

那么这种调整是如何做出来的呢?

        我们引入一个误差值来表示当前值与目标值的差值。当这个误差值越大时,机械臂的转动力就越大。并且我们把误差值转动开关的力的关系表示为一个线性关系,同时误差是关于时间的函数
F = K p ∗ e r r o r (1) F=K_p*error\tag{1} F=Kperror(1)
e r r o r = f ( t ) (2) error=f(t)\tag{2} error=f(t)(2)
其中的 kp 是一个系数,由此公式我们可以看出在最初状态时,机械臂旋转力量是最大的,随着误差越来越小,旋转力也逐渐减小,热水量逐渐接近目标值。这就是PID算法中的 “P” 部分,即***比例部分,它表示的是转动速度v与误差error之间的比例关系***。用图像表示即为

在这里插入图片描述

        同时通过图像,我们也注意到逐渐接近目标值时,虽然转动力最终会为零,但因为存在着 ***“惯性”***,在接近目标时难免会产生一点溢出,产生额外的误差。就好比在目标值处加速度虽然为零,但速度仍然存在,是会超过目标值的。说白了,就是春洲师兄手抖抖抖抖抖抖抖了况且这种振荡误差会因为接近目标值速度过快,即kp过大而愈发明显。 这是我们就需要介绍PID算法中的 ***“D”,算法,即微分算法。***。我们对误差进行微分就是造成震荡溢出的速度,通过这个运算我们可以抵消掉振荡产生的速度。加入微分运算后PID的简易数学表达为
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t (3) F=K_p*error+K_d*{d(error)\over dt}\tag{3} F=Kperror+Kddtd(error)(3)
K d = K p ∗ T d (4) K_d=K_p*T_d\tag{4} Kd=KpTd(4)
其中 Kd 为微分系数, Td 为微分时间常数。基本上通过 PD 两种运算我们就能得到一个平稳的图像。

在这里插入图片描述

        不过细心一些我们就不难发现即使经过了这两种运算,误差也是不能被消除的。随着误差减小,春洲师兄的机械臂转动力也减小,最终会和阻力平衡,这时开关不会在转动,误差仍然存在,也就是说我们还需要抵消掉阻力造成的误差。 在这里,我们通过累积所有时间段的误差,得到一个均值再乘以某个系数来提供给春洲师兄更大的转动力来弥补阻力。数学表达式如下:
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t + K i ∗ ∫ 0 t ( e r r o r ) d t (5) F=K_p*error+K_d*{d(error)\over dt}+K_i*\int_0^t (error)dt \tag{5} F=Kperror+Kddtd(error)+Ki0t(error)dt(5)
K i = K p T i (6) K_i={K_p\over T_i}\tag{6} Ki=TiKp(6)
其中 Ti 为积分时间常数,Ki 为积分系数。综合以上六个式子,我们就得到了PID算法完整的数学表达式:
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t + K i ∗ ∫ 0 t ( e r r o r ) d t F=K_p*error+K_d*{d(error)\over dt}+K_i*\int_0^t (error)dt F=Kperror+Kddtd(error)+Ki0t(error)dt
K i = K p T i K_i={K_p\over T_i} Ki=TiKp
K d = K p ∗ T d K_d=K_p*T_d Kd=KpTd

简易模式图:

在这里插入图片描述

  • 关键的总结

        比例作用是针对系统当前误差进行控制,积分作用则针对系统误差的历史,而微分作用则反映了系统误差的变化趋势,这三者的组合是“过去、现在、未来”的完美结合

二、性能指标

        其实从上面的概念理解中,我们就很容易通过比例、积分、微分三个环节的作用来衡量这个PID控制是否优越。

  • 比例环节可以控制系统的从最开始到稳定值的时间以及震荡的程度
  • 积分环节可以控制系统在稳定值时与目标值之间的差距
  • 微分环节可以控制系统稳定值调整到目标值所需时间

于是就产生了四个衡量PID系统的指标:

1、上升时间 t r t_r tr:从最开始到稳定值的时间

2、超调量 σ % \sigma\% σ%:震荡的程度

3、稳态误差 e s s e_{ss} ess:在稳定值时与目标值之间的差距

4、调节时间 t e t_e te:稳定值调整到目标值所需时间
我们就是通过这四个指标来衡量一个PID控制系统性能好坏,而影响这些指标的即是PID算法公式中的三个系数。所以我们需要通过对这几个参数进行整定来提高系统的性能。

三、参数选取与整定

我们把目光重新放到PID的数学表达式上,我们不难发现 Kd、Ki的值是和Kp有关的。因此这三个系数的值不是独立的,当Kp值最优时,另外两个不一定就是最优的值。我们寻求的不是局部最优,而是三个系数组合起来的全局最优。

  • Kp使得控制器的输入输出成比例关系,为了尽量减小偏差,同时也为了加快响应速度缩短调节时间,就需要增大Kp。但比例作用过大会使系统有较大惯性,造成溢出

  • Ki作用的引入有利于消除稳态误差,但使系统的稳定性下降。尤其在大偏差阶段的积分往往会使系统产生过大的超调,调节时间变长

  • Kd作用的引入使系统能够根据偏差变化的趋势做出反应,适当的微分作用可加快系统响应有效地减小超调,改善系统的动态特性,增加系统的稳定性。不利之处是微分作用对干扰敏感,使系统抑制干扰能力降低

因此,PID控制器的参数选取必须兼顾动态与静态性能指标要求,只有合理地整定Kp、Ki、Kd三个参数,才能获得比较满意的控制性能。

        下面我们介绍正定参数的方法。
        最省事儿的办法就是先增大 Kp ,当能看到明显振荡时,引入Kd并适当减小Kp。如果一直打不到预期值,则引入 Ki并慢慢增大。
        还有一种是正规的方法,临界比例度法(Z-N法)。先只引入比例算法,从大到小逐渐改变调节器的比例度,得到等幅振荡的过渡过程,此时的比例度称为临界比例度,用 δ k {\delta_k} δk表示。相邻两个波峰间的时间间隔,称为临界振荡周期,用 T K {T_K} TK表示。用这两个值根据表格通过计算即可求出调节器的三个整定参数。

在这里插入图片描述

四、代码的具体实现

鉴于代码量繁杂,以下只展示关键代码部分。我们通过运用PID控制算法来让电机匀速稳定转动 。下面代码参考“平衡小车之家”的测试代码,其中串口的收发数据帧协议是由正点原子提供

1、主函数

u8 flag_Stop=1;    //收发数据的停止标志位
int Encoder;       //编码器脉冲计数 
int moto;          //电机PWM变量

int main(void)
 {
    
    	
	 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断分组
	 Stm32_Clock_Init(9);      //系统时钟设置函数
    delay_init();             //延时函数初始化
	 LED_Init();               //初始化LED的GPIO口
	 uart_init(115200);        //串口初始化
	 MOTO_Init();              //初始化电机所接GPIO口
	 pwm_Init(7199,0);         //初始化PWM波
	 Encoder_Init_TIM2();      //初始化定时器
	 TIM3_Int_Init(99,7199);   //初始化中断函数 每10ms一次中断
	 while(1)
	 {
    
    
     printf("%d\r\n",Encoder); //通过串口打印脉冲数来形象看到PID控制
     delay_ms(10);
	 }
 }

2、串口的初始化与串口收发数据函数

 
#if EN_USART1_RX   //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误   	
u8 USART_RX_BUF[USART_REC_LEN];     
//接收缓冲区最大存储USART_REC_LEN个字节.
//接收状态
//bit15,	接收完成标志
//bit14,	接收到0x0d
//bit13~0,	接收到的有效字节数目
u16 USART_RX_STA=0;       //接收状态标记	  
  
void uart_init(u32 bound){
    
             //GPIO端口设置
   GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
  
	//USART1_TX   GPIOA.9
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1_RX	  GPIOA.10初始化
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

  //Usart1 NVIC 配置
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
  
   //USART 初始化设置

	USART_InitStructure.USART_BaudRate = bound;//串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

  USART_Init(USART1, &USART_InitStructure); //初始化串口1
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
  USART_Cmd(USART1, ENABLE);                    //使能串口1 

}

void USART1_IRQHandler(void)                	//串口1中断服务程序
	{
    
    
	u8 Res;
#if SYSTEM_SUPPORT_OS 		//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
    
    
		Res =USART_ReceiveData(USART1);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
			{
    
    
			if(USART_RX_STA&0x4000)//接收到了0x0d
				{
    
    
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
				}
			else //还没收到0X0D
				{
    
    	
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
					{
    
    
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收	  
					}		 
				}
			}   		 
     } 
#if SYSTEM_SUPPORT_OS 	//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntExit();  											 
#endif
} 
#endif		

3、初始化定时器2为编码器接口模式

void Encoder_Init_TIM2(void)
{
    
    
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  
  TIM_ICInitTypeDef TIM_ICInitStructure;  
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器2的时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PA端口时钟
	
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;	//端口配置
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //浮空输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);					      //根据设定参数初始化GPIOB
  
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
  TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 
  TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD-1; //设定计数器自动重装值
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1;TIM向上计数  
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
	
  TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3
	
  TIM_ICStructInit(&TIM_ICInitStructure); //把TIM_ICInitStruct 中的每一个参数按缺省值填入
  TIM_ICInitStructure.TIM_ICFilter = 10;  //设置滤波器长度
  TIM_ICInit(TIM2, &TIM_ICInitStructure);//根据 TIM_ICInitStruct 的参数初始化外设	TIMx
 
  TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除TIM的更新标志位
  TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//使能定时器中断

  TIM_SetCounter(TIM2,0);//设置TIMx 计数器寄存器值
  TIM_Cmd(TIM2, ENABLE); //使能定时器
}

4、单位时间读取脉冲数

int Read_Encoder(u8 TIMX)//读取计数器的值
{
    
    
  int Encoder_TIM;
	//printf("%d\r\n", TIM2->CNT);
 	switch(TIMX)
	{
    
    
	  case 2:Encoder_TIM=(short)TIM2->CNT; TIM2 -> CNT=0;
          break;
		case 3:Encoder_TIM=(short)TIM3->CNT; TIM3 -> CNT=0; 
       break;
		case 4:Encoder_TIM=(short)TIM4->CNT; TIM4 -> CNT=0;
        break;
		default: Encoder_TIM=0;
	}
  return Encoder_TIM;
}

5、定时器3中断函数(对脉冲计数进行处理)

nt Target_velocity=50;  //设定速度控制的目标速度为50个脉冲每10ms
void TIM3_IRQHandler(void)
{
    
    
 if(TIM_GetFlagStatus(TIM3,TIM_FLAG_Update)==!RESET)
 {
    
    
   TIM_ClearITPendingBit(TIM3,TIM_IT_Update);   //===清除定时器1中断标志位
	 Encoder=Read_Encoder(2);                     //取定时器2计数器的值
   Led_Flash(100);                              //LED闪烁
	 moto=Incremental_PI(Encoder,Target_velocity);    //===位置PID控制器
	 Xianfu_Pwm();
   Set_Pwm(moto);
	
 }
}

6、PID算法核心代码(其实代码很简单)

float Incremental_PI (int Encoder,int Target)
{
    
     	
   float Kp=75,Ki=10;	
	 static float Bias,Pwm,Last_bias;
	 Bias=Encoder-Target;                //计算偏差
	 Pwm+=Kp*(Bias-Last_bias)+Ki*Bias;   //增量式PI控制器
	 Last_bias=Bias;	                   //保存上一次偏差 
	 return Pwm;                         //增量输出
}

7、其他辅助实现PID的函数

void Set_Pwm(int moto)//赋值给PWM寄存器
{
    
    
 if(moto>0) AIN1=0,   AIN2=1;
	else      AIN1=1,   AIN2=0;
	PWMA=myabs(moto);
}

void Xianfu_Pwm(void) //限制幅度的函数
 {
    
    
   int Amplitude=7100;  //===PWM满幅是7200 限制在7100
	 if(moto<-Amplitude)  moto = -Amplitude;
	 if(moto>Amplitude)   moto =  Amplitude;
 }

int myabs(int a) //取绝对值
{
    
     		   
	 int temp;
	 if(a<0)  temp=-a;  
	 else temp=a;
	 return temp;
}

我们每隔10ms就将脉冲值通过串口打印出来,并使用一款可以将串口数据绘制成图像的软件(SerialPlot)来形象感受PID。

在这里插入图片描述

通过图像 我们不难看到响应时间较短,静差几乎消除,稳定性较好。这是一个性能较为优越的PID系统。

猜你喜欢

转载自blog.csdn.net/weixin_50950634/article/details/114145808