初识PID算法

    在工控中,PID算是运用比较广泛的算法了。看了一篇老外的博客,感觉不错,记录下来(PS:个人四级没过,翻译的不好,勿喷。附上原文链接,英语好的看原文,不好的就忍受我蹩脚的理解与翻译吧)。

    PS:新手参看解释文:https://blog.csdn.net/he__yuan/article/details/80739800?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task(适合我这种的)

    大手参看解释文:https://blog.csdn.net/qq229596421/article/details/51419813?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

    现有一辆无人车,我们要控制它以匀速 5m/s 运动到目标点。初始,小车无法直接从零速蹦到 5m/s,这就需要后期速度补偿,以达成小车全程运动是均速 5m/s。并且事实上,小车达到目的速度以后,本身硬件上仍然无法绝对维持速度一直是 5m/s(或者人工后期想改成10m/s),所以至始至终,都需要速度补偿,让均速无限接近目的速度。为应对此类现象,出现了一大批工控算法,在这些算法里,PID运用较为广泛,在这里做初始介绍。

    PID(Proportional 比例、Integral 积分、Differential 微分),如其名,以三种方式得到实际输出值,达到工控目的。这篇文章,对两类人有用:

    1、想理解 PID 算法的;

    2、想获取他人思路写出自己 PID 算法的。

    直接写一个具有高效的、鲁棒性的、可广泛运用的算法,对普通人来说有点难,所以可以化成台阶,恰似打怪升级,一步步去完善,最终获取成果。

    这是 PID 的算法公式:

    一眼看上去很恐怖是不是?我四级八次没过(是真的八次!甚憾至今),因高数一直挂,导致大学还留了一级,现在又给我看这个,如同当初出卷老师的心思一样,想我死吗?当初放弃最多拿不到毕业证,现在放弃吃不上饭呀!我太难了。。

    算了,生活还要继续,咱们接着往下来。上面公式可令初学者写出以下原始算法(反正我写不出):

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
void Compute()
{
   /*How long since we last calculated*/
   unsigned long now = millis();
   double timeChange = (double)(now - lastTime);
  
   /*Compute all the working error variables*/
   double error = Setpoint - Input;
   errSum += (error * timeChange);
   double dErr = (error - lastErr) / timeChange;
  
   /*Compute PID Output*/
   Output = kp * error + ki * errSum + kd * dErr;
  
   /*Remember some variables for next time*/
   lastErr = error;
   lastTime = now;
}
  
void SetTunings(double Kp, double Ki, double Kd)
{
   kp = Kp;
   ki = Ki;
   kd = Kd;
}

    Compute() 被定期或不定期调用,效果不错。但作为程序员,直接 Ctrl + C/V 过去,不符合咱们的气质,还需要考虑拷贝到应用的实际问题:

    1、采样时间 – 如果定期对PID算法进行评估,则其效果最佳。如果算法知道此间隔,我们还可以简化一些内部数学运算;
    2、剔除衍生峰值 – 不是重点,但易于处理;
    3、实时调整参数 – 一种好的PID算法可以在不影响内部工作的情况下更改调整参数;
    4、减轻重置损失 – 我们将介绍什么是重置消除,并实施具有附带好处的解决方案;
    5、开/关(自动/手动)– 在大多数应用中,有时需要关闭PID控制器并手动调节输出,而不会干扰控制器;
    6、初始化 – 控制器首次开启时,我们希望进行“无扰动的传输”。也就是说,我们不希望输出突然变为某个新值;
    7、控制器指导 – 这最后一个并没有改变坚固性本身的名称。它旨在确保用户输入带有正确符号的调整参数;
    8、新增:比例测量 – 添加此功能使控制某些类型的过程更加容易(我没有找到此项,老外都这么皮吗?)。

    既然列出问题了,那就一一解决吧,这是程序员的宿命。爆发吧,小宇宙!

