Arduino小车PID自整定调速——菜鸟也来写PID自整定库

前言

        说到PID整定这个问题,它常常是控制领域一个令人头疼的问题。PID算法本身并不复杂,非常清晰的一个数学公式展现在我们面前便可以充分了解它,但当真正开始尝试投参数的时候,又往往对波形的变化摸不着头脑,花了好大工夫都难以获得一个理想的效果。对于有经验的工程师,他们可以通过分析输出波形的变化得出参数调整的正确方向,但作为初学者的我们既缺乏PID调节的经验,又想尽快实现PID调节的目标,这时候便可以采用PID自整定的方式来实现目标。

        笔者的PID调节对象是单轮驱动的Arduino小车。我们知道在网上有非常丰富的Arduino库可以直接拿来使用,在之前进行手动调节PID时,笔者尝到了直接把网上大神写好的PID库拿来就用的甜头。可在进行PID自整定时,虽然也有相应的库文件可以下载使用,但苦于采用自整定方式的人太少,自整定库一直停留在v0.0.0阶段,而且库中自带的例程难以读懂,网上又没有人分享成功使用这个自整定库的代码,笔者进行了不少尝试,却死活不见效果。在了解了PID自整定的大致原理之后,笔者决定不再浪费时间在尝试使用现有的库上,自己动手写一个PID自整定程序。

PID自整定方法

        有关PID自整定的方法有很多种,这里将要采用的为一种名为继电器自整定的方法。感谢控制领域的大神以及科学家们呕心沥血的研究,才给应用中丰富的方法提供了坚实的理论基础。有了继电器自整定法,我们的目标不是探究其数学推理过程,而是利用其优美的研究成果进行代码实现,让其真正能够在实际应用中起作用。本文主要采用了brettbeauregard博客的一篇有关PID自整定库的文章中讲解的原理,并结合自己对继电器自整定方式的理解,按照以下的思路来写代码:

        自整定的过程大致可以分为以下几个步骤:

1、稳定为先:先用一个无需非常完美的PID参数调整系统,使系统的被调量和输出都达到稳态,并且被调量近似达到设定值。
2、强行震荡:再暂时禁用PID调节,使控制器将一个矩形波状的控制作用力作用于系统,当被调量低于设定值时,控制输出产生一个正的阶跃信号,迫使被调量向高处转变,当被调量高于设定值时,控制输出产生一个负的阶跃信号,迫使被调量向低处转变,由此强行使系统产生震荡。

                   强行震荡过程

 

3、分析波形:分析以上过程产生的波形的特征,根据需要计算出相应的PID参数
4、投入使用:将自整定得到的参数投入PID调节器中,即可让系统实现较好的PID调节效果。


前期准备

研究对象:基于Arduino nano板,采用mega328P芯片的单轮驱动三轮小车
研究目标:运用继电器自整定法实现小车PID自动调速功能
已实现功能

             采用MsTimer2定时器中断库实现的码盘采样测速程序,可控制测速周期,默认为100ms测一次
             小车轮式驱动单元配套操作库文件,可即时改变小车PWM值和实现基本的控制
             准备好brettbeauregard博客里写的PID_v1库文件,可直接在Arduino IDE的库文件管理器中下载或在网上下载

实现过程
整体构思

                              工作流程图
        Arduino的程序主体函数是void loop(){} 形式的,主体函数中的内容会不断循环执行,而按照上文中所写,继电器自整定方法需要按步骤完成。为此,笔者采用了条件语句来将程序划分成不同阶段来完成。

 稳定为先

        程序第一部分为PID预调节环节,目的就是让系统开始强行震荡之前先达到被调量input基本稳定在设定值setpoint,同时调节量output也基本稳定。因为该部分已经应用到了PID调节器,所以在进行PID自整定工作之前可以进行适当的手动调节PID工作,大致掌握系统的PID需求。而得出精确可靠的PID参数的工作则交给后续的自动调节过程来完成。该部分设定的PID参数至少采用了比例积分作用(PI),以能够使系统达到稳定为首要任务,而无需过多考虑达到稳定的快速性。

        PID调节器需要将实时的被调量采集到系统中,进行计算,得出合适的调节量再传送给系统,因此在PID调节过程中需不断循环执行“传入>计算->传出”的步骤。笔者在程序中编写了一个if()条件语句来实现这样的功能:当系统时间<5s时,则执行PID预调节环节,否则执行接下来的环节。这里的判据时间可以根据实际需要设置得再长一点或者短一点,只要能让系统在之前设定的PID参数下最终获得稳定就行。采用时间作为判据,可以免除判断输出是否近似稳定的麻烦(实时上是笔者想偷懒)。如果硬件内存资源充足的话,更好的办法是写一段代码来判断系统是否达到稳定,这样更可靠、更科学也更能节省整体工作的时间。

        此外,在第一个环节还有一项工作要做,就是根据稳定下来的调节量output,计算出在第二步强行产生震荡时,output分别向大和向小移动指定步长并产生阶跃信号的值大小。一个简便的方法是在第一部分每次执行PID计算时,便随即计算出在该时刻的调节量output值基础上的两个阶跃信号值。因此当第一部分PID预调节完成,被调量和调节量都基本稳定,执行满足判断条件时的最后一次PID计算时,便可得到能用于第二阶段产生震荡时所需的阶跃信号值。

        第一部分代码如下:

Input = speed;                          //loop()每执行一次就将当前速度传给Input
if (millis() < 5000) {
    myPID.Compute();
    YM3.changePwm(Output);

    OutputLow = OutputSet - outputStep;
    OutputHigh = OutputSet + outputStep;
} else if(...){
      ...
}


强行震荡

        在手动整定参数时有一种方法叫做Ziegler-Nichols整定法,它的主要方法是仅在比例作用下将系统调节到临界震荡状态,得到临界增益和震荡周期,根据这两个量进行相应的计算便得到相对合适的PID参数。这种方法被广泛运用到PID调节中,但也存在临界状态难以把握的缺点本文中采用的继电器自整定法也是让系统先产生震荡,再通过测量峰峰值、周期,从而反映出这个系统本身的特点,根据这些值计算出合适的PID参数。它的优点是系统的震荡状态由调节量的阶跃信号产生,而不需要费劲寻找临界状态。

        继电器自整定法中产生震荡的办法是以设定值setpoint为触发线,过程如图所示

                   这里写图片描述

 

        为完成这个步骤,笔者依旧采用了以系统运行时间为判据的方法。当系统时间<10秒时,则重复执行震荡过程。代码如下:

if (millis() < 5000) {
    //第一部分:PID预调节
    } else if(millis() < 10000){
        if (Input < Setpoint) { 
            Output = OutputHigh;
            if (Output > 255) Output = 255;
        } else if (Input > Setpoint) {
            Output = OutputLow;         
            if (Output < 0) Output = 0;
        }
        YM3.changePwm(Output);
    } else {
        ...
}


        在此过程中,笔者还将记录被调量input的历史值的代码临时嵌入到定时器中断测速代码中,先定义了一个容量为20的数组,采用队列结构存放最新的20组测速数据。因为笔者的测速程序是每100ms测速一次,因此就相当于保存了最近两秒之内的速度历史记录。代码如下:

for (int i = 1; i < 20; i++) {
    inputHistory[20 - i] = inputHistory[20 - i - 1];
}                                                   
inputHistory[0] = Input;    


分析波形

        有了震荡的数据后,接下来要做的就是分析数据,从中提取出对我们有用的信息。前面强行震荡部分看似是继电器自整定的主要步骤,但代码实现起来并不困难。分析波形看似仅是后期处理步骤,但却是整个程序中至关重要的一环。我们需要根据采集到被调量的20个历史数据,从中计算出震荡周期以及波形的峰峰值。笔者设立了一个计算状态标记cacluateFlag,当已对数据分析计算过,并产生了新的PID参数,则标记为0,否则为1,以此避免每次执行loop()程序都重复计算一遍。

        在波峰波谷检测中,排除20个数据的首尾两个数据,将中间的18个数据依次同相邻的两数据进行比较,如果是波峰或者波谷,则加在波峰或波谷的数据总合上,同时更新波峰或波谷计数。全部完成波峰波谷判断后,用其总值除以计数的个数,便可以得到震荡时波峰波谷的平均值。

        在周期检测中,采用分析两波峰之间间隔来获取周期。当检测到第一个波峰时,记录下该数据在数组中的下标,同时用另外一个变量来记录检测到最新的一个波峰时的下标,当所有数据分析完毕,这个变量便记录下的是最后一个波峰的下标。将两下标相减,并乘以测速周期100ms,可得到第一个波峰和最后一个波峰的时间间隔,再除以(波峰个数-1),便可得到周期的平均数。

        完成波形分析后,仅需将得到的峰峰值和周期带到PID前辈们推导出的公式中,计算得到相应的PID参数,再将PID参数更新投入使用,PID自整定便大功告成!

                        计算式

 

        代码如下:

if (millis() < 5000) {
    //第一部分:PID预调节
    } else if(millis() < 10000){
        //第二部分:强行震荡
    } else {
        while (caculateFlag) {
            for (int i = 1; i < 20; i++) {
                if (inputHistory[i] > inputHistory[i - 1] && inputHistory[i] > inputHistory[i + 1]) {       //如果是波峰
                    inputHighAll += inputHistory[i];                                            //波峰值总数增加
                    inputHighNum++;                                                                         //波峰个数计数增加
                    if (inputHighNum == 1) atemp = i;                                                       //当产生第一个波峰的时候,atemp记录下此时是第几个数据(每个数据相隔100ms)
                    btemp = i;                                                                              //当对20个历史数据分析完,btemp则可记录下最后一个波峰对应的是第几个数据
                } else if (inputHistory[i] < inputHistory[i - 1] && inputHistory[i] < inputHistory[i + 1]) {//如果是波谷,类似上面
                    inputLowAll += inputHistory[i];
                    inputLowNum++;
                }
            }
            float autoTuneA = (inputHighAll / inputHighNum) - (inputLowAll / inputLowNum);                  //峰峰值的平均数A
            float autoTunePu = (btemp - atemp) * 0.1 / (inputHighNum - 1);                                  //两个波峰之间的时间间隔Pu(s)
            double Ku = 4 * outputStep / (autoTuneA * 3.14159);
            Kp = 0.6 * Ku;
            Ki = 1.2 * Ku / autoTunePu;
            Kd = 0.075*Ku*autoTunePu;

            caculateFlag = 0;
            Serial.println("PID AutoTune has completed!");
            myPID.SetTunings(Kp, Ki, Kd);
            YM3.changePwm(0);
            delay(500);
        }

        myPID.Compute();
        YM3.changePwm(Output);
    }

        在PID参数计算时有两种模式可以选择:一是仅用比例积分作用(PI),二是使用比例积分微分作用(PID)。在手动整定PID时常听说在许多场合仅使用比例积分作用就可以达到较好的整定效果,而无需微分作用,我们在手动整定时也确实会为了图方便而不考虑微分作用。但此处继电器自整定中提供的两种模式,笔者将两者都测试了一下,PI作用下最直观的感受是系统达到稳定状态的时间长,稳定效果较好。PID作用下系统达到稳定的时间短,稳定效果也很好。通过输出参数,发现使用PI模式时的比例、积分作用都弱于PID模式,而PID模式的微分作用实际上也非常的弱,几乎可以忽略不计。因此笔者将这两种模式理解为PI模式以最终让系统实现可靠的稳定为目标,PID模式兼顾了稳定时间和稳定效果两方面因素。我们可以根据实际需要选择不同的模式。

        部分代码

void loop() {
    Input = speed;                  
    if (millis() < 5000) {
        myPID.Compute();
        YM3.changePwm(Output);

        OutputLow = Output - outputStep;
        OutputHigh = Output + outputStep;
    } else if(millis() < 10000){
        if (Input < Setpoint) {     
            Output = OutputHigh;            
            if (Output > 255) Output = 255;
        } else if (Input > Setpoint) {  
            Output = OutputLow;         
            if (Output < 0) Output = 0;
        }
        YM3.changePwm(Output);                          
    } else {    
        while (caculateFlag) {
            for (int i = 1; i < 20; i++) {
                if (inputHistory[i] > inputHistory[i - 1] && inputHistory[i] > inputHistory[i + 1]) {
                    inputHighAll += inputHistory[i];                                        
                    inputHighNum++;                                                         
                    if (inputHighNum == 1) atemp = i;                                       
                    btemp = i;                                                              
                } else if (inputHistory[i] < inputHistory[i - 1] && inputHistory[i] < inputHistory[i + 1]) {
                    inputLowAll += inputHistory[i];
                    inputLowNum++;
                }
            }
            float autoTuneA = (inputHighAll / inputHighNum) - (inputLowAll / inputLowNum);  
            float autoTunePu = (btemp - atemp) * 0.1 / (inputHighNum - 1);                  

            double Ku = 4 * outputStep / (autoTuneA * 3.14159);
            Kp = 0.6 * Ku;
            Ki = 1.2 * Ku / autoTunePu;
            Kd = 0.075*Ku*autoTunePu;

            caculateFlag = 0;
            Serial.println("PID AutoTune has completed!");
            myPID.SetTunings(Kp, Ki, Kd);
            YM3.changePwm(0);
            delay(500);
        }
        myPID.Compute();
        YM3.changePwm(Output);
    }
}

