目录
前言
本人对应PID算法只算初窥门径,对于更加深入的学习不足,这里参考了很多大佬的博客,加上我自己对于PID算法的总结,希望这篇博客能对你的PID学习之路有所帮助。如有不足之处,欢迎进行讨论学习。
一、PID的原理
PID 是“比例-积分-微分”(Proportional-Integral-Derivative)的缩写,是一种常见的反馈控制算法,广泛应用于工业控制系统和各种自动化设备中。PID 控制器通过计算偏差或误差值(即期望输出与实际输出之间的差值),并应用比例(P)、积分(I)和微分(D)三种控制作用来调整控制量,以达到期望的控制效果。
- 比例(P):这部分负责对当前的误差进行响应。比例增益越大,控制器对误差的反应越强烈,但过高的比例增益可能导致系统振荡。
- 积分(I):积分作用累积误差随时间的变化,用于消除稳态误差,确保系统的长期准确性。积分作用太强可能会导致系统响应缓慢和过冲。
- 微分(D):微分作用预测误差的趋势,对误差的变化率进行响应。它有助于减少系统的超调和振荡,提高系统的稳定性。微分作用较强会降低响应速度。
个人理解P是主要的动力来源,但是由于其作为一个百分比很多时候无法到达目标值,即总是存在一个误差,称为稳态误差。这时候就需要I来上场,用以消除这个总是存在的误差,使系统能够达到目标值。但是当动力较大而阻力较弱,整个系统处于不平衡的状态最终导致系统的振荡,而D的作用相当于系统的阻力,阻力与动力平衡后,系统就能趋于稳定的状态。
1、位置式
(1)位置式流程图
从图上可以看出PID系统,输入为目标值、反馈值,输出是电机转动的PWM。并且与增量式不同,位置式的反馈值是累计的脉冲值。当到达目标值后电机会停止运动。但是如果反馈值是每次的脉冲值就会导致电机一直运动。
(2)位置式公式
e(k)/e(i) 误差值 = 目标值 - 反馈值
e(k - 1) 上一次的误差值
(3)位置式使用注意事项
增量式算法不需要做累加,控制量增量的确定仅与最近几次偏差采样值有关,计算误差对控制 量计算的影响较小。而位置式算法要用到过去偏差的累加值,容易产生较大的累加误差。
位置式PID需要有积分限幅和输出限幅,积分限幅用来防止积分饱和,输出限幅用来保护电机,而增量式只需要输出限幅
2、增量式
(1)增量式流程图
(2)增量式公式
e(k) 误差值 = 目标值 - 反馈值
e(k - 1) 上一次的误差值
e(k - 2) 上上一次的误差值
增量式PID根据公式可以很好地看出,一旦确定了 KP、TI 、TD,只要使用前后三次测量值的偏差, 即可由公式求出控制增量,并且得出的控制量▲u(k)对应的是近几次位置误差的增量,而不是对应与实际位置的偏差 ,没有误差累加
也就是说,控制增量Δu(k)的确定仅与最近3次的采样值有关,容易通过加权处理获得比较好的控制效果,并且在系统发生问题时,增量式不会严重影响系统的工作。
3、串级PID
串级PID是将位置环和速度环串级使用,内环是速度环,外环是位置环。单一位置环是由于路线上的因素,电池的因素,每次转动的速度并不相同,这就会导致误差的发生。而串级PID就可以改善这个问题,减小误差。
(1)串级PID流程图
输入的值有三个:目标值、外环的反馈值、内环的反馈值。整个系统的目标值先作为外环(位置环)的目标值,外环的输出作为内环(速度环)的目标值,内环的输出就是整个系统的输出。反馈值都在系统运算前测出。
(2)串级PID注意事项
调试时两个环要先断开,先调试内环,在调试外环。两个环都调好后在串到一起,注意不要把输入的参数写错。
二、PID代码
1、反馈值测量
反馈值getSpeed = (float)(计数器值zj) * 1000 / 30.0f / 4 / 11;
参数解释:1000: 计数器值 / 10ms = 计数器值 * 1000
30.0f :电机减速比1:30
4:4倍频,计数器加4实际是加1
11:电机转一圈是11个脉冲
/*main函数中开启编码器*/
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);//开启TIM2编码器模式
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);//开启TIM3编码器模式
/*****************************************************************/
/*10ms定时器中断*/
int16_t zj;
float getSpeed = 0;
float Get_Speed(TIM_HandleTypeDef *Encoder_TIM_Handle)
{
zj = __HAL_TIM_GetCounter(Encoder_TIM_Handle);
__HAL_TIM_SetCounter(Encoder_TIM_Handle, 0);
getSpeed = (float)zj * 1000 / 30.0f / 4 / 11;
return getSpeed;
}
2、初始化
main函数中初始化
MyUsart_Init();
Motor_Init();
// for(int i=0; i<2; i++)
// {
// pid_init(&motor_pid[i]);
// /*pid, maxout,intergral_limit,deadband,period,max_err,target,*/
// motor_pid[i].f_param_init(&motor_pid[i],40,40,2,0,30,0,1.8f,0.05f,0);//1.8f,0.05f,0
//motor_pid[i].f_param_init(&motor_pid[i],5000,500,2,0,30,0,0.08f,0.00004f,0.015f);//0.08f,0.00004f,0.015f//位置环
// }
for(int i=0; i<2; i++)//串级PID参数
{
pid_init(&CascadeMotor_pid[i].inner,SpeedPid_calculate);
CascadeMotor_pid[i].inner.f_param_init(&CascadeMotor_pid[i].inner,40,40,250,0,30,0,1.8f,0.05f,0.0f);//1.8f,0.05f,0//速度环
pid_init(&CascadeMotor_pid[i].outer,LocationPid_calculate);
CascadeMotor_pid[i].inner.f_param_init(&CascadeMotor_pid[i].outer,5000,500,2,0,30,0,0.08f,0.0f,0.015f);//0.08f,0.00004f,0.015f//位置环0.08f,0.000001f,0.09f
}
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);//开启TIM2编码器模式
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);//开启TIM3编码器模式
MotorLocationTarget[0] = 2000;
MotorLocationTarget[1] = 2000;
// MotorTarget[0] = 30;
// MotorTarget[1] = 30;
HAL_TIM_Base_Start_IT(&htim4);
static void pid_param_init(
PID_TypeDef * pid,
uint16_t maxout,
uint16_t intergral_limit,
float deadband,
uint16_t period,
int16_t max_err,
int16_t target,
float kp,
float ki,
float kd)
{
pid->ControlPeriod = period; //没用到
pid->DeadBand = deadband;
pid->IntegralLimit = intergral_limit;
pid->MaxOutput = maxout;
pid->Max_Err = max_err;
pid->target = target;
pid->last_err = 0;
pid->llast_err = 0;
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->output = 0;
}
PID参数结构体
typedef struct _PID_TypeDef
{
float target; //目标值
float lastNoneZeroTarget;
float kp;
float ki;
float kd;
float measure; //测量值
float err; //误差
float last_err; //上次误差
float llast_err; //上上次误差
float sumErr; //误差和
float pout;
float iout;
float dout;
float output; //本次输出
float last_output; //上次输出
float MaxOutput; //输出限幅
float IntegralLimit; //积分限幅
float DeadBand; //死区(绝对值)
float ControlPeriod; //控制周期
float Max_Err; //最大误差
void (*f_param_init)(struct _PID_TypeDef *pid, //PID参数初始化
uint16_t maxOutput,
uint16_t integralLimit,
float deadband,
uint16_t controlPeriod,
int16_t max_err,
int16_t target,
float kp,
float ki,
float kd);
void (*f_pid_reset)(struct _PID_TypeDef *pid, float kp,float ki, float kd); //pid三个参数修改
int16_t (*f_cal_pid)(struct _PID_TypeDef *pid, float measure,int16_t target); //pid计算
}PID_TypeDef;
//串级PID的结构体,包含两个单级PID
typedef struct
{
PID_TypeDef inner; //内环
PID_TypeDef outer; //外环
int16_t output; //串级输出,等于inner.output
}CascadePID;
2、位置环
int16_t LocationPid_calculate(PID_TypeDef* pid, float measure,int16_t target)//位置式PID
{
pid->measure = measure;
pid->target = target;
pid->err = pid->target - pid->measure; //与pid P系数相乘。
比例误差值 当前差值=目标值-实际值
pid->sumErr += pid->err;//与pid I系数相乘。稳态误差值 误差相加作为误差总和,给积分项
//是否进入死区
// if((ABS(pid->err) > pid->DeadBand))
// {
pid->pout = (float)pid->kp * pid->err;
pid->iout = (float)pid->ki * pid->sumErr;
pid->dout = (float)pid->kd * ((float)pid->err - (float)pid->last_err);
//积分是否超出限制
if(pid->iout > pid->IntegralLimit)
pid->iout = pid->IntegralLimit;
if(pid->iout < - pid->IntegralLimit)
pid->iout = - pid->IntegralLimit;
pid->output = pid->pout + pid->iout + pid->dout; //计算PID
if(pid->output>pid->MaxOutput)
{
pid->output = pid->MaxOutput;
}
if(pid->output < -(pid->MaxOutput))
{
pid->output = -(pid->MaxOutput);
}
pid->llast_err = pid->last_err;
pid->last_err = pid->err;
pid->last_output = pid->output;
// }
return pid->output;
}
3、速度环
int16_t SpeedPid_calculate(PID_TypeDef* pid, float measure,int16_t target)//增量式,速度环
{
float proportion,differential;
pid->measure = measure;
pid->target = target;
pid->err = pid->target - pid->measure;
proportion = (float)pid->err - (float)pid->last_err; //计算比例项
differential = (float)pid->err - 2 * (float)pid->last_err + (float)pid->llast_err; //计算微分项
pid->pout = (float)pid->kp * proportion;
pid->iout = (float)pid->ki * pid->err;
pid->dout = (float)pid->kd * differential;
pid->output = (float)pid->last_output + pid->pout + pid->iout + pid->dout; //计算PID
//是否进入死区
// if((ABS(pid->err) > pid->DeadBand))
// {
if(pid->output>pid->MaxOutput)
{
pid->output = pid->MaxOutput;
}
if(pid->output < -(pid->MaxOutput))
{
pid->output = -(pid->MaxOutput);
}
pid->llast_err = pid->last_err;
pid->last_err = pid->err;
pid->last_output = pid->output;
// }
return pid->output;
}
float speed[2] = {0};
int16_t set_speed[2] = {0};
static void PID_Speed(void)
{
speed[0] = Get_Speed(&Encoder1_TIM_Handle);
set_speed[0] = motor_pid[0].f_cal_pid(&motor_pid[0],speed[0],MotorTarget[0]);
speed[1] = Get_Speed(&Encoder2_TIM_Handle);
set_speed[1] = motor_pid[1].f_cal_pid(&motor_pid[1],speed[1],MotorTarget[1]);
LeftMotor(set_speed[0]);
RightMotor(set_speed[1]);
printf("%f,%f,%d\n",speed[0],speed[1],MotorTarget[0]);//发送数据给上位机
}
float location[2] = {0};
int16_t set_location[2] = {0};
static void PID_Location(void)
{
location[0] += Get_Speed(&Encoder1_TIM_Handle);
location[1] += Get_Speed(&Encoder2_TIM_Handle);
set_location[0] = motor_pid[0].f_cal_pid(&motor_pid[0],location[0],MotorLocationTarget[0]);
set_location[1] = motor_pid[1].f_cal_pid(&motor_pid[1],location[1],MotorLocationTarget[1]);
LeftMotor(set_location[0]);
RightMotor(set_location[1]);
printf("%f,%f,%d\n",location[0],location[1],MotorLocationTarget[1]);
}
4、串级PID
/*放到10ms定时器中断*/
float locationOuter[2] = {0};
int16_t set_locationOuter[2] = {0};
float speedInner[2] = {0};
int16_t set_speedInner[2] = {0};
static void PID_Cascade(void)//串级PID
{
speedInner[0] = Get_Speed(&Encoder1_TIM_Handle);
speedInner[1] = Get_Speed(&Encoder2_TIM_Handle);
locationOuter[0] += speedInner[0];
locationOuter[1] += speedInner[1];
set_locationOuter[0] = CascadeMotor_pid[0].outer.f_cal_pid(&CascadeMotor_pid[0].outer,locationOuter[0],MotorLocationTarget[0]);
set_locationOuter[1] = CascadeMotor_pid[1].outer.f_cal_pid(&CascadeMotor_pid[1].outer,locationOuter[1],MotorLocationTarget[1]);
set_speedInner[0] = CascadeMotor_pid[0].inner.f_cal_pid(&CascadeMotor_pid[0].inner,speedInner[0],set_locationOuter[0]);
set_speedInner[1] = CascadeMotor_pid[1].inner.f_cal_pid(&CascadeMotor_pid[1].inner,speedInner[1],set_locationOuter[1]);
LeftMotor(set_speedInner[0]);
RightMotor(set_speedInner[1]);
printf("%f,%f,%d,%f,%f,%d,%d\n",locationOuter[0],locationOuter[1],MotorLocationTarget[1],speedInner[0],speedInner[1],set_locationOuter[0],set_locationOuter[1]);
}
三、PID调参
1、上位机软件Vofa+
下载链接:https://www.vofa.plus/downloads/
使用介绍:https://www.vofa.plus/docs/learning/start/quick_start
2、调参
(1)位置式
在调整PID控制器时,通常首先从比例(P)开始,以赋予系统基本的响应能力。当系统开始出现震荡时,适当减小P值以稳定系统。接下来,调整积分(I)项,其大小应根据系统的响应能力来设定。可以先假设一个阈值,通过计算与系统实际响应能力对比后,调整到一个合适的I值。有时,仅调整I值后系统就能达到预期性能。此时,如果需要,可以进一步调整微分(D)项。
对于微分(D)项,通常在系统震荡时减小D值以获得更好的稳定性。调整顺序并非固定,有时也可以先调整P使系统过冲,然后通过调整D来限制震荡,最后微调I以补足控制效果。
当I项调整到足够时,可以适当降低P值;而当D项调整得当时,可以适度增加P值。这里的增加或减少都应该是细微的调整,而不是大幅度变动。
(2)增量式
首先,调整积分(I)参数,设定一个初始值并观察系统的响应。如果系统能够收敛,尝试逐渐增加I参数以加快收敛速度。如果收敛速度过快或过慢,相应地调整I参数直到找到一个适中的收敛趋势。一旦系统表现出良好的收敛性,再引入比例(P)参数。
对于比例(P)参数,观察系统是否会出现震荡。设定一个初始P值,如果震荡幅度减小,可以适当增加P值;如果震荡幅度增大,则减小P值。需要不断微调P和I参数,以达到最佳效果。例如,如果调整P后效果不明显,可以适当减小I值,然后再次调整P。
至于增量式PID中的微分(D)参数,调试中对系统变化不太明显。
四、PID使用注意事项
1、PID是用来计算和控制运动,使用时应该只改变目标值。
2、避免在计算过程中使用延时,避免测量脉冲的卡顿导致疯转。