本篇文章是我看完江科大的PID入门教程所写的,能够帮助大家快速入门PID

同时如果调整软件PID很长时间都没有见到好的效果,其实可以想一想是不是硬件的问题啊

目录

1理论知识

1.1PID基本原理

1.2PID程序实现

位置式PID程序的实现

增量式PID程序的实现

基础驱动编写

编码器解释:

2代码编写

位置式PID

增量式PID

位置式PID定位控制

增量式PID定位控制

2.1总结一下

3PID算法改进

积分限幅

积分分离

变速积分

微分先行

不完全微分

输出偏移

输入死区

扩展:多环串级PID

代码封装:


我使用的编码电机是wheeltec的mg513编码电机

1理论知识

1.1PID基本原理

PID是比例(Proportional)、积分(Integral)、微分(Differential)的缩写 PID是一种闭环控制算法,它动态改变施加到被控对象的输出值(Out),使得被控对象某一物理量的实际值(Actual),能够快速、准确、稳定地跟踪到指定的目标值(Target)

PID是一种基于误差(Error)调控的算法,其中规定:误差=目标值-实际值,PID的任务是使误差始终为0

PID对被控对象模型要求低,无需建模,即使被控对象内部运作规律不明确,PID也能进行调控

PID公式与系统框图

比例项(P)

下面四个图就是不同kp的时候目标值与实际值的误差

稳态误差

因为如果误差项为0的时候,那么电机驱动力度就为0了,由于摩檫力等因素,这个误差项会加大,然后再去调控,当摩檫力与电机驱动力度相同的时候,那么这个就会保持在一个实际转动速度,但是小于目标值

所以稳态误差就是由于一些摩擦之类的

积分项(I)PI控制

在程序中积分就是给一个变量,所产生的误差累加起来,累加起来的值乘以Ki就是输出值

积分项最严重的问题就是滞后性(可能会使过充过大)!!!因为积分累加这是一个过程,比例项的反应是最快的

累加之后的值就是摩檫力,正好抵消

Ki的值过大就会超调,抖动也会更厉害

微分项(D)

连续形式的PID与离散形式的PID公式

单片机的PID是离散型的,每隔一个周期就会去调控一次,因为最开始通用的是模拟电子电路

模拟电子电路里面就会有许多运算方式,但是模拟电路实现PID已经被淘汰了

连续形式PID离散化的示意图

位置式PID与增量式PID公式

位置式PID其实就是简化过后的离散式PID

位置式PID与增量式PID的比较

举个栗子吧

用打开阀门来举例,位置式PID相当于告诉阀门打开位置50%,30%,增量式PID告诉阀门每次增加8%,减少7%等等

1.2PID程序实现

1 ,在while循环中执行调控,用delay()来控制调控时间

2,用一个定时器进行调控,中断中去调用,但是要注意,中断中,相当于是多线程,不能同时访问同一块内存空间(这个也是常用的)

3, 这个用标志位来进行调控(主程序不能阻塞)

位置式PID程序的实现

增量式PID程序的实现

基础驱动编写

这个就是串口,电机驱动,定时器等等的底层编写

编码器解释:

我用的式mg513GMR编码器,磁铁级数为500,减速比1:30

这里编码器是检测AB相的上升下降沿,那以下面为例,转一圈AB输出11个脉冲,那么编码器的计数值就为4*11=44,然后减速比为9.3,那么你吧电机旋转一圈编码器就会有44*9.3=408个数,这样就可以通过这个数除408再*360,就可以计算出转动的角度,这个速度就是用定时器每隔一段时40ms间,就Encoder_get(),这样就是/40ms----就可以换算速度了

编码器测速周期要与PID调控周期一样

还要注意,这个电机正向旋转给的PWM的极性要和编码器测速的极性相同,这样调控才是负反馈,要不然这两个极性相反的话就是正反馈了,电机直接满速转动

PID参数的整定

P:一般就是输出/输入

I : 这个要看是否有稳态误差,一般比较小

D:不一定要加,看到实际的波形再做调整

2代码编写

位置式PID

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"
#include <math.h>

int16_t Speed;
uint8_t KeyNum;
int8_t PWM;

float Target,Actual,Out;
float Kp=0.6,Ki=0.2,Kd=0.2;
float Error0,Error1,ErrorInt;

