初学者的PID调速

  • 格式:doc
  • 大小:488.00 KB
  • 文档页数:8

初学者的PID调速

背景:

偶然购买了智能小车配件,使用现在大热的arduino控制板,实现了wifi的小车运动。但是遇到了走不直,控不住的问题。上网google后,发现里面还有很大的学问,直到发现了一个名为PID的控制方法,可以控制电机转速。只有实现了PID调速,才有基础让小车走直线。

硬件准备:

N20直流电机,配有1:145的金属变速箱。电机必须带测速。

Aduino nano控制板,主控芯片atmel328p。

实现电机控制的H桥电路板,带pwm输入。

供电电池。

软件准备:

Arduino IDE开发环境。

串口猎人,收集数据,绘制可视化图像。

目的就是控制电机转速,但是如何入手是个门槛。

通过疯狂google,网上的偏重理论研究的很多,也有很多的学生毕业设计论文,但是都是说的不仔细不透彻。我本身工作多年,数学基础,自动控制理论也比较薄弱,看了也是云里雾里。正在发愁之际,点入丁老师的“嵌入之梦”网站。里面的一篇PID调速文章使用通俗易懂的语言介绍让我非常兴奋,给我一个不错的起点,总算是找到门了!

还是要先有个大概的理论。PID指的是(比例-积分-微分)控制器。要控制就要涉及到三个量:设定值(控制目的)、被调量(控制什么)、输出量(控制结果)。对应到直流电机上就是:

设定值:希望(给定)电机输出的转速

被调量:PWM占空比

输出量:实际电机输出的转速

实现自动控制保持速度恒定,有一个必不可少的东西,反馈。只有有了反馈才能知道如何调整被调量。假设我是控制小车的程序,我只有看到当前时刻的速度才能采取动作,是调快点还是调慢点。

电机上体现速度的数字量是单位时间内的电平改变的次数(ticks)。我设置的是引脚变化触发计数,有改变就计数。因此,速度 = ticks。对应的量就变为:

设定值:希望的单位时间内ticks

被调量:PWM占空比

输出量:实际单位时间内ticks

下面问题来了,如何测定ticks。程序可以通过定时器来计数,问题是这个采样时间怎么取?参考丁老师的文章“因为 PID 控制的基础是以周期 T 测量,计算出偏差后修正输出。而测量周期不能太长,那样响应将滞后;但过短也没有意义,因为对象的惯性特征,导致极小的时间间隔中不会有变化。根据小车电机的驱动特性,100ms 左右的周期比较合适。”这里我觉得我这个电机测速是放在转轴上未减速前,每圈有12个ticks应该可以再设短些,就设为15ms测速一次,30ms取上2次的平均值,作为测量输入,执行一次PID计算调整输出。

有了采样周期,就可以得到了2个量:设定值,输出量。下面就是建立被调量PWM与这2个量的关系了。

实现P控制器

再回到PID算法上来吧。我们现在知道了什么?有了设定值,有了输出值,那么就得到了偏差值。在时刻t ,e(t) = setpoint – inputMeter。Kp*e(t),就可以用来控制了PWM了。e(t)有可能为正,也有可能为负,快了就变成负值,慢了就变成正值,这正是我们想要的结果啊。赶快动手试一试:

结合我的情况我的给定值是40,测得的实际速度是25。最终稳定在25。Kp参数经过调整,直到不震荡的状态。

可以看到实际的输出值与想要的速度有一个恒定的误差,也就是专业术语的稳态误差了。

double error = mySetpoint - inputMeter;

outputPWM = kp*error;

//arduino 的调节范围就是0 - 255

if(outputPWM > 255) outputPWM = 255;

if(outputPWM < 0) outputPWM = 0;

实现PI控制器

实现了p控制器后,我们来看看有什么能改进的。通过改变参数kp,直到系统不震荡就达到目的了。“系统不振荡”就是一个“参数整定”的过程。P控制器可以快速响应我们的给定,比如我改变速度为50。实际速度也跟着上去了。可是我给定了一个速度,实际却不能达到,我需要电机速度是40ticks,真实输出是25ticks。为了消除这个稳态误差,就需要积分控制器帮忙。下面涉及到的理论偏多。但是耐心思考下,还是比较容易理解的。所谓积分作用是指调节器的输出与输入偏差的积分成比例的作用。积分方程为:

式中:TI是积分时间常数,它表示积分速度的大小,TI越大,积分速度越慢,积分作用越弱。

