Arduino爱好者

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 4534|回复: 9

【搬运】初学者的PID教程 by Brett Beauregard

[复制链接]
发表于 2021-5-1 14:07 | 显示全部楼层 |阅读模式
本帖最后由 SeanM 于 2021-5-1 17:05 编辑

在网上看到一个写的很详细的PID教程,一步一步地讲解Arduino PID库的原理,非常棒。帖子的作者是Brett Beauregard,网址是Improving the Beginner’s PID(http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/)。习惯看英文的朋友建议直接去看。
Brett Beauregard.png

抱着边看边学的想法,我准备把这一系列的帖子翻译过来。一方面是督促自己学习,一方面也是向大家介绍PID详细的知识。

要说明的是原文是2011年4月写的,到现在已经十年时间了,他所分析的arduino PID库应该已经更新,不一定能完全兼容现在的库。



 楼主| 发表于 2021-5-1 14:38 | 显示全部楼层

第一篇:初学者的PID-简介 by Brett Beauregard

本帖最后由 SeanM 于 2021-5-5 15:26 编辑

为了配合新发布的 Arduino PID Library,我决定来写这个系列的帖子。上一版本的库,虽然很稳定,但是没有代码解释,所以这次我打算巨详细地解释一下代码的原理,希望能帮助下面两类盆友:
  • 希望了解Arduino PID Library本身的朋友可以得到巨详细的解释
  • 希望自己写PID算法的朋友也能有所借鉴

这个主题本身十分硬核,但是我也在尝试用不那么痛苦的方式来解释我的代码,我会从“新手的PID”开始,然后一步一步地把它变成一个高效的,健壮的PID算法。


新手的PID

大家以前学到的PID公式是这样的:

PID

PID

其中e(t)=设定点 - 输入
这个公式让大家可以编写出以下的代码:
(译者注:在实际使用中一般使用PID的离散形式:

PID离散

PID离散
,后面的代码其实是按这个写的)


[pre]/*声名变量*/

unsigned long lastTime;
double Input, Output, SetPoint;
double errSum, lastErr;
double kp, ki, kd;

void Compute()
{
    /*计算上次计算到本次计算间隔的时间*/
    unsigned long now=millis();
    double timeChange=now-lastTime;
   
    /*计算所需要的所有工作变量*/
    double error=SetPoint-Input;
    errSum+=(error*timeChange);
    double dErr=(err-lastErr)/timeChange;
   
    /*计算PID输出*/
    Output=kp * error + ki * errSum + kd * dErr;
   
    /*记录下次要使用的变量*/
    lastErr=error;
    lastTime=now;
}

void  SetTunings(double Kp, double Ki, double  Kd)
{
    kp=Kp;
    ki=Ki;
    kd=Kd;
}
[/pre]

Compute()这个函数可以被定期调用或者随时调用,他都可以正常使用。但是,这个系列的帖子不会仅仅满足于工作得还好。如果我们想要把它改得和工业用的PID控制器一样出色,那么我们应该注意以下几个问题:
  • 采样时间 — 如果PID算法的采样间隔是固定的,那么PID的效果最好。如果算法知道采样间隔,我们可以简化一些数学计算;
  • 微分冲击 — 这个不是什么大问题,很好处理,我们后面再说;
  • 即时调整 — 好的PID算法可以在调整参数时对控制过程不产生太大的影响;
  • 缓解积分饱和 — 我们将要学习什么是积分饱和并且找到一个解决办法,这个解决办法还带了一些额外的好处;
  • On/Off(自动/手动) — 在大多数的应用中,经常会想要关掉PID控制器,手动控制输出;
  • 初始化 — 在刚打开PID控制器的时候,我们希望是“无扰切换”,即我们不希望输出值忽然变成一个新的值;
  • 控制器的方向 — 这个名词不是“鲁棒性”的另一种说法,它是为了确保用户能输入正确的调优参数而设计的;
  • New-比例测量 — 加上这个新特性可以使某些过程的控制更容易。


