改进初学者的PID-测量的比例编码

最近看到了Brett Beauregard发表的有关PID的系列文章,感觉对于理解PID算法很有帮助,于是将系列文章翻译过来!在自我提高的过程中,也希望对同道中人有所帮助。作者Brett Beauregard的原文网址:http://brettbeauregard.com/blog/2017/06/proportional-on-measurement-the-code/

 

在上一篇文章中,我把所有的时间都花在解释了比例测量的好处上。在这篇文章中,我将解释代码。人们似乎很欣赏我上次一步一步地解释事情的方式,所以在此我也将采取这样的方式。下面的3个步骤详细介绍了我是如何将 PonM 添加到 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#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;#define P_ON_M 0
#define P_ON_E 1
bool PonE = true;
double initInput;
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 P-Term*/if(PonE) Output = kp * error;else Output = -kp * (Input-initInput);/*Compute Rest of PID Output*/Output += 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,int pOn)
{if (Kp<0 || Ki<0|| Kd<0) return;PonE = pOn == P_ON_E;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;initInput = Input;ITerm = Output;if(ITerm > outMax) ITerm= outMax;else if(ITerm < outMin) ITerm= outMin;
}void SetControllerDirection(int Direction)
{controllerDirection = Direction;
}

随着输入的变化,测量的比例提供了越来越大的阻力,但如果没有参照系,我们的表现会有些不稳定。如果我们第一次打开控制器时的PID输入是10000,我们真的想从Kp*10000开始抵制吗?不,我们希望使用我们的初始输入作为参考点 (第108行),从那里开始随着输入的变化增加或减少阻力 (第38行)。

我们需要做的另一件事是允许用户选择是要在偏差上做比例或在测量上做比例。在最后一个帖子之后,它看起来像 PonE 是无用的,但重要的是要记住,对于许多回路,它工作的很好。因此,我们需要让用户选择他们想要的模式 ,然后在计算中相应地操作。

第二阶段–动态更改整定参数

虽然上面的代码确实有效,但它有一个我们以前看到的问题。当整定参数在运行时发生更改,我们会得到一个不希望出现的信号。

为什么会这样?

上次我们看到这一点时,是积分项被一个新的Ki 重新调整。而这一次,是比例项(输入-初始输入) 被新的Kp所更改。我选择的解决方案类似于我们为 Ki 所做的:我们不再是将(输入-初始输入)作为一个整体乘以当前 Kp,而是我把它分解成单独的步骤乘以当时的Kp:

/*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;#define P_ON_M 0
#define P_ON_E 1bool PonE = true;
double PTerm;
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 P-Term*/if(PonE) Output = kp * error;else{PTerm -= kp * dInput;Output = PTerm;}/*Compute Rest of PID Output*/Output += 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,int pOn)
{if (Kp<0 || Ki<0|| Kd<0) return;PonE = pOn == P_ON_E;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;PTerm = 0;ITerm = Output;if(ITerm > outMax) ITerm= outMax;else if(ITerm < outMin) ITerm= outMin;
}void SetControllerDirection(int Direction)
{controllerDirection = Direction;
}

我们现在保留一个有效的和组成的P项,而不是将输入-初始输入作为一个整体乘以Kp。在每一步中,我们只需将当前输入变化乘以当时的Kp,并从P项(第41行) 中减去它。在这里,我们可以看到变化的影响:

因为旧的Kp是已经存储,调整参数的变化只会影响我们后续的过程。

最终阶段–求和问题。

我不会进入完整的细节 (花哨的趋势等) 以及上述代码有什么问题。这相当不错,但仍有重大问题。例如:

类式积分饱和:虽然最终的输出限制在OUTmin和OUTmax之间。当PTerm不应该增长时,它有可能增长。它不会像积分饱和那样糟糕,但仍然是不可接受的。