这里说明下我们程序是在中断处理函数中进行。理论公式给出的是连续量,模拟式的表示方法。所以就要进行所谓的“离散化”。

u(t)对应的就是我们的pwm输出。Kp就是P控制器整定后的值。T就是设定的采样周期。Ti就是需要通过实际测试,需要整定的值。理论公式要求的积分,对应到我们实际程序编写就是把所有的误差加起来。积分 约等于 做加法。更直观一些积分就是求面积。j就是执行了多少个采样周期了。面积可以用梯形求和法。 00()()()kkjjetdtejTTTejT离散化 (0,1,2,)tkTk

伪代码:

增加全局变量 errSUM

double error = mySetpoint - inputMeter; errSUM += error;

outputPWM = kp*error + ki * errSUM ;

//arduino 的调节范围就是0 - 255

if(outputPWM > 255) outputPWM = 255;

if(outputPWM < 0) outputPWM = 0;

这里需要说明的是,程序中的ki对应到理论公式其实是kp*T/Ti,实际中直接用ki替换。整定时只要改变ki,减少了程序的运算。马上动手实现,看效果吧

实现PID控制器

实现PD控制器之后,我们的目的已经达到。但是还有不足,那就是系统到达稳定状态的时间较长,还要经历多次震荡过程。能否减少震荡,加快到达稳态?回答是肯定的,微分控制器就是做这个事情的。说的形象点,前面的积分控制器表征了系统的历史,积累了多少误差,然后给它补上。相对的这个微分就是提前决定给多大的补充。微分作用与被调量的大小无关,与被调量的变化速率有关

系统变化越快,给出的调节值趋向无穷大。当然现实中不存在无穷大。

再次回到理论,这次总算是完整的公式了:

这里关注点是微分部分。涉及到2个值,本次的误差e(kT) ,上次的误差e[(k-1)T],2者的差值除以采样时间就是变化率了。Td就是微分系数。和积分一样这次用Kd = kp*Td/T ,同样是为了减少程序的计算量,整定时直接修改即可。

0()()[()(1)]kpIDjKekKejKekek0(){()()[()(1)]}kDpjITTukKekejekekTTde(t)()[(1)]dekTekTtT00()()()kkjjetdtejTTTejT()()etekT(0,1,2,)tkTk01()[()()()/]tpDIutKetetdtTdetdtT离散化 伪代码:

全局变量 errSUM 积分项

新增上次的偏差值变量errorLast 微分项使用

double error = mySetpoint - inputMeter;

errSUM += error;

outputPWM = kp*error + ki * errSUM + kd *(error – errorLast);

//arduino 的调节范围就是0 - 255

if(outputPWM > 255) outputPWM = 255;

if(outputPWM < 0) outputPWM = 0;

实验结果(这次2个电机一起来吧):

效果令人满意,呵呵。

再进一步,到目前为止,我们实现的算法,叫做位置PID,也就是每次都使用整个系统的状态。我理解为:偏差使用的是所有的全量偏差。还有一种算法是增量式PID。摘录网上的话:“位置式PID算法每次输出与整个过去状态有关,计算式中要用到过去误差的累加值,因此,容易产生较大的累积计算误差。而增量式PID只需计算增量,计算误差或精度不足时对控制量的计算影响较小。”

公式:

上一次的输出为u(k-1),这次的输出为u(k) ,变化量为^u(k):

我们需要的pwm输出是全量,而不能是变化量,那么加上上次的u(k-1)即可。

u(k) = u(k-1) + ^u(k),也就是把u(k)项移到等号左边,把^u(k)移动到等号右边。好了,让我们来实现它,这次是最终代码,直接上代码片段。

#define PVAL (error - last1Error)

#define IVAL ((error+last1Error)/2)

#define DVAL (error - 2*last1Error + last2Error)

/*Compute all the working error variables*/

double input = *myInput;

double error = *mySetpoint - input;

/*Compute PID Output*/

double output = *myOutput + kp * PVAL + ki * IVAL + kd * DVAL ;

if(output > outMax) output = outMax;

else if(output < outMin) output = outMin;

*myOutput = output;

/*Remember some variables for next time*/

last2Error = last1Error;

last1Error = error;

好了,享受成果的时候到了: [()(1)]()[()2(1)(2)]PIDKekekKekKekekek10(1)(1)()[(1)(2)]kpIDjukKekKejKekek()()(1)ukukuk