一旦我们解决了这些问题,我们将有一个对PID算法深刻的了解。我们还会拥有最新的Arduino PID控制库。所以不管你是想写出自己的PID算法还是想去了解PID算法里到底发生了什么,我希望这些都能帮上你。现在我们开始旅程吧。

更新:在所有的代码示例中,我都使用double数据类型。在Arduino上double其实和float(单精度)一样。真正的双精度对于PID有点大材小用了,如果你用的语言支持真正的双精度数据类型,建议你把他们都改成float。


 楼主| 发表于 2021-5-1 17:16 | 显示全部楼层

第二篇 初学者的PID-采样时间

问题:
初学者的PID被设计成可以被不按固定的时间间隔调用,这就导致两个问题:
  • 你不能得到稳定的PID输出,因为它可能有时候调用的很频繁,有时候又不是;
  • 你要在代码中计算积分项和微分项,因为这两项都依赖于采样的时间差;


解决办法:
发表于 2021-6-17 15:29 | 显示全部楼层
mark!
改天实践验证一下
 楼主| 发表于 2022-6-20 17:43 | 显示全部楼层
本帖最后由 SeanM 于 2022-6-21 17:15 编辑

第二篇 初学者的PID-采样时间(Sample Time)

问题:

初学者的PID被设计成可以被不按固定的时间间隔调用,这就导致两个问题:
  • 你不能得到稳定的PID输出,因为它可能有时候调用的很频繁,有时候又不是;
  • 你要在代码中计算积分项和微分项,因为这两项都依赖于采样的时间差;


解决办法:
保证按固定的时间间隔计算PID。我的方法是按照预先设定的采样时间(Sample Time),在每个周期调用一次PID计算函数,并让PID算法决定是要重算还是立即返回。
一旦我们决定按照固定的时间间隔计算PID,PID微分项和积分项的计算就大大简化了。哈!

代码:
  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double errSum, lastErr;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. void Compute()
  8. {
  9.    unsigned long now = millis();
  10.    int timeChange = (now - lastTime);
  11.    if(timeChange>=SampleTime)
  12.    {
  13.       /*Compute all the working error variables*/
  14.       double error = Setpoint - Input;
  15.       errSum += error;
  16.       double dErr = (error - lastErr);

  17.       /*Compute PID Output*/
  18.       Output = kp * error + ki * errSum + kd * dErr;

  19.       /*Remember some variables for next time*/
  20.       lastErr = error;
  21.       lastTime = now;
  22.    }
  23. }

  24. void SetTunings(double Kp, double Ki, double Kd)
  25. {
  26.   double SampleTimeInSec = ((double)SampleTime)/1000;
  27.    kp = Kp;
  28.    ki = Ki * SampleTimeInSec;
  29.    kd = Kd / SampleTimeInSec;
  30. }

  31. void SetSampleTime(int NewSampleTime)
  32. {
  33.    if (NewSampleTime > 0)
  34.    {
  35.       double ratio  = (double)NewSampleTime
  36.                       / (double)SampleTime;
  37.       ki *= ratio;
  38.       kd /= ratio;
  39.       SampleTime = (unsigned long)NewSampleTime;
  40.    }
  41. }
复制代码
在第10行和第11行,算法会自行决定是否需要重算。同时,由于我们已经知道了两次采样的时间间隔,因此在计算积分项和微分项的时候,不用再乘(或除)时间间隔了。是需要相应调整ki和kd就好了,这样在数学上结果是一样的,但是效率更高(31和32行)。
这样做有一点要注意。如果用户在PID运行过程中要想修改采样时间,那么需要同时修改ki和kd(39和40行)

你们可能注意到了,我在第29行把采样时间改成了以秒为单位。严格来讲,可以不用这么做。但是这种方法可以让用户输入ki和kd时以1/秒(1/sec)或秒(s)为单位,而不用以毫秒为单位。