问题一 采样时间

    原始算法被不定期调用,会导致两个问题:

    1、你无法从PID中获得一致的行为,因为有时会经常调用它,有时却不会。
    2、你需要做额外的数学运算来计算导数和积分,因为它们都取决于时间的变化。

    解决方案:

    确保定期调用PID。执行此操作的方法是指定每个周期调用一次计算函数。 根据预定的采样时间,PID决定是应该立即计算还是返回。

    一旦知道以恒定的间隔对PID进行评估,就可以简化导数和积分计算。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      errSum += error;
      double dErr = (error - lastErr);
 
      /*Compute PID Output*/
      Output = kp * error + ki * errSum + kd * dErr;
 
      /*Remember some variables for next time*/
      lastErr = error;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

    在第10和11行,该算法自行决定是否需要计算。 另外,由于我们现在知道采样之间的时间是相同的,因此我们不需要不断地乘以时间变化。 我们只能适当地调整Ki和Kd(第31和32行),故结果在数学上是等效的,但效率更高。

    虽然这样做有点那啥(意会吧,不知道咋翻译)。 如果用户决定在操作过程中更改采样时间,则需要重新调整Ki和Kd以反映此新更改。 这就是第39-42行的全部内容。

    另请注意,我在第29行将采样时间转换为秒。严格来说,这不是必需的,但是允许用户以1 / sec和s为单位输入Ki和Kd,而不是1 / mS和mS。

    上面的更改为我们做了三件事:
    1、不管调用 Compute() 的频率如何,PID算法都会以固定的时间间隔进行评估[第11行]
    2、由于时间相减[第10行],当 millis() 换回0时不会有任何问题。这只会每55天发生一次,但是我们需要防备吗?
    3、我们不再需要乘以时间变化。 由于它是一个常数,因此我们可以将其从计算代码中移出[第15 + 16行],然后将其与调整常数一起输入[第31 + 32行]。 从数学上讲,它的计算结果相同,但是每次对PID求值时都保存了乘法和除法运算。

    关于中断的旁注:
    如果此PID进入微控制器,则可以使用中断作为一个很好的依据。 SetSampleTime() 设置中断频率,然后在时间到达时调用函数 Compute() 。 在这种情况下,不需要9-12、23和24行。如果您打算通过PID强制执行此操作,那就去吧骚年,看好你哟! 不过请继续阅读本系列。 希望您会从后续修改中受益。
    我没有使用中断的三个原因:
    1、就本系列而言,并不是每个人都可以使用中断。
    2、如果您希望它同时实现许多PID控制器,事情将会变得棘手。
    3、老实说,这不是我想的。 吉米·罗杰斯(Jimmie Rodgers)在为我校对该系列作品时提出了建议。 我可能决定在PID库的将来版本中使用中断。

问题二 剔掉衍生峰值

    此次修改将微调派生项。 目的是消除被称为“衍生踢”的现象(衍生踢是谷歌给出的翻译,标题也是,但我看着不爽,改成“剔掉衍生峰值”了,如果有看客内行,还望能联系本人改下,拜谢~)。

    再来一张图,被高数折磨多年的我,再次瑟瑟发抖。

    上图显示了该问题。 由于error = Setpoint - Input,因此,Setpoint 中的任何更改都会导致瞬时错误。 这种变化的导数是无穷大(实际上,由于dt不为0,所以它实际上是一个非常大的数字)。该数字被输入pid方程,这会导致输出出现不希望的峰值。 幸运的是,有一种简单的方法可以摆脱这种情况。

    解决方案:

    看不懂啊看不懂!少壮不努力,老大徒伤悲!多么痛的领悟。。代码还要继续,囫囵吞枣吧。

    结果表明,当设定值更改时,误差的导数等于输入的负导数,除非最后这是一个完美的解决方案。 而不是添加(Kd * Error的导数),而减去(Kd * Input的导数)。 这被称为使用“测量导数”。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      errSum += error;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ki * errSum - kd * dInput;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

    此处的修改非常简单。 我们将+ dError替换为-dInput。 现在我们不用记住lastError了,而是记住了lastInput。

    结果:

    修改为我们带来了这些变化。 请注意,输入看起来仍然差不多。 因此,我们获得了相同的性能,但是每次设定值更改时,我们都不会产生巨大的输出峰值。
    这可能不重要。 这完全取决于你的应用程序对输出峰值的敏感程度。 从我的角度来看,不费吹灰之力就不需要做更多的工作,那何乐而不为呢?

