
主要的PID算法优化有以上几种,为了找出适用于平衡车的改进方法,可以一个一个尝试。输出偏移、微分先行、积分限幅比较有用。
输出偏移

目前平衡车在直立时会小距离来回移动,无法较好地静止,原因一是平衡车使用的是直流减速电机,减速箱的齿轮有间隙,当电机没有转动时轮子也可以凭借齿轮间隙而小幅度转动,所以控制不精确;二是当输出PWM较小时控制力度小到无法克服轮子摩擦力,需要PID积分项不断增大才能控制,造成响应有延迟。
所以可以使用输出偏移,让PID输出值直接跳过控制力度小的阶段,除了输出0以外所有输出值都能控制小车。
输出偏移作用于角度环,其输出值控制平均PWM。
在PID结构体中新定义一个输出偏移Outoffset
cs
typedef struct { //定义PID结构体变量类型
float Target; //目标值,由用户设定
float Actual; //实际值,从传感器读取
float Actual1; //上次实际值
float Out; //输出值,作用于执行器
float Kp; //比例项权重
float Ki; //积分项权重
float Kd; //微分项权重
float Error0; //本次误差
float Error1; //上次误差
float ErrorInt; //误差积分
float ErrorIntMax; //误差积分的最大值
float ErrorIntMin; //误差积分的最小值
float OutMax; //输出限幅的最大值
float OutMin; //输出限幅的最小值
float OutOffset; //输出偏移
} PID_t;
随后在角度环的结构体赋值中加入out offset的赋值,后面再来调参。
cs
PID_t AnglePID = { //角度环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 5, //比例项权重
.Ki = 0.1, //积分项权重
.Kd = 5, //微分项权重
.OutMax = 100, //输出限幅的最大值
.OutMin = -100, //输出限幅的最小值
.OutOffset = 3, //输出偏移
};
接下来是最关键的一步,在角度环PID计算出输出值之后 或者 输出值赋值给平均PWM之前,加入输出偏移
cs
/*输出偏移*/
if (p->Out > 0) {p->Out += p->OutOffset;} //如果输出值为正,则加上结构体指定的OutOffset
if (p->Out < 0) {p->Out -= p->OutOffset;} //如果输出值为负,则减去结构体指定的OutOffset
最后就是调参,这里依然使用小程序的滑杆调参,并在OLED屏幕显示一下out offset。
cs
else if (strcmp(Name, "Offset") == 0) //如果滑杆名称是Offset
{
AnglePID.OutOffset = atof(Value); //则把滑杆值赋值给OutOffset
}
cs
OLED_Printf(56, 56, OLED_6X8, "Offset:%02.0f", AnglePID.OutOffset); //显示输出偏移值
一边用滑杆调参一边观察小车情况,最后将合适参数写入角度环结构体。同时还可以加大角度环的K项和D项,最后小车已经可以较好地静止了,但不能做到完全静止,这是齿轮间隙的缘故,如果换成无刷电机就可以完全静止,因为它没有齿轮,可以直接驱动。
微分先行


微分先行也作用于角度环,因为只有它调节了D项。
在PID结构体中新定义一个上次实际值Actual1
cs
typedef struct { //定义PID结构体变量类型
float Target; //目标值,由用户设定
float Actual; //实际值,从传感器读取
float Actual1; //上次实际值
float Out; //输出值,作用于执行器
float Kp; //比例项权重
float Ki; //积分项权重
float Kd; //微分项权重
float Error0; //本次误差
float Error1; //上次误差
float ErrorInt; //误差积分
float ErrorIntMax; //误差积分的最大值
float ErrorIntMin; //误差积分的最小值
float OutMax; //输出限幅的最大值
float OutMin; //输出限幅的最小值
float OutOffset; //输出偏移
} PID_t;
随后在计算PID的PID更新函数中加入获取Actual1以及微分先行的过程。由于每次进入函数时实际值的数值已经更新,所以不能像误差一样在函数开头就加入Actual1,必须在最后加入。
cs
/*PID计算*/
/*使用位置式PID公式,计算得到输出值*/
p->Out = p->Kp * p->Error0
+ p->Ki * p->ErrorInt
// + p->Kd * (p->Error0 - p->Error1); //普通PID计算公式
- p->Kd * (p->Actual - p->Actual1); //微分先行计算公式
/*输出偏移*/
if (p->Out > 0) {p->Out += p->OutOffset;} //如果输出值为正,则加上结构体指定的OutOffset
if (p->Out < 0) {p->Out -= p->OutOffset;} //如果输出值为负,则减去结构体指定的OutOffset
/*输出限幅*/
if (p->Out > p->OutMax) {p->Out = p->OutMax;} //限制输出值最大为结构体指定的OutMax
if (p->Out < p->OutMin) {p->Out = p->OutMin;} //限制输出值最小为结构体指定的OutMin
/*获取上次实际值*/
p->Actual1 = p->Actual; //本轮计算后进行变量传递,下轮计算时Actual1即为上次实际值
}
最后在PID初始化函数中加入Actual1并清零
cs
/**
* 函 数:PID初始化
* 参 数:p 指定结构体的地址
* 返 回 值:无
*/
void PID_Init(PID_t *p)
{
/*把PID表示状态的参数清零,避免之前遗留的参数对本次启动造成影响*/
p->Target = 0;
p->Actual = 0;
p->Actual1 = 0;
p->Out = 0;
p->Error0 = 0;
p->Error1 = 0;
p->ErrorInt = 0;
}
改进后控制小车表面上没有什么明显的作用,但实际上对小车的控制变得更加顺滑了。
积分限幅