结果:
上面的修改实现了3个改进
1.无论Compute( )调用得有多频繁,PID算法都会按照固定的时间间隔计算(11行)
2.因为使用时间相减,所以及时millis()绕回到0时也不会有问题(这种情况55天会发生一次)
3.我们不需要计算PID时再乘或除时间间隔。因为时间间隔是常数,所以我们可以把它从第15和16行中移除。仅仅需要用他们乘以固定的ki或kd(31和32行)。这样做在数学上没有差异,但是避免了在每一次计算PID时都做乘法和除法计算。

注释:使用中断
在MCU中使用PID时,可以用SetSampleTime来设置中断的频率,然后就可以按照这个频率调用Compute( )。这样的话,就不需要9-12、23和24行代码了。我在这个库里没有这么做,主要原因有三个:
1.并不是所有人都会用中断
2.当在MCU中用多个中断时会有一些麻烦
3.以后的版本用

 楼主| 发表于 2022-6-21 11:52 | 显示全部楼层

第三篇 初学者的PID-微分冲击(Derivative Kick)

本帖最后由 SeanM 于 2022-6-22 12:44 编辑

问题:
本篇的目的是解决“微分冲击”的问题
DonE.png

上面的图片展示了微分冲击现象。因为error=Setpoint-Input,任何对Setpoint的修改,都会导致error的大幅变化。error的微分理论上会达到无穷大(实际上,因为dt不是0,所以微分只会达到一个很大的值)。这个值代入到pid计算公式中,会导致输出值有一个尖峰。还好,这个问题不难解决。

解决办法:
DonMExplain.png


上面的公式证明,当设定的目标(Setpoint)不变时,误差值(Error)的微分等于输入值的(Input)微分。于是可以得出一个很完美的解决方案:使用(-kd*输入的微分)代替(kd*误差的微分)。这种方法就是使用了“对测量值的微分”(Derivative on Measurement)。

  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double errSum, lastInput;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. void Compute()
  8. {
  9.    unsigned long now = millis();
  10.    int timeChange = (now - lastTime);
  11.    if(timeChange>=SampleTime)
  12.    {
  13.       /*Compute all the working error variables*/
  14.       double error = Setpoint - Input;
  15.       errSum += error;
  16.       double dInput = (Input - lastInput);

  17.       /*Compute PID Output*/
  18.       Output = kp * error + ki * errSum - kd * dInput;

  19.       /*Remember some variables for next time*/
  20.       lastInput = Input;
  21.       lastTime = now;
  22.    }
  23. }

  24. void SetTunings(double Kp, double Ki, double Kd)
  25. {
  26.   double SampleTimeInSec = ((double)SampleTime)/1000;
  27.    kp = Kp;
  28.    ki = Ki * SampleTimeInSec;
  29.    kd = Kd / SampleTimeInSec;
  30. }

  31. void SetSampleTime(int NewSampleTime)
  32. {
  33.    if (NewSampleTime > 0)
  34.    {
  35.       double ratio  = (double)NewSampleTime
  36.                       / (double)SampleTime;
  37.       ki *= ratio;
  38.       kd /= ratio;
  39.       SampleTime = (unsigned long)NewSampleTime;
  40.    }
  41. }
复制代码
修改的内容(4、16、19和20行)很容易理解。我们把+dError替换成-dInput。同时,我们可以不用在记录lastError,改为记录lastInput。

结果:
DonM.png

上面就是修改代码后的结果。可以看到,输入没有改变,但是输出已经没有尖峰了。这是一个很大的改进。
如果你的应用对输出的尖峰不敏感,那么这个改进其实可有可无。但是这么做既不麻烦,又能避免冲击,何乐而不为呢?










 楼主| 发表于 2022-6-21 12:56 | 显示全部楼层
本帖最后由 SeanM 于 2022-6-21 12:57 编辑

第四篇 初学者的PID-改变参数(Tuning Changes)


问题:
在系统运行中改变PID的参数,对于所有的PID算法都是必须考虑的。
BadIntegral.png

