Arduino PID库 (6):初始化
参考:手把手教你看懂并理解Arduino PID控制库——初始化
Arduino PID库 (5):开启或关闭 PID 控制的影响
问题
在上一节中,我们实现了关闭和打开PID的功能。我们关闭了它,但现在让我们看看当我们重新打开它时会发生什么:
哎呀!PID 跳回到它发送的最后一个输出值,然后从那里开始调整。这会导致我们不希望有的输入凸起。
上一节,讨论的是 PID 控制由开转关的过程中存在的问题,那么紧接着上一节,如果在关闭后,突然再次开启,那么会产生什么问题呢?直观来看,对于被控量会出现图中,绿色线的一个 bump,这个 bump 是由于由关转开的一瞬间,输出突然放大,那么对于灵敏度高的系统,则会出现,滞后大的系统可能不会如此明显,但不管怎么说,为了杜绝一切不利因素,我们都应该想办法消除这个 bump。由于这个 bump 的根因是由于输出的突变而造成的,所以需要想办法控制这个输出的突变。
解决方案
想一想,所有此类问题都是发生在时间轴上的,那么 PID 控制中时间轴会影响的项只有积分项和微分项(可以想象,比例项为 Kp * (设定值 - 被控量)
,这是一个连续量,不存在突变的可能(除非是采样时间特别长,在改变输出前,被控量飞上天了),所以只需要从这两项上想办法,控制住这两项的突变,即可控制住输出的突变。首先,问题发生在 PID 关闭转开启的过程中,由于 PID 开启关闭控制的函数是 SetMode,所以在此函数中,增加一个 initial
函数用于控制积分项和微分项即可。具体做法是:
1、更新微分项上一次采样值为 PID 开启一瞬间的采样值,这样可以保持微分项维持在上一次开启结束的状态不变。
2、将积分项设置为当前的输出,为什么要这么做呢?积分项由于 PID 关闭长时间维持关闭前的状态,一旦开启,如果不改变积分项,输出会瞬间被拉回到上一次开启结束的状态,突变就这样产生了
这个很容易修复。由于我们现在知道何时打开(从手动到自动),我们只需要初始化即可平稳过渡。这意味着massaging2个存储的工作变量(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
以防止导数出现峰值。比例项不依赖于过去的任何信息,因此不需要任何初始化。
结果
我们从上图中看到,正确的初始化会导致从手动到自动的无颠簸转移:这正是我们所追求的。
下一>>
更新:为什么不是 ITerm=0?
我最近收到很多问题,问为什么我不在初始化时设置 ITerm=0
。作为答案,我要求您考虑以下场景:pid 是手动的,用户已将输出设置为 50。一段时间后,该过程稳定到输入 75.2。用户设定值为 75.2 并打开 pid。应该怎么做?我认为切换到自动后,输出值应保持在 50。由于 P 和 D 项将为零,因此发生这种情况的唯一方法是将 ITerm 初始化为输出值。
如果您处于需要输出初始化为零的情况,则无需更改上面的代码。只需在调用例程中设置 Output=0
,然后将 PID 从手动转换为自动。
方向
问题
PID连接的过程分为两组:直接作用和反向作用。到目前为止,我展示的所有例子都是直接行动。也就是说,输出的增加会导致输入的增加。对于反向作用过程,情况正好相反。例如,在冰箱中,冷却的增加会导致温度下降。为了使初学者 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”变成了我目前知道如何制作的最强大的控制器。对于那些正在寻找PID库详细解释的读者,我希望你得到你想要的。对于那些编写自己的PID的人,我希望你们能够收集一些想法,从而节省一些周期。
最后两点:
- 如果本系列中的某些内容看起来有问题,请告诉我。我可能错过了一些东西,或者可能只需要在我的解释中更清楚。无论哪种方式,我都想知道。
- 这只是一个基本的PID。为了简单起见,我故意省略了许多其他问题。比如:前馈,重置回扣,整数数学,不同的pid形式,使用速度而不是位置。如果有兴趣让我探索这些主题,请告诉我。
example
- Arduino PID 库 - 亮度控制