Arduino PID库 (1)– 简介
- pid内容索引-CSDN博客
- pid术语及整定原则
- 参考:手把手教你看懂并理解Arduino PID控制库——引子)
- 库的改进QuickPID-sTune库
原文地址
随着新的Arduino PID库的发布,最后一个库虽然很可靠,但并没有真正附带任何代码解释。这一次的计划是详细解释为什么代码是这样的。我希望这对两种人有用:
- 详细解析Arduino PID库内部发生的事情。
- 任何编写自己的PID算法的人都可以看看我是如何做的。
这将是一个艰难的过程,但我想我找到了一种不太痛苦的方式来解释我的代码。我将从我所谓的“初学者的PID”开始。然后,我将逐步改进它,直到得到一个高效、健壮的 pid 算法。
初学者的PID
这是每个人第一次学习的PID方程:
O
u
t
p
u
t
=
K
p
e
(
t
)
+
K
I
∫
e
(
t
)
d
t
+
K
D
d
d
t
e
(
t
)
e
=
S
e
t
p
o
i
n
t
−
I
n
p
u
t
Output = K_pe(t)+K_I \int e(t) dt +K_D\frac{d}{dt}e(t) \\ \\ e=Setpoint - Input
Output=Kpe(t)+KI∫e(t)dt+KDdtde(t)e=Setpoint−Input
几乎每个人都编写以下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() 在需要进行PID控制量计算的时候被调用,在这样的代码支持下,PID控制可以工作得很好。但是,如果是一个性能较强的工业控制器,还需要考虑一下几个问题:
- sample time采样时间 - 改变采样时间会带来怎样的后果
- Derivative Kick - 突然改变设定值或者微分时间,如何避免过冲
- On-The-Fly Tuning Changes即时调谐更改 - 一个好的PID算法是可以在不干扰内部工作的情况下更改调谐参数的算法。
- Reset Windup Mitigation重置清盘缓解 - 我们将介绍什么是重置清盘,并实施具有副作用的解决方案
- on/off(Auto/Manual开/关(自动/手动)- 在大多数应用中,有时希望关闭PID控制器并手动调整输出,而不会干扰控制器
- Initialization初始化– 当控制器首次打开时,我们想要“无颠簸传输”。也就是说,我们不希望输出突然猛地跳动到某个新值。
- Controller Direction控制器方向 – 最后一个并不是健壮性名称本身的更改。 它旨在确保用户输入具有正确符号的优化参数。
- NEW: Proportional on Measurement新:测量比例 – 添加此功能可以更轻松地控制某些类型的进程
一旦我们解决了所有这些问题,我们将拥有一个可靠的PID算法。我们还将拥有在最新版本的Arduino PID库中使用的代码。因此,无论您是尝试编写自己的算法,还是尝试了解PID库中发生的事情,我都希望对您有所帮助。让我们开始吧。
库代码下载地址
更新:在所有代码示例中,我都使用双精度。在Arduino上,双精度与浮点数相同(单精度)。真正的双精度对于PID来说是矫枉过正的。如果您使用的语言确实是双精度,我建议将所有双精度更改为浮点数。
代码注释
PID_v1.h
#ifndef PID_v1_h
#define PID_v1_h
#define LIBRARY_VERSION 1.1.1
class PID
{
public:
//Constants used in some of the functions below
// 这里定义的两个变量分别指代两种工作模式:AUTOMATIC 对应 PID控制开启; MANUAL 对应PID控制关闭
#define AUTOMATIC 1
#define MANUAL 0
// 这里定义两个变量分别代表【控制量与被控量】方向:DIRECT 对应两者同向; REVERSE 对应两者反向
// 其中同向指: 如果控制量增大,那么被控量也会增大;反之亦然。
// 其中反向指: 如果控制量增大,那么被控量缺减小;反之亦然。
#define DIRECT 0
#define REVERSE 1
//commonly used functions **************************************************************************
//构造函数
PID(double*, double*, double*, // * constructor. links the PID to the Input, Output, and
double, double, double, int); // Setpoint. Initial tuning parameters are also set here
// 设置自动模式还是手动模式,两者区别目前还未清楚
void SetMode(int Mode); // * sets PID to either Manual (0) or Auto (non-0)
// 计算PID, 在每个计算周期都应当调用 ,计算频率和是否计算可以在setMode和SetSampleTime中指定
bool Compute(); // *执行 PID 计算,每次循环 loop() 时都应调用它。
//可以使用 SetMode SetSampleTime 分别设置开/关和计算频率。
//将输出限制在特定范围内。默认值为 0-255,但用户可能会根据应用需要进行更改
void SetOutputLimits(double, double); //clamps the output to a specific range. 0-255 by default, but
//it's likely the user will want to change this depending on
//the application
//available but not commonly used functions ********************************************************
// 设定P、I、D参数,可以在运行的时间周期内,指定运行需要的参数
void SetTunings(double, double, double); // * While most users will set the tunings once in the
// constructor, this function gives the user the option
// of changing tunings during runtime for Adaptive control
// 设定控制器的方向,限制输出的正反向,仅需要在开始的时候设置一次
void SetControllerDirection(int); // * Sets the Direction, or "Action" of the controller. DIRECT
// means the output will increase when error is positive. REVERSE
// means the opposite. it's very unlikely that this will be needed
// once it is set in the constructor.
// 采样周期,以毫秒作为设置单位,默认为100
void SetSampleTime(int); // * sets the frequency, in Milliseconds, with which
// the PID calculation is performed. default is 100
//Display functions ****************************************************************
// 获取PID运行参数
double GetKp(); // These functions query the pid for interal values.
double GetKi(); // they were created mainly for the pid front-end,
double GetKd(); // where it's important to know what is actually
// 获取运行模式
int GetMode(); // inside the PID.
//获取PID 方向
int GetDirection(); //
private:
// 此函数初始化,完成从手动模式到自动模式的无缝切换
void Initialize();
// 保留用户输入的参数格式,以便显示
double dispKp; // * we'll hold on to the tuning parameters in user-entered
double dispKi; // format for display purposes
double dispKd; //
double kp; // * (P)roportional Tuning Parameter
double ki; // * (I)ntegral Tuning Parameter
double kd; // * (D)erivative Tuning Parameter
int controllerDirection;
// 其中包含了INput、 OUTput以及setPoint
double *myInput; // * Pointers to the Input, Output, and Setpoint variables
double *myOutput; // This creates a hard link between the variables and the
double *mySetpoint; // PID, freeing the user from having to constantly tell us
// what these values are. with pointers we'll just know.
// 此3个参数需要参考CPP才知道
unsigned long lastTime;
double ITerm, lastInput;
unsigned long SampleTime;
double outMin, outMax;
// 是否自动参数的标志
bool inAuto;
};
#endif
PID_v1.cpp
/**********************************************************************************************
* Arduino PID Library - Version 1.1.1
* by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com
* This Library is licensed under a GPLv3 License
**********************************************************************************************/
#include "PID_v1.h"
/*Constructor (...)*********************************************************
* The parameters specified here are those for for which we can't set up
* reliable defaults, so we need to have the user set them.
* 这里指定的参数是我们无法设置可靠的默认值参数,因此需要由用户来设置。
***************************************************************************/
PID::PID(double* Input, double* Output, double* Setpoint,
double Kp, double Ki, double Kd, int ControllerDirection)
{
// 赋值控制量、被控量及设定值初始地址,注意这里是地址
myOutput = Output;
myInput = Input;
mySetpoint = Setpoint;
// 初始化auto模式为false
inAuto = false;
// 默认控制量限制在0到255,此函数可以根据实际系统需要修改控制量输出限制范围
PID::SetOutputLimits(0, 255); //default output limit corresponds to
//the arduino pwm limits
// 默认采样周期为100ms,同样可以根据需求修改
SampleTime = 100; //default Controller Sample Time is 0.1 seconds
// 设置输出的方向
PID::SetControllerDirection(ControllerDirection);
// 设置PID 控制参数
PID::SetTunings(Kp, Ki, Kd);
// 用于存储PID构造时,对应的系统运行时间
// millis()作用是获取当前系统运行时间(单位ms),此函数针对arduino;移植到别的系统,可以其他类似作用函数替代
// 这里减去SampleTime是为了保证在构造后能马上进行PID控制,而不需要等待到下一个SampleTime周期
lastTime = millis()-SampleTime;
}
/* Compute() **********************************************************************
* This, as they say, is where the magic happens. this function should be called
* every time "void loop()" executes. the function will decide for itself whether a new
* pid Output needs to be computed. returns true when the output is computed,
* false when nothing has been done.
* 此函数用于PID控制量计算,函数可以频繁的在进程中被调用。
**********************************************************************************/
bool PID::Compute()
{
// 如果没有开启PID返回失败,退出;控制量不变,仍为上一次控制量
if(!inAuto) return false;
// 获取当前系统运行时间并求出相对上一次计算时间间隔
unsigned long now = millis();
unsigned long timeChange = (now - lastTime);
// 如果时间间隔大于或者等于采样时间,那么则计算,否则不满足采样条件,计算失败,退出;
if(timeChange>=SampleTime)
{
/*Compute all the working error variables*/
// 保存当前被控量,如果是一个实时控制系统,此时被控量可能与构造时的被控量不一致
double input = *myInput;
// 求出设定值与当前被控量之间的偏差
double error = *mySetpoint - input;
// 计算积分项 此处积分项和标准PID控制方程略微有差距
ITerm+= (ki * error);
// 如果 积分项超过最大限制,那么设置积分项为最大限制;同样,最小限制也做同样处理
// 此处为何这么做一句两句说不清楚,主要是为了PID 控制量长时间超限后,突然降低设定值,能够让系统马上反应而不会产生一个时间滞后。
if(ITerm > outMax) ITerm= outMax;
else if(ITerm < outMin) ITerm= outMin;
// 求出两个被控量之间偏差,也就是在计算周期(这里不用采用周期是因为计算周期可能会超过采样周期)被控量的变化。
// 总的来说是为了防止控制量和被控量突变
double dInput = (input - lastInput);
/*Compute PID Output*/
// PID 调节算式,这就不需要说明了
double output = kp * error + ITerm- kd * dInput;
// 这里做限制和ITerm做限制的作用是一样的。。
if(output > outMax) output = outMax;
else if(output < outMin) output = outMin;
*myOutput = output;
/*Remember some variables for next time*/
lastInput = input;
lastTime = now;
return true;
}
else return false;
}
/* SetTunings(...)*************************************************************
* This function allows the controller's dynamic performance to be adjusted.
* it's called automatically from the constructor, but tunings can also
* be adjusted on the fly during normal operation
* 此函数用于设定PID调节参数
******************************************************************************/
void PID::SetTunings(double Kp, double Ki, double Kd)
{
// 如果PID参数中有小于0的参数,那么设定失败,直接退出,仍然沿用原来的参数
if (Kp<0 || Ki<0 || Kd<0) return;
// 仅做显示用。
dispKp = Kp; dispKi = Ki; dispKd = Kd;
// 获取采样时间,由ms转为s
double SampleTimeInSec = ((double)SampleTime)/1000;
// 调整PID参数, I 和 D 参数的调节主要是为了满足采样周期改变导致的影响,
// 主要是 积分项和 微分项是和时间有关的参数,所以采样周期改变会导致这两项需要重新计算,
// 这里为了减少这些工作,通过采样周期变换,转换ID参数变化
// 至于为什么可以这么做,是因为前面做了特殊处理,修改了PID标准表达式,使每一次计算对历史依赖较小
kp = Kp;
ki = Ki * SampleTimeInSec;
kd = Kd / SampleTimeInSec;
// 设定PID调节方向
if(controllerDirection ==REVERSE)
{
kp = (0 - kp);
ki = (0 - ki);
kd = (0 - kd);
}
}
/* SetSampleTime(...) *********************************************************
* sets the period, in Milliseconds, at which the calculation is performed
******************************************************************************/
//更新新的采样时间,同时按照比例更新ID参数
void PID::SetSampleTime(int NewSampleTime)
{
if (NewSampleTime > 0)
{
double ratio = (double)NewSampleTime
/ (double)SampleTime;
ki *= ratio;
kd /= ratio;
SampleTime = (unsigned long)NewSampleTime;
}
}
/* SetOutputLimits(...)****************************************************
* This function will be used far more often than SetInputLimits. while
* the input to the controller will generally be in the 0-1023 range (which is
* the default already,) the output will be a little different. maybe they'll
* be doing a time window and will need 0-8000 or something. or maybe they'll
* want to clamp it from 0-125. who knows. at any rate, that can all be done
* here.
* 此函数容易产生控制量的突变,在运行过程中,尽量不要缩小范围。
* 这个函数的使用频率要远远高于 SetInputLimits。
* 虽然控制器的输入一般在 0-1023 范围内(这已经是默认值了),
* 但输出会有些不同。
**************************************************************************/
void PID::SetOutputLimits(double Min, double Max)
{
// 赋值限制
if(Min >= Max) return;
outMin = Min;
outMax = Max;
if(inAuto)
{
if(*myOutput > outMax) *myOutput = outMax;
else if(*myOutput < outMin) *myOutput = outMin;
if(ITerm > outMax) ITerm= outMax;
else if(ITerm < outMin) ITerm= outMin;
}
}
/* SetMode(...)****************************************************************
* Allows the controller Mode to be set to manual (0) or Automatic (non-zero)
* when the transition from manual to auto occurs, the controller is
* automatically initialized
* 允许将控制器模式设置为手动(0)或自动(非 0),当从手动转换为自动时,控制器将自动初始化
******************************************************************************/
void PID::SetMode(int Mode)
{
bool newAuto = (Mode == AUTOMATIC);
// 如果模式不一样,那么则重新初始化
if(newAuto == !inAuto)
{ /*we just went from manual to auto*/
PID::Initialize();
}
inAuto = newAuto;
}
/* Initialize()****************************************************************
* does all the things that need to happen to ensure a bumpless transfer
* from manual to automatic mode.
* 完成从手动模式到自动模式的无缝切换
******************************************************************************/
void PID::Initialize()
{
ITerm = *myOutput;
lastInput = *myInput;
if(ITerm > outMax) ITerm = outMax;
else if(ITerm < outMin) ITerm = outMin;
}
/* SetControllerDirection(...)*************************************************
* The PID will either be connected to a DIRECT acting process (+Output leads
* to +Input) or a REVERSE acting process(+Output leads to -Input.) we need to
* know which one, because otherwise we may increase the output when we should
* be decreasing. This is called from the constructor.
* PID 要么正向作用过程(+输出导致+输入),
* 要么连接到反向作用过程(+输出导致-输入),我们需要知道是哪一个,
* 否则可能会在本应减少输出的情况下增加输出。
* 在构造函数中调用此函数。
******************************************************************************/
void PID::SetControllerDirection(int Direction)
{
if(inAuto && Direction !=controllerDirection)
{
kp = (0 - kp);
ki = (0 - ki);
kd = (0 - kd);
}
controllerDirection = Direction;
}
/* Status Funcions*************************************************************
* Just because you set the Kp=-1 doesn't mean it actually happened. these
* functions query the internal state of the PID. they're here for display
* purposes. this are the functions the PID Front-end uses for example
******************************************************************************/
double PID::GetKp(){ return dispKp; }
double PID::GetKi(){ return dispKi;}
double PID::GetKd(){ return dispKd;}
int PID::GetMode(){ return inAuto ? AUTOMATIC : MANUAL;}
int PID::GetDirection(){ return controllerDirection;}
采样时间
参考:手把手教你看懂并理解Arduino PID控制库——采样时间
(这是关于编写固定 PID 算法的更大系列)
问题所在
一般来说,PID 控制都是周期性调用(也就是意味着,每次计算的间隔都是固定的常量),但或多或少,由于各种需求会被奇葩的非周期调用。如果非得修改采样时间,对 PID 控制进行非周期调用,那么这样会导致以下问题:
观察这个 “可恶” 的方程:
O
u
t
p
u
t
=
k
p
e
(
t
)
+
K
I
∫
e
(
t
)
d
t
+
K
D
d
d
t
e
(
t
)
e
=
S
e
t
p
o
i
n
t
−
I
n
p
u
t
Output = k_pe(t)+K_I \int e(t) dt +K_D\frac{d}{dt}e(t) \\ \\ e=Setpoint - Input
Output=kpe(t)+KI∫e(t)dt+KDdtde(t)e=Setpoint−Input
如果采样时间变化了,那么对于积分项和微分项(也就是 KI 和 KD 对应的项),这两项是和采样时间间隔有关的,那么则需要进行额外的微积分运算 (不能再按照原来写好的代码进行运算,必须对时间参数进行调整)。
初学者的PID被设计为不规则地调用。这会导致 2 个问题:
- 您不会从 PID 获得一致的行为,因为有时它经常被调用,有时则不然。
- 你需要做额外的计算导数和积分,因为它们都依赖于时间的变化。
解决方案
确保定期调用 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(第 29 和30 行),结果在数学上是等价的,但效率更高。
不过,这样做有点问题。如果用户决定在操作过程中更改采样时间,则需要重新调整Ki和Kd以反映此新更改。这就是第 35-39 行的全部内容。
另请注意,我将第 27 行的采样时间转换为秒。严格来说,这不是必需的,但允许用户以 1/秒
为单位输入,而不是 1/mS
。
观察 SetTunings 和 SetSampleTime
两个函数,这两个函数完成了适应采样间隔改变功能。其中 SetSampleTime 在采样间隔改变后,按照比例放大 / 缩小了与采样间隔改变相同的倍数。在 SetTunings 做归一化处理。为什么这里只对 Ki 和 Kd 进行处理在前面已经说过了。那么大家可能又会存在这样的疑问:
如果 Ki 变化了,那么和经典的 PID 控制公式结果不是会差很大吗?答案是差别不大!!
观察积分项,并改写为离散形式:
如果在调节过程中,Ki 不是一个常量的话,那么可以进一步改写为:
上式的第一项分别观察 Ki 及 e (t),改变采样间隔后,e (t) 受采样间隔变化影响,产生对应时间内的变化,而 ki 等比例反向放大 / 缩小,效果相当。
结果
上述更改为我们做了 3 件事
- 无论调用
Compute()
的频率如何,PID 算法都将定期评估 [第 11 行] - 由于时间减法 [第 10 行],当 millis() 返回 0 时不会有问题。这每 55 天才会发生一次,但我们要防止还记得吗?
- 我们不再需要乘以时间变化。由于它是一个常量,我们可以将其从计算代码 [第 15+16 行] 中移出,并将其与调参常量 [第 31+32 行] 混为一谈。从数学上讲,它的工作原理相同,但是每次计算PID时都会节省乘法和除法
关于中断的旁注
如果这个PID进入微控制器,则可以为使用中断提出一个很好的论据。SetSampleTime 设置中断频率,然后在需要时调用 Compute。在这种情况下,不需要第 9-12、23 和 24 行。如果您打算用PID影响来做到这一点,那就去做吧!不过,请继续阅读本系列。希望您仍然可以从随后的修改中获得一些好处。
我没有使用中断有三个原因
- 就本系列而言,并不是每个人都能使用中断。
- 如果您希望它同时实现许多PID控制器,事情会变得棘手。
example
- Arduino PID 库 - 亮度控制