如果使用我们目前的算法,在系统运行时改变参数就会造成很大的问题。来看看为什么,下表是使用目前的算法时,参数改变前和改变后的情况对比。
BadIntegralCode.png

可以很容易看到,由于积分项(Integral Term)的变化引起输出的凸起(bump)。为什么积分参数的变化,会导致输出的大幅变化呢?这是由于目前的算法中,积分项的计算方法带来的问题。

BadIntegralEqn1.png
在Ki不变时,这种算法没有什么问题。但是当ki改变时,算法将新的ki与整个error sum相乘。由于error sum时从开始时一直累计下来的,所以造成了积分项的大幅变化。这不是我们想要的。我们想要的时ki的变化仅影响未来的项目。

解决方案:
解决这个问题的办法有很多种。我原先用的办法是按比例调整errSum。当ki翻倍时,将errSum减半。这个办法可以避免积分项凸起(bumping)。这个办法有点笨,后来想想了个更好的办法。
新的办法要有一点代数知识:
GoodIntegralEqn.png

我们把Ki挪到积分号里面来。看着好像没有什么变化,用起来你就会看到差别。
我们先用error乘以目前的ki,然后把各个error*ki加在一起。当ki变化时,旧的ki就不再用了,因此积分项就不会变化很大。新旧ki就得以平稳过度。

代码:
  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double ITerm, lastInput;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. void Compute()
  8. {
  9.    unsigned long now = millis();
  10.    int timeChange = (now - lastTime);
  11.    if(timeChange>=SampleTime)
  12.    {
  13.       /*Compute all the working error variables*/
  14.       double error = Setpoint - Input;
  15.       ITerm += (ki * error);
  16.       double dInput = (Input - lastInput);

  17.       /*Compute PID Output*/
  18.       Output = kp * error + ITerm - kd * dInput;

  19.       /*Remember some variables for next time*/
  20.       lastInput = Input;
  21.       lastTime = now;
  22.    }
  23. }

  24. void SetTunings(double Kp, double Ki, double Kd)
  25. {
  26.   double SampleTimeInSec = ((double)SampleTime)/1000;
  27.    kp = Kp;
  28.    ki = Ki * SampleTimeInSec;
  29.    kd = Kd / SampleTimeInSec;
  30. }

  31. void SetSampleTime(int NewSampleTime)
  32. {
  33.    if (NewSampleTime > 0)
  34.    {
  35.       double ratio  = (double)NewSampleTime
  36.                       / (double)SampleTime;
  37.       ki *= ratio;
  38.       kd /= ratio;
  39.       SampleTime = (unsigned long)NewSampleTime;
  40.    }
  41. }
复制代码

我们把errSum换成ITerm(第4行)。ITerm等于各个 Ki*error的和,而不是error的和再乘以Ki。同时,因为Ki已经包含在ITerm中,因此可以把它从PID计算公式中去除掉(19行)

结果:
GoodIntegral.png

GoodIntegralCode.png

来总结一下,问题时怎么解决的。当Ki变化是,它影响了整个误差的合计(sum of the error)。修改代码后,以前的误差合计保持不变,新的Ki仅仅影响以后的误差。

 楼主| 发表于 2022-6-21 17:15 | 显示全部楼层

第五篇 初学者的PID-积分饱和(Reset Windup)

本帖最后由 SeanM 于 2022-6-21 17:24 编辑

问题
Windup.png

积分饱和是新手最经常遇到的问题。当PID算法不知道自己的上限在哪里的时候,这个问题最容易发生。比如说,arduino的PWM输出在0-255之间,一般来说PID并不知道这个限制。这就会导致PID的输出可能是300、400甚至是500。实际上,PWM被限制到了255以内,PID会不断输出更大的值,但实际上却到不了这个值。
积分饱和问题通常表现为奇怪的控制延迟。从上图可以看出来,PID的输出大于外部的限制。当控制目标(setpoint)被调低后,输出依然会在255的限制线以上,并持续一段时间。

