1 what? 什么是pid算法
2 why? 为什么要用?pid算法?PID算法的作用?
2.1 KP控制
2.2KD控制
2.3 KI控制
3 how? 如何使用pid算法(过程 编码)
3.1 口诀
3.2算法选择
3.3 代码实现
写在前面
最近业务需要,电机控制相关,所以打算对PID重新学习算法。本文整理PID算法的概念、功能和代码实现,有需要的学生可以编译文章的最终代码进行测试。
1 what? 什么是pid算法
PID即:Proportional(比例)、Integral(积分)、Differential缩写(微分)。顾名思义,PID控制算法是将比例、积分和微分相结合的控制算法,pid算法也是闭环反馈控制算法。因此,硬件必须有闭环反馈单元,即反馈。例如,控制电机转速时,必须有传感器测量转速,并将反馈信号(转速)反馈给控制单元。
连续控制系统在工业过程中的理想PID控制算法如下:
Kp |
比例增益,Kp与比例成倒数关系; |
Tt |
积分时间常数; |
TD |
微分时间常数; |
u(t) |
PID控制器输出信号; |
e(t) |
给定值r(t)与测量值的差异。 |
可以认为,PID流程本质:通过误差反馈信号控制被控量,控制器本身是比例、积分和微分的效果加和。
2 why? 为什么要用?pid算法?PID算法的作用?
例如,控制恒温器,将水温保持在45℃。
也许你在想:
小菜一碟,小于45℃让它加热,超过45°c就断电,几行代码用STM32 分分钟写出来。
没错 ! 如果场景要求不高,真的可以这样做~ But! 换句话说,你就知道问题出在哪里了:
如果控制对象是汽车呢?如果你想保持汽车的速度 100km/h 巡航,你敢这样做吗?
想象一下,如果汽车的定速巡航计算机在某个时间测量速度为90km/h。
接到欠速反馈后,域控制器立即命令发动机:加速!
结果,100%全油门突然来到发动机侧,汽车加速到130km/h。
&nsp; 域控制器接到超速速反馈之后,立刻命令制动系统:刹车!
这样多来几次,地上会多几条胎印,如果车上有人,估计还会留下一摊呕吐物。实际应用过程中,没有一下厂家敢这么干。
所以,在大多数场合中,用“开关量”来控制一个物理量,就显得比较简单粗暴了。有时候,是无法保持稳定的。因为单片机、传感器不是无限快的,采集、控制需要时间,也就是我们所常说的,滞后性。
而且,控制对象具有惯性。比如你把恒温器开关拔掉,它的“余热”(即热惯性)可能还会使水温继续升高一小会。
这时,就需要一种『算法』:
它可以将需要控制的物理量带到目标附近
它可以“预见”这个量的变化趋势
它也可以消除因为散热、阻力等因素造成的静态误差
于是,当时的数学家们发明了这一历久不衰的算法——这就是PID。
2.1 KP控制
需要控制定速巡航速度,有它现在的『当前值80km/h』,也有我们期望的『100km/h』。
当『当前值』 < 『目标值』:且两者差距不大时,发动机“轻轻地”加速一下。
当『当前值』 < 『目标值』:速度降低很多,两者差距大,发动机“稍稍用力”加速一下。
当『当前值』 < 『目标值』:要是当速度比目标速度低得多,就让发动机“开足马力”加速,尽快让速度到达巡航速度附近。
当『当前值』 > 『目标值』:这个时候,松开油门,不用加速了。
这就是P的作用,跟开关控制方法相比,是不是“温文尔雅”了。但是kP越大,调节作用越激进,kP调小会让调节作用更保守。
2.2 KD控制
仍然是巡航的场景,刚才有了KP的作用。不难发现,只有KP,车速时加速时减速,晃晃悠悠,整个系统不是特别稳定,总是在“抖动”。
设想一个弹簧:现在在平衡位置上。拉它一下,然后松手。这时它会震荡起来。因为阻力很小,它可能会震荡很长时间,才会重新停在平衡位置。
请想象一下:要是把上图所示的系统浸没在水里,同样拉它一下 :这种情况下,重新停在平衡位置的时间就短得多。
我们需要一个控制作用,让被控制的物理量的“变化速度”趋于0,即类似于“阻尼”的作用。
因为,当比较接近目标时,P的控制作用就比较小了。越接近目标,P的作用越温柔。什么意思呢,就像巡航速度快接近设定的巡航速度,这个时候,理想速度和实际速度得差值变得更小,再乘上一个kp,这个值得影响变得更小,晃动的程度也变小了。
但是有很多内在的或者外部的因素,使控制量发生小范围的摆动。
kD的作用就是让这个小范围摆动的物理量趋于0,只要什么时候,这个量具有了速度,kD就向相反的方向用力,尽力刹住这个变化。
kD参数越大,向速度相反方向刹车的力道就越强。(反应到车速场景上来说,就是油门的加减)。
2.3 KI控制
这个参数的话,用巡航场景不是特别好理解,可以借助水温的场景进行理解。
还是说水温的时,比如冬天你讲恒温器放到东北的室外,仍然要把水温烧到45°c。
但是有个尴尬的情况:在KP的作用下,水温慢慢升高。直到升高到35℃时,天气太冷,水散热的速度,和P控制的加热的速度相等了。这就尴尬了。
KP这样想:我和目标已经很近了,只需要轻轻加热就可以了。
KD这样想:加热和散热相等,温度没有波动,我好像不用调整什么。
于是,水温永远只能达到35°c,但这些都只是计算机的逻辑。作为一个有思想,有常识的人类,我们明白,如果要将这水烧到45°c,我们还需要继续加热,可是加热 的力度应该多大呢。
数学家们设置了一个积分量,只要偏差存在,就不断地对偏差进行积分(累加),并反应在调节力度上。
这样一来,即使35℃和45℃相差不太大,但是随着时间的推移,只要没达到目标温度,这个积分量就不断增加。系统就会慢慢意识到:还没有到达目标温度,该增加功率啦!
到了目标温度后,假设温度没有波动,积分值就不会再变动。这时,加热功率仍然等于散热功率。但是,温度是妥妥的45℃。所以说KI的作用就是,减小静态情况下的误差,让受控物理量尽可能接近目标值。
kI的值越大,积分时乘的系数就越大,积分效果越明显。
3 how? 如何使用pid算法(过程+编码)
3.1口诀
参数整定找最佳,从小到大顺序查,
先是比例后积分,最后再把微分加,
曲线振荡很频繁,比例度盘要放大,
曲线漂浮绕大湾,比例度盘往小扳,
曲线偏离回复慢,积分时间往下降,
曲线波动周期长,积分时间再加长,
曲线振荡频率快,先把微分降下来,
动差大来波动慢,微分时间应加长,
理想曲线两个波,前高后低四比一,
一看二调多分析,调节质量不会低
3.2 算法选择
最简单的闭环控制只有 P 控制,将当前结果反馈回来,再与目标相比,为正的话,就减速,为负的话就加速。pid 是比例(P)、积分(I)、微分(D)控制算法。但并不是必须同时具备这三种算法,也可以是 PD,PI,甚至只有 P 算法控制。PID 算法的结构
采用 P 比例控制,能较快地克服扰动的影响,作用于输出值较快的场景,但不能很好稳定在一个理想的数值。
它适用于一阶惯性对象,负荷变化不大,工艺要求不高、如用于压力、液位、串级副控回路,控制要求不高、被控参数允许在一定范围内有余差的场景。如:热水器水位控制等。
比例积分控制也是应用最广泛的控制算法之一。积分能在比例控制的基础上消除余差;
它适用于被控参数不允许有余差的场景。如:油库供油管流量控制系统等。
微分控制具有超前预判的功能,对于惯性较大的对象,为了使控制及时,常常希望能根据被控变量变化的快慢来控制。响应快,偏差小,能增加系统稳定性,有超前控制作用,可以克服对象的惯性
微分作用与偏差变化率成比例,即它是根据偏差变化趋势产生控制作用,因而有“预先控制”的性质。俗称超前调节。微分作用的超前特性,只对广义对象的容量滞后有效。而对很大的纯滞后无效。
PID 控制是一种较理想的控制规律,它在比例的基础上引入积分,可以消除余差,再加入微分作用,又能提高系统的稳定性。它适用于控制通道时间常数或容量滞后较大、控制要求较高的场合。如过热蒸汽温度控制,PH值控制等。
3.3 代码实现
代码的实现过程中,使用了位式和增量式两种算法进行实现,下面的代码直接可以使用,有需要的小伙伴可以自行下载实验,对于仿真的实验后续如果有空也会更新上来。
其实算法的实现过程,可以根据具体的场景进行变化,比如同一个过程,但是涉及到不同的场景,比如开车:加速+巡航+减速,在不同场景中,我们可以叠加不同的算法,也叫做算法分离。
#include <stdio.h>
#include <stdlib.h>
/*
对于位式和增量式那种算法好:具体场景具体分析
*/
typedef struct
{
float kp; // 比例系数
float ki; // 积分系数
float kd; // 微分系数
float err_last; // 上次误差
float err_sum; // 误差累计
float result;
}pid_pos_typedef;
typedef struct
{
float kp; // 比例系数
float ki; // 积分系数
float kd; // 微分系数
float err_last; // 上一次的误差
float err_pree; // 上二次的误差
float result;
}pid_delta_typedef;
void pid_delta_init(pid_delta_typedef* pid, float kp, float ki, float kd);
float pid_delta_calc(pid_delta_typedef* pid, float currVal, float objVal);
void pid_pos_init(pid_pos_typedef* pid, float kp, float ki, float kd);
float pid_pos_calc(pid_pos_typedef* pid, float currVal, float objVal);
// 位置式pid算法初始化
void pid_pos_init(pid_pos_typedef* pid, float kp, float ki, float kd)
{
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->result = 0;
pid->err_last = 0;
pid->err_sum = 0;
}
// 位置式pid算法计算
float pid_pos_calc(pid_pos_typedef* pid, float currVal, float objVal)
{
float err_c = objVal - currVal; // 当前误差
pid->err_sum += err_c; // 误差累计
pid->result = pid->kp * err_c + pid->ki * pid->err_sum + pid->kd * (err_c - pid->err_last);
pid->err_last = err_c;
return pid->result;
}
// 增量式pid算法初始化
void pid_delta_init(pid_delta_typedef* pid, float kp, float ki, float kd)
{
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->result = 0;
pid->err_last = 0;
pid->err_pree = 0;
}
// 增量式算法计算
float pid_delta_calc(pid_delta_typedef* pid, float currVal, float objVal)
{
float err_c; // 当前误差
float err_p; // p误差
float err_i; // i误差
float err_d; // d误差
float increment; // 增量
err_c = objVal - currVal;
err_p = err_c - pid->err_last;
err_i = err_c;
err_d = err_c - 2 * pid->err_last + pid->err_pree;
increment = pid->kp * err_p + pid->ki * err_i + pid->kd * err_d;
pid->err_pree = pid->err_last;
pid->err_last = err_c;
pid->result += increment;
return pid->result;
}
void mydelay(int ms){
int i;
for(;i<1000000*ms;i++)
{
}
}
int main(void)
{
unsigned int i = 0;
float currVal = 0; // 当前值
float objVal = 10; // 目标值
pid_delta_typedef pid_delta;
pid_pos_typedef pid_pos;
pid_delta_init(&pid_delta, 0.2, 0.001, 0.0001);
pid_pos_init(&pid_pos, 0.2, 0.001, 0.0001);
printf("------ Start. \n");
while(1)
{
i++;
/*从这边可以对比出到第是哪一种算法会更加好*/
//currVal = pid_delta_calc(&pid_delta, currVal, objVal);
currVal = pid_pos_calc(&pid_pos, currVal, objVal);
printf("[%d] objVal[%f] currVal:%f \n", i, objVal, currVal);
if(currVal>9.998990){
mydelay(100);//方便观察
}
if(currVal >= 9.999)
break;
}
printf("------ End. \n");
return 0;
}