测试

        由于受小车电机所限,较为可靠的运行速度范围为150mm/s~300mm/s,因此笔者将在这个速度范围内对以上代码进行多组测试。采用PID模式整定。

设定速度为150mm/s

产生参数:Kp = 0.37 Ki = 1.86 Kd = 0.02


设定速度为200mm/s

产生参数:Kp = 0.51 Ki = 2.72 Kd = 0.02

                          150mm/s
设定速度为250mm/s

 产生参数:Kp = 0.9 Ki = 5.14 Kd = 0.04

                         200mm/s

 
设定速度为300mm/s

产生参数:Kp = 1.18 Ki = 6.91 Kd = 0.05

                       300mm/s

 

总结与不足

        本文是笔者在多次尝试使用已有的PID自整定库未果的情况下做的一次有意义的尝试,而实际代码却远不能当做一个通用的库来使用,具有局限性。至于为什么使用已有的库没成功,一来可能是库的版本较低,没有得到较好的维护,以至于像我这样的初级玩家还不能充分理解其深邃的思想和实现方式,二来可能是库文件中对数据分析比较严格,里面有对于可靠性不高的计算数据废弃不用的代码实现,笔者的小车测速代码还存在着一定的缺陷,这可能是导致自整定失败的原因。

        在编写代码过程中,笔者曾遇到了因为全局变量使用过多,动态内存不足导致编译失败的问题,这也值得反思,可从这个方面进一步去优化代码。

        在强行震荡过程中,阶跃信号的输入是根据被调量是否越过触发线来确定的,然而小车的测速程序是以一定周期执行的,不能做到完全实时的给出被调量数值大小,这就可能导致调节量的改变出现滞后的问题。可以选择的提升办法是减小测速周期,提高测速精度。但减小测速周期的同时又需增大存放被调量历史数据的数组容量,增加了单片机资源开销。

        传感器的使用难免会有噪声产生,这给数据分析带来了不小的麻烦。本文中的代码几乎可以说是处于理想状态下的实现,而实际应用中可能会产生误差。当给小车电机一个确定的PWM值,测出来的速度数据不是理想的单一值,而是在一个小的范围内不断的无规律波动,这样的波动给强行震荡、分析波形和投入使用都带来了麻烦。好在在强行震荡过程中,小车的速度是不断变化的,同时因为采样周期的存在,致使记录下的数据在更大程度上是能够直接反映出速度变化的趋势,淡化了噪声的影响,分析波形中也是如此。尽管如此,在实际测试当中仍然偶尔会有自整定效果不好的情况,这时便需要重新运行一遍。
        在PID调节中有调节死区的概念,它的存在就是避免系统因为测量上的噪声而不断的调整,延长系统的寿命。在自动整定过程中也可以运用这样的思想,设定一个噪声宽度,在每次判断两个相邻数据的大小时引入噪声宽度,当两数据差在这个宽度以内则当成相等,超出这个宽度再比较大小。

        再回到PID自整定这个方法上来,关于PID自整定,许多人认为整定出来的参数是值得参考的,但要想获得最好的效果,还需要在这个基础上进一步人为整定。正如啥都能加热的微波炉可以把烤鸡做熟,却难以比得上专门的烤箱做出来的美味。当然,效果的提升是以付出时间和精力为代价的,提升效果可以作为学习时的有益尝试,但实际应用中并非效果越好就越好,需根据实际应用场合,做到满足要求就是最好。笔者是一名PID小菜鸟,这样的整定效果已经足够自己愉快的玩耍一阵子啦。

参考
[1] 《Arduino PID Autotune Library》
[2] 《如何利用继电器法实现PID自整定》

若有不当之处或更好的建议,欢迎进行讨论!
————————————————
版权声明:本文为CSDN博主「_Laputa」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yanggg1997/article/details/76674986

  • 5
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值