解决方案-Step 1
No-Windup.png
有很多办法可以解决积分饱和的问题。我选择的解决方案是告诉PID输出的限制值是多少。下面代码中可以看到新加了一个SetOuputLimits()函数.当PID输出达到外部限制时,算法会停止积分。因为这时候再积分也没有用。由于积分不在饱和,当控制目标(setpoint)下降后到可以控制的区间后,PID的输出会立刻做出反映。

解决方案-Step 2
再仔细观察上面的图,可以发现当我们搞定了积分饱和造成的控制延迟后,事情被没有完全和我们预料的一样。由于比例项和微分项的影响,PID实际的输出与它以为的输出还是有差异。
即使积分项已经被控制了,P和D依然会导致结果超过输出上限。对于我来说,这还是不可以接受。因为调用了SetOutputLimits(),就应当保证输出不会超过上限。所以在Step 2,我们应该保证整个输出值(Output value)不高于设定值。
(你可能会想,为什么我们要同时控制输出值和积分项。只控制输出值不就好了吗?我们只控制输出值,那么积分项就会逐渐累计,越来越大。在这个时候,输出看起来没有什么以上,但是一旦调整了控制目标,你就能看到明显的延迟(因为积分项已经很大了,难以马上抵消掉))

代码
  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double ITerm, lastInput;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. double outMin, outMax;
  8. void Compute()
  9. {
  10.    unsigned long now = millis();
  11.    int timeChange = (now - lastTime);
  12.    if(timeChange>=SampleTime)
  13.    {
  14.       /*Compute all the working error variables*/
  15.       double error = Setpoint - Input;
  16.       ITerm+= (ki * error);
  17.       if(ITerm> outMax) ITerm= outMax;
  18.       else if(ITerm< outMin) ITerm= outMin;
  19.       double dInput = (Input - lastInput);

  20.       /*Compute PID Output*/
  21.       Output = kp * error + ITerm- kd * dInput;
  22.       if(Output > outMax) Output = outMax;
  23.       else if(Output < outMin) Output = outMin;

  24.       /*Remember some variables for next time*/
  25.       lastInput = Input;
  26.       lastTime = now;
  27.    }
  28. }

  29. void SetTunings(double Kp, double Ki, double Kd)
  30. {
  31.   double SampleTimeInSec = ((double)SampleTime)/1000;
  32.    kp = Kp;
  33.    ki = Ki * SampleTimeInSec;
  34.    kd = Kd / SampleTimeInSec;
  35. }

  36. void SetSampleTime(int NewSampleTime)
  37. {
  38.    if (NewSampleTime > 0)
  39.    {
  40.       double ratio  = (double)NewSampleTime
  41.                       / (double)SampleTime;
  42.       ki *= ratio;
  43.       kd /= ratio;
  44.       SampleTime = (unsigned long)NewSampleTime;
  45.    }
  46. }

  47. void SetOutputLimits(double Min, double Max)
  48. {
  49.    if(Min > Max) return;
  50.    outMin = Min;
  51.    outMax = Max;
  52.    
  53.    if(Output > outMax) Output = outMax;
  54.    else if(Output < outMin) Output = outMin;

  55.    if(ITerm> outMax) ITerm= outMax;
  56.    else if(ITerm< outMin) ITerm= outMin;
  57. }
复制代码

新加一个函数,让用户可以指定输出的限额(52-63行)。这个限额同时限制了积分项(I-Term)(17-18行)和输出值(Output)(23-24行)

结果
No-Winup-Clamped.png

结果
正如我们预料的一样,饱和现象被消灭了,输出也保持在我们要求的范围内。这意味着我们没有必要在算法外部另加限制。如果你要把输出限制在23-167的范围内,可以直接设置Output的范围。


