本文主要依托于Brett Beauregard大神针对Arduino平台撰写的PID控制库Arduino PID Library及其对应的帮助博客Improving the Beginner’s PID。在没有Brett Beauregard帮助以前,也尝试过按照PID控制基本理论写过PID控制程序,并成功应用于工业设备中,但从未深刻考虑过将其写成适合工业控制的通用库。根据Brett Beauregard的理念,此PID库主要想为如下两类人服务:git
本文在上述基础上,主要有如下几方面工做:算法
接触过PID控制的工程师应当都会对下面的公式印象深入:app
上述公式的具体说明就不加以说明了,请各位参考维基百科的PID controller。大部分同志可能会写出以下代码(或者相似),包括我本身less
/*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控制能够工做得很好。可是,若是是一个性能较强的工业控制器,还须要考虑一下几个问题:ide
若是上述几个问题没有太多的理解,不要紧,先看一下PID库中代码是如何写的(若是仅想看上述7个问题的解决方案请跳过下一章节)。函数
头文件oop
#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(); // * performs the PID calculation. it should be // called every time loop() cycles. ON/OFF and // calculation frequency can be set using SetMode // SetSampleTime respectively //指定输出的范围,其中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, // * While most users will set the tunings once in the double); // 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. // 采样周期,以毫秒做为设置单位,默认为10 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: // 此函数初始化,还不知什么用,须要参考CPP 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
源文件性能
/********************************************************************************************** * 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 参数的调节主要是为了知足采样周期改变带致使的影响, // 主要是 积分项和 微分项是和时间有关的参数,因此采样周期改变会致使这两项须要从新计算,这里为了减小这些工做,将采样周期变换转换我I D参数变化 // 至于为何能够这么作,是由于前面作了特殊处理,修改了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. * 此函数容易产生控制量的突变,在运行过程当中,尽可能不要缩小范围 **************************************************************************/ 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 ******************************************************************************/ 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. ******************************************************************************/ 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;}
(这里代码过长,提供下载地址)。ui
上述代码提供对PID库的必要注释,其中有些注释没法一两句话就能说清,特别是针对上述7个问题的解决方案,具体的代码分析,请参考下一章节。this
若有不足之处请告知,^.^
下一章节将分析采样时间变化对PID控制的影响
PS:转载请注明出处:欧阳天华