1.PID简介
PID是控制领域相当经典且重要的控制算法。
PID就是“比例(proportional)、积分(integral)、微分(derivative)”,是一种很常见的控制算法。它应用的范围相当之广。小到我们玩的无人机,平衡车,大到工业领域的温度、液位、流量控制,都出现了PID的身影。
1.1个人对PID的理解
个人理解的PID就是一个“跟随”算法。
让被控对象,比如温度,水位,速度,高度,压力等到达我们期望的值,并且保持住。
PID在实际使用中更像一个数学的二元函数,你只要输入你想要的值,当前值,在调节好参数的情况下,无需关心内部过程,直接使用PID算出的结果值送入到执行机构即可。
PID有3个值,设定值(期望值,给定值),实际值(测量值),输出值(结果值)。
这是不是很像我们数学中经常使用的函数,y=f(x,y),输入期望值,当前值,得到结果。
1.2 PID变形以及参考代码
PID存在非常多的变形,最基本的PID变形是增量式PID和位置式PID。还有其他的变形,如去掉PID某一部分的PD算法,PI算法,再有更高端一些的抗饱和PID,微分先行PID,自适应PID,还有模糊PID。这些PID算法万变不离其宗,只要掌握了基本PID使用规律,用什么样的PID都是一样的。
位置式和增量式PID略有区别,主要区别如下:
位置式PID控制器的输出是一个绝对值,即控制器对系统的控制作用量的绝对大小。它根据比例项、积分项和微分项直接计算出最终的输出值。
而增量式PID控制器的输出是一个增量值,即控制器对系统的控制作用量的变化量。它根据比例增量、积分增量和微分增量来计算最终的输出增量,然后将该增量值与前一时刻的输出值相加得到当前的输出值。
位置式PID的c语言参考代码如下:
// PID控制器结构体
typedef struct
{
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float setpoint; // 设定值
float integral; // 积分项
float prev_error; // 上一次误差
} PID_Controller;
// PID控制器初始化函数
void PID_Init(PID_Controller *pid, float Kp, float Ki, float Kd, float setpoint)
{
pid->Kp = Kp;
pid->Ki = Ki;
pid->Kd = Kd;
pid->setpoint = setpoint;
pid->integral = 0.0f;
pid->prev_error = 0.0f;
}
// PID控制器计算函数
float PID_Compute(PID_Controller *pid, float measured_value)
{
float error = pid->setpoint - measured_value; // 计算误差
pid->integral += error; // 更新积分项
float derivative = error - pid->prev_error; // 计算微分项
pid->prev_error = error; // 更新上一次误差
float output = pid->Kp * error +
pid->Ki * pid->integral +
pid->Kd * derivative; // 计算PID输出
return output;
}
增量式PID的c语言参考代码如下:
// 增量式PID控制器结构体
typedef struct
{
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float prev_error; // 上一次误差
float prev_prev_error; // 上上一次误差
} IncrementalPID_Controller;
// 增量式PID控制器初始化函数
void IncrementalPID_Init(IncrementalPID_Controller *pid, float Kp, float Ki, float Kd) {
pid->Kp = Kp;
pid->Ki = Ki;
pid->Kd = Kd;
pid->prev_error = 0.0f;
pid->prev_prev_error = 0.0f;
}
// 增量式PID控制器计算函数
float IncrementalPID_Compute(IncrementalPID_Controller *pid, float setpoint, float measured_value)
{
float error = setpoint - measured_value; // 计算当前误差
float delta_error = error - pid->prev_error; // 计算误差变化量
float increment = pid->Kp * (error - pid->prev_error) +
pid->Ki * error +
pid->Kd * (delta_error - pid->prev_prev_error); // 计算增量
pid->prev_prev_error = pid->prev_error; // 更新之前的误差值
pid->prev_error = error;
return increment; // 返回增量值
}
2.PID+编码器电机闭环控制介绍
做智能车的同学电机闭环控制直接采用增量式PI算法,方向控制直接采取位置式PD即可。这是无数前辈车友总结出的结果。
2.1编码器使用简介
编码器有很多种,有角速度编码器,正交编码器,方向编码器,绝对值编码器等。
智能车用的比较多的是正交,方向,绝对值这三种编码器,他们都属于脉冲编码器。
正交和方向编码器输出结果有方向,车轮正转输出正数,车轮反转输出负数。绝对值编码器不分正反转,只有正处输出。
脉冲编编码器根据输出范围分为1024线,512线,256线编码器等。他们的含义是编码器轴每转一圈,输出脉冲个数,我们配置单片机读取脉冲个数就可以。
以1024线编码器为例。当我间隔10毫秒读出编码器数值为100时,说明编码器在0.01秒钟,轴转动了100/1024≈0.1圈。
注意,是编码器轴转动了这么多圈,车轮转动还要考虑齿轮之间的齿轮比。
2.2转速获取
智能车里面通过编码器获得车轮转速,具体原理如下。
首先,我们知道
其中dt越大,v就是平均速度,dt越小,v就趋近顺时速度。
我们将编码器放在单片机定时器中断中读取,比如我们每间隔10毫秒去读取一次编码器的值,读完了将它清空,那么我们就得到了在每10毫秒中编码器的旋转圈数。
(读取完要将寄存器清空,因为寄存器的值是累加的)
得到编码器旋转的圈数,再根据齿轮比算出轮胎转动圈数,再测量轮胎半径,理论上可以得到非常准确的车轮转速,但一般我们不这么做。
因为没有意义,理论上经过上述处理换算的转速是有单位的,但是我们做智能车控制非常多的控制量都是模糊量,他根本没有单位,比如摄像头误差。
我们常规处理直接在定时器中读取编码器值就好,不必做特殊处理。
因为当你的dt固定了,也就是你定时器中断的时间固定了,你的速度:
k是个比例系数。1/dt是定时器时间的倒数,b是齿轮比,以及车轮半径相关的系数,都是固定值
也就是说你又是测编码器之间的齿轮比,又是测车轮的半径,求得的速度也不过是在编码器反馈的数据前乘上一个固定系数而已。完全没有什么实际意义。何况车轮还有可能打滑,磨损造成测量不准。
大家如果想知道车速,直接用尺子测赛道长度,然后用手机掐秒表算一下平均速度即可。
2.3电机闭环控制
上面我们知道了如何获取电机转速,下面我们讲一讲如何控制电机转速。
上面这张图是智能车最常用的电机。
他有两个接头,用来驱动电机。显然在电机两端施加电压越大,电机转速越快。
那么很简单,我只需要控制电机两端电压就可以控制电机转速。
比如相同的电机,我施加3V电压,他的转速肯定不如6v的电压转速快。
至于如何测量他的转速,我们使用上文提到的编码器外接齿轮啮合住电机输出端即可。
按照道理在一定区间内,电压与转速应该是呈现线性关系,实际情况有可能因为电机转动中发热,老化,电机磁铁退磁,齿轮啮合过紧等原因会有偏差。但是我给的电压越高,他转的越快,这一点是肯定没有问题的。
该电机推荐电压7.2V,空载电流约630mA。(其实你电压给大一点也可以,只是电机更容易烧)
单片机所使用的电源一般都是3.3V或5V,驱动电流更是毫安级别,不可能驱动这个大家伙。
2.3.1 电机驱动
想要驱动电机,起码需要一个比较高的电压。
这里就想到了我们车上有一块电池,用来给所有外设供电。
这样就有了7.4v的电压,把电机线接上电池,电机可以转,电池电压降低,电机转速变慢。
但是显然这样不符合我们的控制需求,不能自由控制电机转速。
这时候我们就需要一个电机驱动。
上面的电机驱动有四组对外接口。
一个输入电源,用来连接电池,两个输出接口,用来连接电机(一辆C车有两个电机),还有一组排针接口,作为输入控制信号。
通过输入信号,控制输出给电机的输出电压,这就是驱动。
用3.3v的电压,控制7.4v(此处电压取决于电池电压)的电机。
2.3.2 PWM
对于一个电机来说,驱动信号是两路信号。这两路信号可以控制电机的转速和方向。
根据驱动器芯片的不同这两路信号略微有点区别。
无论是哪一种控制信号,想要控制速度,都需要控制PWM占空比。
这里简单介绍一下PWM,等后续有空再出一篇文章专门讲一讲PWM。
PWM(Pulse width modulation)也叫脉宽调制,可以用示波器测一下,他的样子就是一连串的矩形波。
他有两个参数,一个是频率,一个是占空比。
大家看驱动参数那张图可以在下面看到小字,不同型号的电机他的工作频率不一样,这个是电机的固有特性,大家按照要求设置即可。
我们主要看看占空比。
前面我们说,我们通过PWM可以控制电机转速。我们又知道,电机的电压变化会导致电机转速变化。我们能否得到结论,PWM就是控制的电机两端电压来控制的转速呢?
答案是可以的。
占空比是指在一个周期内,高电平(或低电平)所占的时间长度,他的范围是0%-100%
上图中,纵坐标是电压,横坐标其实是时间。
第一行,高电平时间占周期的50%,那么他等效电压就是5V*50%=2.5V
等效2.5V的电压施加到电机上。
第二行同理,75%的占空比,那么5V*75%=3.75V,3.75V的电压将被施加到电击上。
第三行同理,20%的占空比,那么5V*20%=1V,1V的电压将被施加到电击上。
我们将纵坐标换成我们的电池电压,我们改变占空比就可以产生7.4v*x%就的电压,可以控制电机转速。
所谓驱动,就是使用3.3V的PWM波,让输出端输出频率,占空比相同的以电池电压为单位的PWM波。
2.3.3 开环
开环控制一般来说就是没有反馈的控制。
现在我们已经可以通过调整PWM占空比(或脉宽)控制车轮转速。在直道处,占空比给高一点,弯道处占空比给小一点,这样就实现了转速控制。
但是这样有个问题,车子是有惯性的。电机的加速减速也是需要时间反应的,很有可能在直道想要提速,加大了占空比,结果车子加速不及时,已经到了弯道速度才加起来。又到了弯道,需要减速,然后一个减速不及时冲了出去。
开环控制是属于能用,但不好用的情况。
主要是因为惯性的存在,导致加速不及时,减速刹不住。
2.3.3 闭环
好了,我们讲了这么久,终于到了PID+PWM闭环控制车轮转速。
开环更多的是定性控制,闭环可以做到定量控制转速。
还记得我们上面讲过的测速吗,如果是开环控制的话你可以看到在占空比不变的情况下速度值其实是很不稳定的。而且根据电池的电量不同,相同的占空比得到的转速也不同。在开环状态下其实测转速意义不大,毕竟你只能看,也不能用。
所谓闭环,就是反馈控制。每算出一次占空比输出后,测一下转速。查看当前转速和我期望转速差距大不大,如果当前转速还是与我期望转速差距过大,那么我就增大占空比输出,如果过小,我可以减小占空比,甚至可以让电机反转,给车子一个反向的力,让他迅速刹车。
那么我的期望值就是我期望车轮转速,无论是空载还是负载,都可以保证速度的稳定。
这个输出占空比的计算,是由PID完成。
使用时,输入两个参数,当前转速,期望转速,输出一个参数,占空比。
随后在电机驱动函数中,将刚算出的占空比应用到电机中去,就可以做到对于转速的准确控制。
注意这两者的时间关系,我个人习惯是电机转速读取频率要大于等于控制频率。当转速读取的频率过低,反馈的意义就不大了。
最后补充一下,PID的占空比输出和电机转速的关系。
比如我10毫秒读取一次电机转速,PID20毫秒控制一次占空比,和我20毫秒读取一次电机转读,PID20毫秒控制一次占空比。
这两者肯定会有很大的不同,哪怕是客观上一样的转速,10毫秒读取一次编码器,和20毫秒读取一次编码器读取到的编码器的值理论上是有两倍的差距的。
那么我们如何来克服这些差距的影响?
答案是调参。
电机控制一般采用增量式PI,那么就需要调整P和I系数,来消除读取频率,电机种类这些差距,做到编码器数值与占空比相对应。
至于调参,那就是一门玄学.
2.3.4 调参
一般来说,P代表比例,他决定反应速度,I代表积分,是对误差的累计,代表稳定后的波动范围,D代表微分,代表对误差变化的趋势,可以理解为预判。
当然,一切都要看实际情况而定。
具体参数也没什么范围而言,控制周期5毫秒和10毫秒,他的参数也不见得是两倍关系。
我的车参数在30左右,我队友的车子参数在几万,所以不同平台,不同周期,不同设备之间参数并不通用。
3.其他领域的PID控制
类似的,如果我想要控制某个水缸的水温,那我需要读取当前的水温,设置我期望的温度,输出量可以是加热棒的功率。
比如焊接电路板的烙铁就是这个原理,在到达设定值附近,会有一颗加热灯在一闪一闪,就是在间歇性加热,保证烙铁头温度在设定的范围附近。
比如液位控制,那就读取当前水位,设置期望水位,输出量就可以是阀门的开度(视阀门类型而定)。
其他领域基本都是同理,由于我也没有调过,就不多说了。
希望能够帮助到一些人。
本人菜鸡一只,各位大佬发现问题欢迎留言指出。