int main(void)
{
	OLED_Init();
	LED_Init();
	Key_Init();
	Motor_Init();
	Timer_Init();
	Encoder_Init();
	Serial_Init();
	while (1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			Target+=10;
		}
		else if(KeyNum == 2)
		{
			Target-=10;
		}
		else if(KeyNum == 3)
		{
			Target=0;
		}
		OLED_Printf(0,0,OLED_8X16,"Tar:%+04.0f",Target);
		OLED_Printf(0,16,OLED_8X16,"Act:%+04.0f",Actual);
		OLED_Printf(0,32,OLED_8X16,"Out:%+04.0f",Out);
		Serial_Printf("%f,%f,%f\r\n",Target,Actual,Out);
		OLED_Update();
		
	}
}

void TIM1_UP_IRQHandler()
{
	static uint16_t Count = 0;
	if(TIM_GetITStatus(TIM1,TIM_IT_Update==SET))
	{
		Key_Tick();	
		Count++;
		if(Count >= 20)
		{
			Count = 0;
			//Speed = Encoder_Get()/(50);
			Actual = Encoder_Get()/(25);
			
			Error1 = Error0;
			Error0  = Target -Actual;
			
			if(fabs(Ki)>0.001)
			{
				ErrorInt +=Error0;
			}
			
			Out = Kp*Error0+Ki*ErrorInt+Kd*(Error0-Error1);
			if(Out>100){Out = 100;}
			if(Out<-100){Out = -100;}
			Motor_SetSpeed(Out);
		}
		
		
		TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
	}
}

增量式PID

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"
#include <math.h>
int16_t Speed;
uint8_t KeyNum;
int8_t PWM;

float Target,Actual,Out;
float Kp=0.9,Ki=0.3,Kd=0;
float Error0,Error1,Error2;

int main(void)
{
	OLED_Init();
	LED_Init();
	Key_Init();
	Motor_Init();
	Timer_Init();
	Encoder_Init();
	Serial_Init();
	while (1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			Target+=10;
		}
		else if(KeyNum == 2)
		{
			Target-=10;
		}
		else if(KeyNum == 3)
		{
			Target=0;
		}
		OLED_Printf(0,0,OLED_8X16,"Tar:%+04.0f",Target);
		OLED_Printf(0,16,OLED_8X16,"Act:%+04.0f",Actual);
		OLED_Printf(0,32,OLED_8X16,"Out:%+04.0f",Out);
		Serial_Printf("%f,%f,%f\r\n",Target,Actual,Out);
		OLED_Update();
	}
}

void TIM1_UP_IRQHandler()
{
	static uint16_t Count = 0;
	if(TIM_GetITStatus(TIM1,TIM_IT_Update==SET))
	{
		Key_Tick();	
		Count++;
		if(Count >= 20)
		{
			Count = 0;
			//Speed = Encoder_Get()/(50);
			Actual = Encoder_Get()/(25);
			
			Error2 = Error1;
			Error1 = Error0;
			Error0  = Target -Actual;
			
			Out += Kp*(Error0-Error1)+Ki*Error0+Kd*(Error0-2*Error1+Error2);
			if(Out>100){Out = 100;}
			if(Out<-100){Out = -100;}
			Motor_SetSpeed(Out);
		}
		
		
		TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
	}
}

位置式PID定位控制

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"

#include <math.h>

int16_t Speed;
uint8_t KeyNum;
int8_t PWM;

float Target,Actual,Out;
float Kp=0.7,Ki=0.005,Kd=0.1;
float Error0,Error1,ErrorInt;

int main(void)
{
	OLED_Init();
	LED_Init();
	Key_Init();
	Motor_Init();
	Timer_Init();
	Encoder_Init();
	Serial_Init();
	while (1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			Target+=50;
		}
		else if(KeyNum == 2)
		{
			Target-=50;
		}
		else if(KeyNum == 3)
		{
			Target=0;
		}
		OLED_Printf(0,0,OLED_8X16,"Tar:%+04.0f",Target);
		OLED_Printf(0,16,OLED_8X16,"Act:%+04.0f",Actual);
		OLED_Printf(0,32,OLED_8X16,"Out:%+04.0f",Out);
		Serial_Printf("%f,%f,%f\r\n",Target,Actual,Out);
		OLED_Update();
		
	}
}

void TIM1_UP_IRQHandler()
{
	static uint16_t Count = 0;
	if(TIM_GetITStatus(TIM1,TIM_IT_Update==SET))
	{
		Key_Tick();	
		Count++;
		if(Count >= 20)
		{
			Count = 0;
			//Speed = Encoder_Get()/(50);
			Actual += Encoder_Get()/(50);
			
			Error1 = Error0;
			Error0  = Target -Actual;
			
			if(fabs(Ki)>0.001)
			{
				ErrorInt +=Error0;
			}
			
			Out = Kp*Error0+Ki*ErrorInt+Kd*(Error0-Error1);
			if(Out>100){Out = 100;}
			if(Out<-100){Out = -100;}
			Motor_SetSpeed(Out);
		}
		
		
		TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
	}
}

