0、前言
我这个文章慢慢更新,我现在也是学习阶段,后面会慢慢更新
1、 PID的概述
在我们学习PID之前在程序经常会写
if(温度<温度阈值)
{
加热
}
else
{
不加热
}
他就是直接判断当前的测量值是否跟我的阈值相同,如果相同做什么,不相同就做什么。这样虽然可以做出判断保证这个事件有被处理但是不精确,就比如这个加热模块,我要保证这个温度保持在这个阈值上,如果我当前温度大于阈值我要停止加热,但是由于我保险丝或者加热器还有温度,这个温度就会影响我现在这个温度,导致我们测量的温度会比我们想要的温度高那么一点点,反之亦然。
在我们PID算法中,我们就会尽量减少这种情况,通过控制器减少这种误差,让他保持在我们想要的数据附件。
PID:Proportional(比例)、Integral(积分)、Differential(微分)的缩写。
我们下面说的都是对电机测速
1、P 比例控制
比例控制是一种最简单的控制方式。其控制器的输出与输入误差信号成比例关系。当仅有比例控制时系统输出存在稳态误差。比例项输出:
这个公式的意思是Pout是输出值、Kp是系数我们规定的, e(t)是误差值
我们规定一个我们的期望值(target),还有一个我们单片机测量的值(measure)
e(t) = target - measure
比例控制系统快速响应
如果只用P算法 输出的Pout直接作用给PWM,所以因为线性关系Kp直接决定这个调节的速度。
说明
- Kp越大说明 他没有达到目标他就越容易接近目标、但是相反如果在目标附近他就会产生很大的增幅。
- 如果这个系统存在外力,就是反作用于系统的元素,会导致系统存在静态误差(就是我的增量被我的反作用于系统的东西抵消了,如:我给了系统5N的力让他靠近目标值,但是这个系统还存在5N的摩擦力刚好抵消)
所以,单独的比例控制,在很多时候并不能满足要求。
2、I 积分控制
刚刚说P算法存在静态误差,I算法就是解决这个静态误差的,积分就是把每一次的误差值e(t)加起来,然后慢慢的累加起来,抵消静态误差
Ki 是自己设定的值
积分控制系统的准确性,消除累积的误差,输出到达目标值
积分项是误差对时间的积分,随着时间的增加,积分项会增大。
这样,即便误差很小,积分项也会随着时间的增加而加大,它推动控制器的输出增大使稳态误差进一步减小,直到等于零。
因此,比例积分(PI)控制器,可以使系统在进入稳态后无稳态误差。
不同积分增益 Ki 下, 受控变量的阶跃响应(Kp, Kd 维持稳定值)
相对于P算法来说 I 算法也是对输出起到促进作用,但是作用的强度低于P算法。
3、D 微分控制
D 算法就是相对于P和I算法的,作用是减少P和I的震荡效果
这次误差-上次误差
在微分控制中,控制器的输出与输入误差信号的微分成正比关系。微分调节就是偏差值的变化率。使用微分调节能够实现系统的超前控制。如果输入偏差值线性变化,则在调节器输出侧叠加一个恒定的调节量。大部分控制系统不需要调节微分时间。因为只有时间滞后的系统才需要附加这个参数。
也就是说,他是调节我们变化的快慢的,准确说是抑制,但P算法和I算法快速调节的时候他会减慢他们调节的程度。也就是减少震荡
4、pid算法
将P、I、D三个算法加起来就是我们的PID算法
2、位置式 PID 算法
位置式PID是当前系统的实际位置,与你想要达到的预期位置的偏差,进行PID控制
特点:
- 所以每次输出均与过去状态有关,计算时要对ek进行累加,工作量大
- 如果误差值一直是正的或者负的,位置式PID在积分项会一直累积,所以在使用时,必须对积分项进行限幅,同时也要对输出进行限幅。当偏差开始反向变化,位置式PID在积分项需要一段时间才能从最大值减下来,造成输出的滞后。(可以使用积分分离的思想,就是在保持稳定之前用pd算法,在保持稳定之后加入I算法)
pid.c
#include "pid.h"
#include "main.h"
// pid结构体的初始化
void PIDStructInit(_PID *pid,float target,float p,float i,float d)
{
pid->p = p;
pid->i = i;
pid->d = d;
pid->target = target;
pid->err_curr = 0;
pid->err_last = 0;
pid->err_sum = 0;
}
// pid 位置式计算
float PID_Postion(_PID *pid,float current)
{
volatile float Pout; // 计算的输出值
pid->err_curr = pid->target - current; // 计算当前误差
pid->err_sum += pid->err_curr; //积分
Pout = (pid->p * pid->err_curr) + (pid->i * pid->err_sum) + (pid->d * (pid->err_curr - pid->err_last)); // 计算pid的值
printf("P = %.3f I = %.3f D = %.3f\r\n",(pid->p * pid->err_curr) , (pid->i * pid->err_sum) , (pid->d * (pid->err_curr - pid->err_last)));
pid->err_last = pid->err_curr; // 记录上一次的误差为当前这一次
return Pout;
}
pid.h
#ifndef _PID_H
#define _PID_H
typedef struct _PID
{
volatile float p;
volatile float i;
volatile float d;
float err_curr; // 当前误差
float err_last; // 上次误差
float err_sum; // 误差和 积分
float target; // 目标值
}_PID;
void PIDStructInit(_PID *pid,float target,float p,float i,float d); // 结构体初始化
float PID_Postion(_PID *pid,float current); // 计算位置pid的值
#endif
3、增量式 PID 算法
增量式 PID 是对近几次的数据进行对比,并不像位置式 PID 对之前所有的误差值进行积分,这样就避免了位置式PID的缺陷,但是在实际开发中位置式和增量式要根据但是环境进行选择
特点
- ▲u(k)对应的是近几次位置误差的增量,而不是对应与实际位置的偏差,没有误差累加
- 积分是不需要累加前面所有的误差值的,增量Δu(k)的确定仅与最近3次的采样值有关,所以在系统发生问题时,增量式不会严重影响系统的工作
typedef struct _PID
{
volatile float p;
volatile float i;
volatile float d;
float err_curr; // 当前误差
float err_last; // 上次误差
float err_sum; // 误差和 积分
float err_last_last; // 上上次的误差
float target; // 目标值
float Out; // 增量 输出的值
}_PID;
// pid结构体的初始化
void PIDStructInit(_PID *pid,float target,float p,float i,float d)
{
pid->p = p;
pid->i = i;
pid->d = d;
pid->target = target;
pid->err_curr = 0;
pid->err_last = 0;
pid->err_last_last = 0;
pid->err_sum = 0;
pid->Out = 0;
}
// 增量式PID 计算
float PID_Incremental(_PID *pid,float current)
{
volatile float Pout; // 计算的输出值
pid->err_curr = pid->target - current; // 计算当前误差
Pout = (pid->p * (pid->err_curr - pid->err_last)) + (pid->i * pid->err_last) + (pid->d * ((pid->err_curr - 2* pid->err_last + pid->err_last_last))); // 计算pid的值
printf("P = %.3f I = %.3f D = %.3f\r\n",(pid->p * (pid->err_curr - pid->err_last)) , (pid->i * pid->err_last) , (pid->d * ((pid->err_curr - 2* pid->err_last + pid->err_last_last))));
pid->err_last_last = pid->err_last; // 上上次更新为上次
pid->err_last = pid->err_curr; // 将上次的值更新为这次
pid->Out += Pout;
return pid->Out;
}
4、什么时候用增量式、什么时候用位置式
我的理解就是
- 如果你要的是速度这种线性变化的可以用增量式。
- 如果你要的是那种向我要知道那种位置我当前的位置信息,随机变化很大的可以用位置式
5、 我现在搞好的 pid 驱动(后面还会完善)
// pid.c
#include "pid.h"
#include "main.h"
// pid结构体的初始化
void PIDStructInit(_PID *pid,float target,float p,float i,float d)
{
pid->p = p;
pid->i = i;
pid->d = d;
pid->target = target;
pid->err_curr = 0;
pid->err_last = 0;
pid->err_last_last = 0;
pid->err_sum = 0;
pid->Out = 0;
}
// pid 位置式计算
float PID_Postion(_PID *pid,float current)
{
volatile float Pout; // 计算的输出值
pid->err_curr = pid->target - current; // 计算当前误差
pid->err_sum += pid->err_curr; //积分
Pout = (pid->p * pid->err_curr) + (pid->i * pid->err_sum) + (pid->d * (pid->err_curr - pid->err_last)); // 计算pid的值
printf("P = %.3f I = %.3f D = %.3f\r\n",(pid->p * pid->err_curr) , (pid->i * pid->err_sum) , (pid->d * (pid->err_curr - pid->err_last)));
pid->err_last = pid->err_curr; // 记录上一次的误差为当前这一次
return Pout;
}
// 增量式PID 计算
float PID_Incremental(_PID *pid,float current)
{
volatile float Pout; // 计算的输出值
pid->err_curr = pid->target - current; // 计算当前误差
Pout = (pid->p * (pid->err_curr - pid->err_last)) + (pid->i * pid->err_last) + (pid->d * ((pid->err_curr - 2* pid->err_last + pid->err_last_last))); // 计算pid的值
printf("P = %.3f I = %.3f D = %.3f\r\n",(pid->p * (pid->err_curr - pid->err_last)) , (pid->i * pid->err_last) , (pid->d * ((pid->err_curr - 2* pid->err_last + pid->err_last_last))));
pid->err_last_last = pid->err_last; // 上上次更新为上次
pid->err_last = pid->err_curr; // 将上次的值更新为这次
pid->Out += Pout;
return pid->Out;
}
// pid.h
#ifndef _PID_H
#define _PID_H
typedef struct _PID
{
volatile float p;
volatile float i;
volatile float d;
float err_curr; // 当前误差
float err_last; // 上次误差
float err_sum; // 误差和 积分
float err_last_last; // 上上次的误差
float target; // 目标值
float Out; // 增量 输出的值
}_PID;
void PIDStructInit(_PID *pid,float target,float p,float i,float d); // 结构体初始化
float PID_Postion(_PID *pid,float current); // 计算位置pid的值
float PID_Incremental(_PID *pid,float current); // 增量式PID 计算
#endif