如果一边操控摇杆让小车前进,一边又按住小车,不让它动,那么此时误差疯狂累计,积分项已经很大了,一松手小车就会因为积分项的大力调控而冲出去,不止速度环,角度环和转向环也需要此类改进。
在PID结构体中定义误差积分的最值
cs
typedef struct { //定义PID结构体变量类型
float Target; //目标值,由用户设定
float Actual; //实际值,从传感器读取
float Actual1; //上次实际值
float Out; //输出值,作用于执行器
float Kp; //比例项权重
float Ki; //积分项权重
float Kd; //微分项权重
float Error0; //本次误差
float Error1; //上次误差
float ErrorInt; //误差积分
float ErrorIntMax; //误差积分的最大值
float ErrorIntMin; //误差积分的最小值
float OutMax; //输出限幅的最大值
float OutMin; //输出限幅的最小值
float OutOffset; //输出偏移
} PID_t;
在计算PID的更新函数中,计算积分项后加入积分限幅
cs
/*外环误差积分(累加)*/
/*如果Ki不为0,才进行误差积分,这样做的目的是便于调试*/
/*因为在调试时,我们可能先把Ki设置为0,这时积分项无作用,误差消除不了,误差积分会积累到很大的值*/
/*后续一旦Ki不为0,那么因为误差积分已经积累到很大的值了,这就导致积分项疯狂输出,不利于调试*/
if (p->Ki != 0) //如果Ki不为0
{
p->ErrorInt += p->Error0; //进行误差积分
/*误差积分限幅*/
if (p->ErrorInt > p->ErrorIntMax) {p->ErrorInt = p->ErrorIntMax;} //限制误差积分最大为结构体指定的ErrorIntMax
if (p->ErrorInt < p->ErrorIntMin) {p->ErrorInt = p->ErrorIntMin;} //限制误差积分最小为结构体指定的ErrorIntMin
}
else //否则
{
p->ErrorInt = 0; //误差积分直接归0
}
在三个环的结构体中都加入误差积分的最值
cs
PID_t AnglePID = { //角度环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 5, //比例项权重
.Ki = 0.1, //积分项权重
.Kd = 5, //微分项权重
.OutMax = 100, //输出限幅的最大值
.OutMin = -100, //输出限幅的最小值
.OutOffset = 3, //输出偏移
.ErrorIntMax = 600, //误差积分的最大值
.ErrorIntMin = -600, //误差积分的最小值
};
PID_t SpeedPID = { //角度环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 2, //比例项权重
.Ki = 0.05, //积分项权重
.Kd = 0, //微分项权重
.OutMax = 20, //输出限幅的最大值
.OutMin = -20, //输出限幅的最小值
.ErrorIntMax = 150, //误差积分的最大值
.ErrorIntMin = -150, //误差积分的最小值
};
PID_t TurnPID = { //转向环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 4, //比例项权重
.Ki = 3, //积分项权重
.Kd = 0, //微分项权重
.OutMax = 50, //输出限幅的最大值
.OutMin = -50, //输出限幅的最小值
.ErrorIntMax = 20, //误差积分的最大值
.ErrorIntMin = -20, //误差积分的最小值
};
最值的确定可以通过串口打印或绘图等方式先观察正常情况下的速度值、角度值、转向值,最后在此基础上扩大一定范围来定最值。
加入后如果再出现开始那种情况,积分就不会大到离谱,系统恢复时也不会失控了。