动态更改:在运行时,如果用户想从P _ On _ M 更改为P_ ON _E,并在一段时间后返回,那么P项将不会被初始化,这会导致输出振荡。

还有更多,但仅仅这些就足以让人看到真正的问题是什么。早在我们创建I项的时候,我们已经处理过所有这些问题。我没有对P项重新执行相同的解决方案,而是选择了一个更美观的解决方案。

通过将P项和I项合并到一个名为“outputSum”的变量中,P _ ON _ M 代码将受益于已存在的所有上下文修补程序,并且由于代码中没有两个总和,因此不会出现不必要的冗余。

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double outputSum,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;#define P_ON_M 0
#define P_ON_E 1
bool PonE = true;
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;double dInput = (Input - lastInput);outputSum+= (ki * error);/*Add Proportional on Measurement,if P_ON_M is specified*/if(!PonE) outputSum-= kp * dInput;if(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= outMin;/*Add Proportional on Error,if P_ON_E is specified*/if(PonE) Output = kp * error;else Output = 0;/*Compute Rest of PID Output*/Output += outputSum - 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,int pOn)
{if (Kp<0 || Ki<0|| Kd<0) return;PonE = pOn == P_ON_E;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(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= 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;outputSum = Output;if(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= outMin;
}void SetControllerDirection(int Direction)
{controllerDirection = Direction;
}

现在你可以获得它。因为上述功能现在已存在于 V1.2.0版的Arduino PID库中。

但等等,还有更多:设定点加权。

我没有将下面的代码添加到Arduino库代码中,但是如果您想滚动自己的代码,这个特性可能会很有趣。设置点权重的核心是同时拥有PonE和PonM。通过指定0和1之间的比值,可以得到100% PonM、100% PonE(分别)或介于两者之间的某个比值。如果您的流程不是完全集成的(比如回流炉),并且希望解释这一点,那么这将非常有用。

最终,我决定不在这个时候将它添加到库中,因为它最终会成为另一个需要调整/解释的参数,而且我不认为这样做的好处是值得的。无论如何,如果你想修改代码,使其具有设定值权重,而不仅仅是纯PonM/PonE选择,下面是代码:

/*working variables*/
unsigned long lastTime;
double Input,Output,Setpoint;
double outputSum,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;#define P_ON_M 0
#define P_ON_E 1
bool PonE = true,pOnM = false;
double PonEKp,pOnMKp;
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;double dInput = (Input - lastInput);outputSum+= (ki * error);/*Add Proportional on Measurement,if P_ON_M is specified*/if(pOnM) outputSum-= pOnMKp * dInput;if(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= outMin;/*Add Proportional on Error,if P_ON_E is specified*/if(PonE) Output = PonEKp * error;else Output = 0;/*Compute Rest of PID Output*/Output += outputSum - 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 pOn)
{if (Kp<0 || Ki<0|| Kd<0 || pOn<0 || pOn>1) return;PonE = pOn>0; //some p on error is desired;pOnM = pOn<1; //some p on measurement is desired;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);}PonEKp = pOn * kp;pOnMKp = (1 - pOn) * kp;
}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(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= 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;outputSum = Output;if(outputSum > outMax) outputSum= outMax;else if(outputSum < outMin) outputSum= outMin;
}void SetControllerDirection(int Direction)
{controllerDirection = Direction;
}

没有将pOn设置为整数,而是以双精度输入,允许使用一个比率(第58行)。除了一些标记(第62行和第63行)外,第77-78行计算了加权Kp项。然后在第37行和第43行,将加权后的PonM和PonE贡献添加到整个PID输出中。

欢迎关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499394.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PID:我应该何时计算积分项?

最近看到了Brett Beauregard发表的有关PID的系列文章&#xff0c;感觉对于理解PID算法很有帮助&#xff0c;于是将系列文章翻译过来&#xff01;在自我提高的过程中&#xff0c;也希望对同道中人有所帮助。作者Brett Beauregard的原文网址&#xff1a;http&#xff1a;//brettb…

Arduino PID自整定库

最近看到了Brett Beauregard发表的有关PID的系列文章&#xff0c;感觉对于理解PID算法很有帮助&#xff0c;于是将系列文章翻译过来&#xff01;在自我提高的过程中&#xff0c;也希望对同道中人有所帮助。作者Brett Beauregard的原文网址&#xff1a;http&#xff1a;//brettb…

LwIP应用开发笔记之二:LwIP无操作系统UDP服务器

前面我们已经完成了LwIP协议栈基于逻辑的基本移植&#xff0c;在这一节我们将以RAW API来实现UDP服务器。 1、UDP协议简述 UDP协议全称是用户数据报协议&#xff0c;在网络中它与TCP协议一样用于处理数据包&#xff0c;是一种无连接的协议。在OSI模型中&#xff0c;处于传输层…

LwIP应用开发笔记之三:LwIP无操作系统UDP客户端

前一节我们实现了基于RAW API的UDP服务器&#xff0c;在接下来&#xff0c;我们进一步利用RAW API实现UDP客户端。 1、UDP协议简述 UDP协议全称是用户数据报协议&#xff0c;在网络中它与TCP协议一样用于处理数据包&#xff0c;是一种无连接的协议。在OSI模型中&#xff0c;处…

LwIP应用开发笔记之四:LwIP无操作系统TFTP服务器

前面我们已经实现了UDP的回环客户端和回环服务器的简单应用&#xff0c;接下来我们实现一个基于UDP的简单文件传输协议TFTP。 1、TFTP协议简介 TFTP是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议&#xff0c;提供不复杂、开销不大的文件传输服务。端…

LwIP应用开发笔记之五:LwIP无操作系统TCP服务器

前面我们实现了UDP服务器及客户端以及基于其上的TFTP应用服务器。接下来我们将实现同样广泛应用的TCP协议各类应用。 1、TCP简述 TCP&#xff08;Transmission Control Protocol 传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议&#xff0c;由…

LwIP应用开发笔记之六:LwIP无操作系统TCP客户端

上一篇我们基于LwIP协议栈的RAW API实现了一个TCP服务器的简单应用&#xff0c;接下来一节我们来实现一个TCP客户端的简单应用。 1、TCP简述 TCP&#xff08;Transmission Control Protocol 传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议&a…

LwIP应用开发笔记之七:LwIP无操作系统HTTP服务器

前面我们实现了TCP服务器和客户端的简单应用&#xff0c;接下来我们实现一个基于TCP协议的应用协议&#xff0c;那就是HTTP超文本传输协议。 1、HTTP协议简介 超文本传输协议&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff0c;简称HTTP&#xff0c;是一种基于…

LwIP应用开发笔记之八:LwIP无操作系统HTTP客户端

前面我们实现了TCP服务器和客户端的简单应用&#xff0c;接下来我们实现一个基于TCP协议的应用协议&#xff0c;那就是HTTP超文本传输协议 1、HTTP协议简介 超文本传输协议&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff0c;简称HTTP&#xff0c;是一种基于T…

LwIP应用开发笔记之九:LwIP无操作系统TELNET服务器

前面我们已经实现了基于RAW API的TCP服务器和客户端&#xff0c;也在此基础上实现了HTTP应用。接下来我们实现一个基于TCP的Telnet服务器应用。 1、Telnet协议简介 Telnet协议是TCP/IP协议族中的一员&#xff0c;是Internet远程登陆服务的标准协议和主要方式。它为用户提供了…

在ARM Cortex-M上实现FreeRTOS性能计数器

说明&#xff1a;本文翻译自Erich Styger的文章《Implementing FreeRTOS Performance Counters on ARM Cortex-M》&#xff0c;文章的权属属于原作者。 当使用像FreeRTOS这样的RTOS时&#xff0c;迟早要问一个问题&#xff1a;每个任务花费多少时间&#xff1f;基于Eclipse的M…

STM32学习及开发笔记八:采用主从计时器实现精确脉冲输出

脉冲信号用于设备控制是非常常见的&#xff0c;但在一些情况下&#xff0c;我们希望精确的控制脉冲的数量以实现对运动的精确控制。实现的方式也许有多种多样&#xff0c;但使用计时器来实现此类操作是人们比较容易想到的。 1、原理概述 我们知道在STM32平台上&#xff0c;使…

外设驱动库开发笔记0:EPD总体设计

在产品开发过程中&#xff0c;不可避免需要使用很多外部的元件及传感器&#xff0c;这些元器件也许是板载的&#xff0c;也许是板外的&#xff0c;但不管怎样&#xff0c;为其开发驱动程序都是必须的。每次都需要为这些元器件编写驱动程序。但每次重复编写调试很麻烦&#xff0…

外设驱动库开发笔记1:AD56xx系列DAC驱动

DAC在我们的项目中经常使用到&#xff0c;而使用最多的就是AD56xx系列&#xff0c;包括有单通道的AD5662、双通道的AD5623和AD5663、以及四通道的AD5624和AD5664等。出于方便复用的原因&#xff0c;我们设计并实现AD56xx系列DAC的驱动。 1、功能概述 AD56xx系列DAC属于nanoDA…

外设驱动库开发笔记2:AD8400系列数字电位器驱动

一些时候我们需要在系统使用过程中改变某些电路电阻值以达到改变设定的目的&#xff0c;这时候我们就会使用电位器。在我们使用数字控制电路时多选择数字电位器。在这一篇我们就来设计AD8400系列数字电位器的驱动。 1、功能概述 AD8400/AD8402/AD8403分别是单通道/双通道/四通…

外设驱动库开发笔记3:AD527x系列数字电位器驱动

在一些时候我们需要使用精度更高的数字电位器来实现我们的应用。我们经常使用AD527x系列数字电位器来实现这类应用。在通常情况下&#xff0c;AD527x系列数字电位器完全能够满足要求。为了减少重复工作&#xff0c;在这里我们将分系并实现AD527x系列数字电位器的驱动。 1、功能…

PID控制器改进笔记之一:改进PID控制器之参数动态调整

前面我们发布了一系列PID控制器相关的文章&#xff0c;包括经典PID控制器以及参数自适应的PID控制器。这一系列PID控制器虽说实现了主要功能&#xff0c;也在实际使用中取得了良好效果&#xff0c;但还有很多的细节部分可以改进以提高性能和灵活性。所以在这篇中我们来讨论改进…

外设驱动库开发笔记4:AD9833函数发生器驱动

很多时候我们需要输出某种函数信号&#xff0c;如方波、三角波、正弦波等&#xff0c;但想要获得这样的函数信号&#xff0c;不论是硬件电路还是软件实现&#xff0c;却并不是一件简单的事情。不过AD9833这类函数生成芯片可以简化这方面的操作&#xff0c;这一节我们就来设计并…

PID控制器改进笔记之二:改进PID控制器之手自动切换

前面我们发布了一系列PID控制器相关的文章&#xff0c;包括经典PID控制器以及参数自适应的PID控制器。这一系列PID控制器虽说实现了主要功能&#xff0c;也在实际使用中取得了良好效果&#xff0c;但还有很多的细节部分可以改进以提高性能和灵活性。所以在这篇中我们来讨论改进…

外设驱动库开发笔记5:AD7705系列ADC驱动

我们的经常需要采集一些精度要求较高的模拟信号&#xff0c;使用MCU集成的ADC难以达到要求、所以我们需要独立的ADC芯片。这一节我们就来设计并实现AD7705芯片的驱动、并探讨驱动的使用方法。 1、功能概述 AD7705/AD7706是用于低频测量的完整模拟前端。可以直接从传感器接收低…