问题三 参数变更

    对于任何受人尊敬的PID算法,必须具有在系统运行时更改调节参数的功能。

    如果你尝试在运行时更改参数,则原始算法会有点疯狂(想想我们代码测完事了,一会儿要上线,产品说:来来来,咱们再加个小需求~)。 让我们看看为什么?这是上述参数更改前后原始算法的状态:

    

    因此,我们可以立即将此积分归咎于积分项(即 “ i 项”)。 当参数更改时,这是唯一发生剧烈变化的事情。 为什么会这样呢? 这与原始算法对积分的解释有关:

    直到更改 Ki 为止,这种解释才能正常进行。 然后,突然将这个新 Ki 乘以您累积的整个误差总和。 那不是我们想要的! 我们只想影响前进的脚步!

    解决方案:

    我知道有几种方法可以解决此问题。 我在上一个库中使用的方法是重新缩放 errSum。Ki 加倍? 将 errSum 减半。 这样可以防止 i Term 发生碰撞,并且可以正常工作。 虽然有点笨拙,但我想出了一些更优雅的方法。
    该解决方案需要一些基本的代数(或者是微积分?)

    没有将 Ki 置于积分之外,而是将其引入了积分。 看起来我们什么都没做,但是我们会发现实际上这有很大的不同。
    现在,我们采用误差并将其乘以当时的 Ki 。 然后,我们存储 THAT 的总和。 当 Ki 发生变化时,就不会有颠簸了,因为可以说所有旧 Ki 都已经在“银行”中了。 我们无需额外的数学运算即可顺利进行转换。 它可能会让我成为一个极客,但我认为这很性感(所谓“极客”,很多人的追求,追着追着,慢慢就变成常人眼中的变态了,你看,说话都不正常了~)。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm += (ki * error);
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm - kd * dInput;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}

    因此,我们用复合ITerm变量[第4行]替换了 errSum 变量。 它总结了 Ki * error ,而不仅仅是错误[第15行]。 另外,由于 Ki 现在埋在 i Term 中,因此已从主 PID 计算中删除了该行[第19行]。

    结果:

    在更改 ki 之前,它会重新调整整个错误的总和。 我们看到的每个错误值。 使用此代码,以前的错误将保持不变,新的 ki 仅影响前进的事情,这正是我们想要的。

问题四 界限

    凡事都有个度哈~ PID 算法也不例外。

    重置结束是一个陷阱,它可能要求比任何其他人更多的基础背景。 当 PID 认为自己可以做一些自己做不到的事情时,就会发生这种情况。 例如,Arduino 上的 PWM 输出接受 0-255 之间的值。 默认情况下,PID 不知道这一点。 如果它认为 300-400-500 可以使用,它将尝试使用那些期望得到所需的值。 由于实际上该值固定在 255,因此它将继续尝试越来越多的数字而无所适从。
问题以怪异的滞后的形式显现出来。 在上方,我们可以看到输出“超出”外部极限。 降低设定点后,输出必须先减小,然后才能降至 255 行以下。

    解决方案:

    步骤1

    有几种缓解缠绕的方法,但我选择的方法如下:告诉 PID 输出限制是什么。 在下面的代码中,您将看到一个 SetOuputLimits 函数。 一旦达到任一限制,PID 便停止求和(积分)。它知道无事可做。 由于输出不会结束,因此当设定值下降到可以执行某项操作的范围时,我们会立即得到响应。

    步骤2

    请注意,在上图中,尽管我们消除了饱和滞后,但并没有完全解决。 pid 认为发送的内容与发送的内容之间仍然存在差异。为什么?比例项和(在较小程度上)微分项。
    即使已对积分项进行了安全钳位,P和D仍在加2美分,其结果高于输出限制。我认为这是不可接受的。如果用户调用了一个名为“ SetOutputLimits”的函数,则他们必须假设这意味着“输出将保持在这些值之内。”因此,对于第2步,我们做出一个有效的假设。除了限制I项,我们还限制输出值,使其保持在我们期望的位置。
    注意:您可能会问为什么我们需要同时钳制两个。如果我们无论如何都要进行输出,为什么还要分别钳制积分呢?如果我们所做的只是钳制输出,那么积分术语将回到增长的状态。尽管在升压过程中输出看起来不错,但在降压过程中我们会看到明显的滞后。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
void Compute()
{
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm> outMax) ITerm= outMax;
      else if(ITerm< outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
    
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}

    添加了一个新功能,允许用户指定输出限制[第52-63行]。 这些限制用于钳制I项[17-18]和输出[23-24]。

    结果:

    如我们所见,消除了缠绕。 另外,输出保持在我们想要的位置。 这意味着不需要外部钳位输出。 如果您希望它的范围是 23 到167,则可以将其设置为“输出限制”。