增量式PID定位控制

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"
#include <math.h>
int16_t Speed;
uint8_t KeyNum;
int8_t PWM;

float Target,Actual,Out;
float Kp=0.99,Ki=0.01,Kd=0.2;
float Error0,Error1,Error2;

int main(void)
{
	OLED_Init();
	LED_Init();
	Key_Init();
	Motor_Init();
	Timer_Init();
	Encoder_Init();
	Serial_Init();
	while (1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			Target+=50;
		}
		else if(KeyNum == 2)
		{
			Target-=50;
		}
		else if(KeyNum == 3)
		{
			Target=0;
		}
		OLED_Printf(0,0,OLED_8X16,"Tar:%+04.0f",Target);
		OLED_Printf(0,16,OLED_8X16,"Act:%+04.0f",Actual);
		OLED_Printf(0,32,OLED_8X16,"Out:%+04.0f",Out);
		Serial_Printf("%f,%f,%f\r\n",Target,Actual,Out);
		OLED_Update();
	}
}

void TIM1_UP_IRQHandler()
{
	static uint16_t Count = 0;
	if(TIM_GetITStatus(TIM1,TIM_IT_Update==SET))
	{
		Key_Tick();	
		Count++;
		if(Count >= 20)
		{
			Count = 0;
			//Speed = Encoder_Get()/(50);
			Actual += Encoder_Get()/(25);
			
			Error2 = Error1;
			Error1 = Error0;
			Error0  = Target -Actual;
			
			Out += Kp*(Error0-Error1)+Ki*Error0+Kd*(Error0-2*Error1+Error2);
			if(Out>100){Out = 100;}
			if(Out<-100){Out = -100;}
			Motor_SetSpeed(Out);
		}
		
		
		TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
	}
}

2.1总结一下

在定位控制的程序中,这个积分项一定不能太大,因为如果太大,积分项的累加的结果会使Out直接输出到边界,一个强力的扭力让电机猛的一下转动,结果调整过头了,这样就会反反复复的转动,无法达到平衡,其实也可以直接用PD控制不要积分项

增量式PID比较依赖积分项,因为如果某一次Out是错误的,那么这个错误就会产生稳态误差,需要通过积分项来消除这个误差(所以最好不要将Ki设置为0)

增量式PID Kp,Ki,Kd全为0的时候,Out的值并不会降低到0,而是下降到一个稳定的值,这时候可以手动调整target的值,相当于进行手动控制,而且切换的时候抖动没有位置式PID那么大,更多用于手动切换到自动的模式

3PID算法改进

积分限幅:限制积分的幅度,防止积分深度饱和

积分分离:误差小于一个限度才开始积分,反之则去掉积分部分

变速积分:根据误差的大小调整积分的速度

--------------------------------------------------------------------------------------------------------------------

微分先行:将对误差的微分替换为对实际值的微分

不完全微分:给微分项加入一阶惯性单元(低通滤波器)

--------------------------------------------------------------------------------------------------------------------

输出偏移:在非0输出时,给输出值加一个固定偏移

输入死区:误差小于一个限度时不进行调控

积分限幅

积分限幅一般出现在位置式PID里面,积分项会一直累加,当电机卡住时,积分会饱和,卡住的时间越久,积分到后面达到深度饱和,当外力去掉的时候,电机会全速转动一段时间

积分上下限确定

1可以通过测量正常工作的时候积分项的最大值和最小值来确定,2可以通过Out/Ki来确定积分项的上下限,因为如果Ki*积分项等于100(Out的边界),那么积分项再变大就没有意义了,输出只能还是100

两种程序的实现

积分分离

定位置控制加入积分项会有超调问题,定速没有(很小)

超调的原因:因为积分项累加的力一定会导致超调

阈值的确定:通过测定无积分项的时候移动位置看看无法消除的稳态误差有多少,基本上让阈值大于这个值就行了

两种实现方法

变速积分

变速积分的问题其实就是积分分离的阈值给的比较小,当误差超过这个阈值的时候,就完全没有积分项的作用了,其实变速积分就是积分分离的升级版 (但是变速积分最后用积分限幅控制一下)

变速积分与积分分离的对比

这里就是设置一个函数用来改变积分项

两种实现方法

微分先行

就是微分项在目标突变时会变得很大,微分项其实就是为了阻碍我们朝着目标值变化,当是根据两次误差的斜率来确定的,但是如果目标值突然变大,这个斜率是正无穷大,之后会变为负数逐渐减小(其实这个不用微分先行的话可以加快响应速)