译者注:关于积分饱和还可参考Integral (Reset) Windup, Jacketing Logic and the Velocity PI Form(https://controlguru.com/integral-reset-windup-jacketing-logic-and-the-velocity-pi-form/)
 楼主| 发表于 2022-6-30 15:08 | 显示全部楼层

第六篇 初学者的PID-开和关(On/Off)

本帖最后由 SeanM 于 2022-6-30 15:23 编辑

问题:
有时候我们要使用PID控制器,有时候也要关掉控制器
BadForcedOutput.png

让我们看一下,强制把PID的输出设置为固定值(例如0)会带来的问题。代码如下
  1. void loop()
  2. {
  3. Compute();
  4. Output=0;
  5. }
复制代码
在这个代码里,如论PID的输出是多少,我们只要重置输出即可。但是,在实践中,这个方法很不好。PID控制器就会很困扰:“我明明不断提高输出,但是好像什么都没有发生,让我再增加输出!”。结果,停止重写输出后,PID控制器要花很长时间来调整输出的值。

解决办法:
解决这个问题的办法是设计一套机制打开和关闭PID。开和关分别对应自动(Auto)和手动(Manual)。下面是代码实现:
  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double ITerm, lastInput;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. double outMin, outMax;
  8. bool inAuto = false;

  9. #define MANUAL 0
  10. #define AUTOMATIC 1

  11. void Compute()
  12. {
  13.    if(!inAuto) return;
  14.    unsigned long now = millis();
  15.    int timeChange = (now - lastTime);
  16.    if(timeChange>=SampleTime)
  17.    {
  18.       /*Compute all the working error variables*/
  19.       double error = Setpoint - Input;
  20.       ITerm+= (ki * error);
  21.       if(ITerm> outMax) ITerm= outMax;
  22.       else if(ITerm< outMin) ITerm= outMin;
  23.       double dInput = (Input - lastInput);

  24.       /*Compute PID Output*/
  25.       Output = kp * error + ITerm- kd * dInput;
  26.       if(Output > outMax) Output = outMax;
  27.       else if(Output < outMin) Output = outMin;

  28.       /*Remember some variables for next time*/
  29.       lastInput = Input;
  30.       lastTime = now;
  31.    }
  32. }

  33. void SetTunings(double Kp, double Ki, double Kd)
  34. {
  35.   double SampleTimeInSec = ((double)SampleTime)/1000;
  36.    kp = Kp;
  37.    ki = Ki * SampleTimeInSec;
  38.    kd = Kd / SampleTimeInSec;
  39. }

  40. void SetSampleTime(int NewSampleTime)
  41. {
  42.    if (NewSampleTime > 0)
  43.    {
  44.       double ratio  = (double)NewSampleTime
  45.                       / (double)SampleTime;
  46.       ki *= ratio;
  47.       kd /= ratio;
  48.       SampleTime = (unsigned long)NewSampleTime;
  49.    }
  50. }

  51. void SetOutputLimits(double Min, double Max)
  52. {
  53.    if(Min > Max) return;
  54.    outMin = Min;
  55.    outMax = Max;
  56.    
  57.    if(Output > outMax) Output = outMax;
  58.    else if(Output < outMin) Output = outMin;

  59.    if(ITerm> outMax) ITerm= outMax;
  60.    else if(ITerm< outMin) ITerm= outMin;
  61. }

  62. void SetMode(int Mode)
  63. {
  64.   inAuto = (Mode == AUTOMATIC);
  65. }
复制代码
【修改了8、10、11、15、71-74行】
办法很简单。如果不在自动模式中时,立即退出Compute()函数,而不调整输出和内部的其他变量。

结果

BetterForcedOutput.png


也可以在手动模式时不调用Compute()函数。但是,通过区分运行模式,并调用Compute()的方法可以保持PID一直在运行中,使得我们能够一直跟踪PID的运行模式,更重要的时,可以进行模式切换。

 楼主| 发表于 2022-7-1 12:00 | 显示全部楼层

第七篇 初学者的PID-初始化(Initialization)

本帖最后由 SeanM 于 2022-7-1 12:02 编辑

问题:
上一篇我们研究了怎么开关PID。我们已经关掉了PID,现在就来看一下再把它打开会发生什么。
NoInitialization.png
擦!PID的输出跳回了上一次输出的数值,然后从那个数值开始逐步调整。这导致了我们不愿意看到的输入的凸起(bump)。

解决办法:
这个问题很容易解决。当我们重新打开PID的时候(从手动模式变自动模式)时,我们要初始化PID参数,保证模式的平滑切换。也就是熨平Iterm和lastInput两个变量的变化,保证输出不会跳变。

代码如下:
我们修改了SetMode(…)函数,使它可以检测PID从手动到自动的过程,并在切换过程中调用初始化函数。初始化函数令ITerm=Output来处理积分项,令lastInput=Input来保证没有微分冲击。比例项不依赖pid停止前的信息,所以不需要初始化。

[修改了73-78行,81-87行]
  1. /*working variables*/
  2. unsigned long lastTime;
  3. double Input, Output, Setpoint;
  4. double ITerm, lastInput;
  5. double kp, ki, kd;
  6. int SampleTime = 1000; //1 sec
  7. double outMin, outMax;
  8. bool inAuto = false;

  9. #define MANUAL 0
  10. #define AUTOMATIC 1

  11. void Compute()
  12. {
  13.    if(!inAuto) return;
  14.    unsigned long now = millis();
  15.    int timeChange = (now - lastTime);
  16.    if(timeChange>=SampleTime)
  17.    {
  18.       /*Compute all the working error variables*/
  19.       double error = Setpoint - Input;
  20.       ITerm+= (ki * error);
  21.       if(ITerm> outMax) ITerm= outMax;
  22.       else if(ITerm< outMin) ITerm= outMin;
  23.       double dInput = (Input - lastInput);

  24.       /*Compute PID Output*/
  25.       Output = kp * error + ITerm- kd * dInput;
  26.       if(Output> outMax) Output = outMax;
  27.       else if(Output < outMin) Output = outMin;

  28.       /*Remember some variables for next time*/
  29.       lastInput = Input;
  30.       lastTime = now;
  31.    }
  32. }

  33. void SetTunings(double Kp, double Ki, double Kd)
  34. {
  35.   double SampleTimeInSec = ((double)SampleTime)/1000;
  36.    kp = Kp;
  37.    ki = Ki * SampleTimeInSec;
  38.    kd = Kd / SampleTimeInSec;
  39. }

  40. void SetSampleTime(int NewSampleTime)
  41. {
  42.    if (NewSampleTime > 0)
  43.    {
  44.       double ratio  = (double)NewSampleTime
  45.                       / (double)SampleTime;
  46.       ki *= ratio;
  47.       kd /= ratio;
  48.       SampleTime = (unsigned long)NewSampleTime;
  49.    }
  50. }

  51. void SetOutputLimits(double Min, double Max)
  52. {
  53.    if(Min > Max) return;
  54.    outMin = Min;
  55.    outMax = Max;
  56.    
  57.    if(Output > outMax) Output = outMax;
  58.    else if(Output < outMin) Output = outMin;

  59.    if(ITerm> outMax) ITerm= outMax;
  60.    else if(ITerm< outMin) ITerm= outMin;
  61. }

  62. void SetMode(int Mode)
  63. {
  64.     bool newAuto = (Mode == AUTOMATIC);
  65.     if(newAuto && !inAuto)
  66.     {  /*we just went from manual to auto*/
  67.         Initialize();
  68.     }
  69.     inAuto = newAuto;
  70. }

  71. void Initialize()
  72. {
  73.    lastInput = Input;
  74.    ITerm = Output;
  75.    if(ITerm> outMax) ITerm= outMax;
  76.    else if(ITerm< outMin) ITerm= outMin;
  77. }
复制代码

结果:
Initialization.png
从上面的图里可以看出,适当的初始化方法可以让手动模式和自动模式平滑切换。这正是我们想要的。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino爱好者

GMT+8, 2022-12-6 13:02 , Processed in 0.076969 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表