问题五 开关控制

    拥有 PID 控制器就好了,有时候根本不在乎它在说什么。

    假设你要在程序中的某个时刻将输出强制为某个值(例如0),当然可以在调用例程中执行此操作:

    void loop()
    {
        Compute();
        Output=0;
    }

    这样,无论PID怎么说,都可以覆盖它的值。 但是,这在实践中是一个可怕的想法。 PID会变得非常混乱:“我一直在移动输出,什么也没发生! 是什么赋予了?! 结果,当您停止重写输出并切换回PID时,输出值可能会立即发生巨大变化。

    解决此问题的方法是有一种方法可以打开和关闭PID。 这些状态的常用术语是“手动”(我将手动调节值)和“自动”(PID将自动调节输出)。 让我们看看如何通过代码完成此操作:

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm> outMax) ITerm= outMax;
      else if(ITerm< outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
    
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
  inAuto = (Mode == AUTOMATIC);
}

    一个相当简单的解决方案。 如果你未处于自动模式,请立即退出计算功能,而无需调整输出或任何内部变量。

结果:

    的确,你可以通过不从调用例程中调用 Compute 来达到类似的效果,但是这种解决方案可以保留 PID 的工作原理,这正是我们所需要的。 通过将内容保持在内部,我们可以跟踪所处的模式,更重要的是,它让我们知道何时更改模式。 这就引出了下一个问题……

问题六 初始化

    在上一节中,我们实现了打开和关闭 PID 的功能。 我们关闭了它,但是现在让我们看一下重新打开它会发生什么:

    PID 跳回到它发送的最后一个输出值,然后从那里开始调整。 这会导致我们不愿遇到的输入颠簸。

    这个很容易修复。 现在我们知道打开电源的时间(从手动到自动),因此我们只需初始化即可进行平滑过渡。 这意味着要对2个存储的工作变量(ITerm和lastInput)进行按摩,以防止输出跳跃。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm> outMax) ITerm= outMax;
      else if(ITerm< outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output> outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
    
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto && !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   ITerm = Output;
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}

    我们修改了 SetMode(...) 以检测从手动到自动的过渡,并添加了初始化功能。 它将 ITerm = Output 设置为整数项,并设置lastInput = Input 以防止派生峰值。 比例项不依赖于过去的任何信息,因此不需要任何初始化。

    结果:

    从上图可以看出,正确的初始化会导致从手动到自动的无扰动过渡:这正是我们所追求的。

问题七 方向

    PID 将连接的过程分为两类:正作用和反作用。 到目前为止,我展示的所有示例都是直接表演。 即,输出的增加导致输入的增加。 对于反向作用过程,则相反。 例如,在冰箱中,冷却的增加导致温度下降。 为了使原始算法可以逆向工作,kp,ki 和 kd 的符号都必须为负。
    这本身不是问题,但是用户必须选择正确的符号,并确保所有参数都具有相同的符号。

    解决方案:

    为了使过程更简单,我要求 kp,ki 和 kd 均 > = 0。 如果用户连接到反向过程,则他们使用 SetControllerDirection 函数单独指定该过程。 这样可以确保所有参数都具有相同的符号,并希望使事情更直观。

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm > outMax) ITerm= outMax;
      else if(ITerm < outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

     结课啦~

     我们已经将“原始算法”变成了我目前所知道的最强大的控制器。 对于那些正在寻找 PID 库详细说明的读者,希望你能从中得到帮助。 对于那些编写自己的 PID 的人,希望你能够收集一些想法,从而节省一些时间。
    最后两个注意事项:
    1、如果本系列中的某些内容看起来不对,请通知我。 我可能错过了一些东西,或者可能只是需要在我的解释中更清楚一些。 无论哪种方式,我都想知道。
    2、这只是基本的PID。 为了简化起见,我故意遗漏了许多其他问题。 让我烦恼的是:使用速度而不是位置来进行前馈,重置领带,整数数学,不同的pid形式。 如果有兴趣让我探索这些主题,请告诉我(写这个博客的老外,不是我。ps:这里不讨论佛学)。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值