对误差求微分其实他的值就是对实际值求微分的负值(但是这个可以改进也可以不改变,这种就是让效果变得更平滑,但是突然改变的时候输出响应比较慢)

程序实现

不完全微分

噪声对微分项的影响是 最大的,所以不完全微分其实就是加入低通滤波器(用均值滤波)

但是滤波后这个就会产生时延,对于相应比较快的项目是不行的(所以只对D项加滤波)

加权平均后就会有滤波的作用

在程序中用rand给实际值一个噪声,用来模拟

但是我调整的还不是很明显(但是out输出平缓一点了)

程序实现

输出偏移

这是对于移动需要一定力度的执行器,如果输出值比较小,电机就不会动,这就会产生调控误差

而且这时候的Out比较小,既不产生驱动,而且还消耗电流,我们就可以设定一个区域,直接跳过这个不能驱动电机的Out区域

但是加上输出偏移后,会出现抖动,只要加一点力度,电机就会转动

程序实现

输入死区

这个就是防止抖动

输出偏移和输出死区两个同时用的时候效果是比较好的

其实要我说,最后一个输出偏移+输入死区就是很好的调控(我个人感觉效果最好)

扩展:多环串级PID

单环PID只能对被控对象的一个物理量进行闭环控制,而当用户需要对被控对象的多个维度物理量(例如:速度、位置、角度等)进行控制时,则需要多个PID控制环路,即多环PID,多个PID串级连接,因此也称作串级PID

多环PID相较于单环PID,功能上,可以实现对更多物理量的控制,性能上,可以使系统拥有更高的准确性、稳定性和响应速度

定位置控制单环与双环的比较

内环速度环可以用PI控制器,外环位置环可以用PD控制器

程序实现

其实关键就是外层的输出值是内层的目标值

举个栗子

1如果要实现平衡小车,那么就要用到双环PID,一个环是速度环,一个环是平衡环,平衡环需要MPU6050的水平值为0,速度环需要将速度保持一个速度,但是水平保持如果不稳定,就需要速度的调控

2对于四轴飞行器,其实也是内环是角速度PID ,外环是位置环PID

双环PID是先设计内环,再设计外环,调参也是先调内环再调外环,两个环访问被控对象的时间不同,同时外环的输出值作用于内环的目标值,首先调节内环的调节,调节好了再让外环工作

双环PID的控制效果是很好的,几乎没有误差,多小的误差几乎都可以消除,响应速度也非常快

通过改变外环的输出限幅,就可以进行定速控制

代码封装:

PID.h

#ifndef __PID_H
#define __PID_H
typedef struct 
{
	float Target;
	float Actual;
	float Out;
	
	float Kp;
	float Ki;
	float Kd;
	
	float Error0;
	float Error1;
	float ErrorInt;
	
	float OutMax;
	float OutMin;

}PID_t;

void PID_Update(PID_t * p);
#endif

PID.c

#include "stm32f10x.h"                  // Device header
#include "PID.h"
//这只是最基本的PID调控,在实际运用中可以用许多改进算法来提高准确度
void PID_Update(PID_t * p)
{
	p->Error1 = p->Error0;
	p->Error0 = p->Target -p->Actual;
	
	if(p->Ki!=0)
	{
		p->ErrorInt += p->Error0;
	}
	else
	{
		p->ErrorInt = 0;
	}
	
	p->Out = p->Kp * p->Error0 + p->Ki * p->ErrorInt + p->Kd * (p->Error0-p->Error1);
	if(p->Out > p->OutMax){p->Out = p->OutMax;}
	if(p->Out < p->OutMin){p->Out = p->OutMin;}
}

定时器中的PID控制代码

void TIM1_UP_IRQHandler()
{
	static uint16_t Count1 = 0,Count2=0;
	if(TIM_GetITStatus(TIM1,TIM_IT_Update==SET))
	{
		Key_Tick();	
		//内层PID
		Count1++;
		if(Count1 >= 40)
		{
			Count1 = 0;
			Speed = Encoder_Get()/(50);
			Location +=Speed;
			Inner.Actual = Speed;
			PID_Update(&Inner);
			Motor_SetSpeed(Inner.Out);
		}
		//外层PID
		Count2++;
		if(Count2 >= 40)
		{
			Count2 = 0;
			Outer.Actual = Location;
			PID_Update(&Outer);
			Inner.Target = Outer.Out;//这里可以去限制转动的速率
		}
		
		TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
	}
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