Go Down

Topic: Arduino PID Library (Read 62486 times) previous topic - next topic

br3ttb

Quote
By the way, why on earth is the I parameter equal to the reciprocal of the gain as it appears in the PID algorithm?


sorry for the confusion.  there's various forms of the pid equation.  (http://en.wikipedia.org/wiki/PID_controller scroll down to "Alternative nomenclature and PID forms")

they're mathematically equivalent, but the one listed as the "standard form" is the one i chose.  it's a little more work on the backend as you mentioned, but I find it easier to tune.  if you know the time constant of the process, you just make that the I term, then adjust P to get more aggressive or conservative)

pwillard

Brett, ZeroVector ... You  guys rock!

ZV, The Timer Library you used was the answer I'd been searching for.   I had no clue about it and it allowed me to have much better control over my Kiln temperature with correct PWM output settings.

I use an SSR to control a 2000 Degree F, 20 Amp Kiln.  I have now been able to reach my goal of 1290F and was hovering between +/- 6 degrees with minimal overshoot.  I was thrilled!

defygravity

Thanks!that is i 'm looking for.

laxman

I'll remove the base tags and leave the Playgound strings. :( ::)

br3ttb


Alicemirror

Hi, sorry for this out-of-topic post, but I have no idea of the person I need to contact.  Do you have any idea or contact to post news topics on this area? I have new products recently developed, for Arduino board that I think are interesting for the community. What is the way?

Thank you in advance for your help. Enrico Miglino
conTESTI.eu electronics
http://www.contesti.eu/projects

Tweaked

Brett,
If you're still considering an integer version, here's a technique I've found worthwhile.  Convert everything to long variable type, then assume 12 decimal places.  I use the following:

#define floatToFix(a) ((long)((a)*4096.0)) // mult by 2^12 to use floats in fixed point math
#define fixToFloat(a) ((float)((a)/4096.0)) // div by 2^12 to convert back to float
#define multFix(a,b) ((long)((((long)(a))*((long)(b)))>>12))

for analog reads,

 a1 = analogRead(pin);
 aIn1 = (long)a1 << 12; // add 12 decimal places

this approach speeds up the math without losing precision or accuracy.

hth,
Billy

br3ttb

Quote
#define multFix(a,b) ((long)((((long)(a))*((long)(b)))>>12))


one of the main issues I've been wrestling with on the integer library is ensuring that overflow doesn't happen.  I haven't tried this, but if a and b were big enough wouldn't a*b overflow?

Tweaked

Brett,
If this overflows, you could reduce the number of bit-shifts or use 'long long' int64_t data type which should not overflow.  I'm fumbling my way through modifying the library, and will email it to you for performance baseline testing for speed and accuracy.

Billy

Tweaked

Brett,
The integer version of the library is functional, with one caveat... the output increments from 0 to 127 then increments from -128 to -1.  It's like the 8th bit is being used for sign.  I've renamed the library to PID_Beta6b in order to maintain the integrity of the original library and to make it easy to switch back and forth.  Here's the first half of the code:

Code: [Select]

#include <wiring.h>
#include <PID_Beta6b.h>


/* Standard Constructor (...)***********************************************
*    constructor used by most users.  the parameters specified 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 Kc, double TauI, double TauD)
{

 PID::ConstructorCommon(Input, Output, Setpoint, Kc, TauI, TauD);  
 UsingFeedForward = false;
 PID::Reset();

 
}

/* Overloaded Constructor(...)**********************************************
*    This one is for more advanced users.  it's essentially the same as the
* standard constructor, with one addition.  you can link to a Feed Forward bias,
* which lets you implement... um.. Feed Forward Control.  good stuff.
***************************************************************************/
PID::PID(double *Input, double *Output, double *Setpoint, double *FFBias, double Kc, double TauI, double TauD)
{

 PID::ConstructorCommon(Input, Output, Setpoint, Kc, TauI, TauD);  
 UsingFeedForward = true;                        //tell the controller that we'll be using an external
 long myBias = (*FFBias * 4096.0);                  //bias, and where to find it
 PID::Reset();

}

/* ConstructorCommon(...)****************************************************
*    Most of what is done in the two constructors is the same.  that code
* was put here for ease of maintenance and (minor) reduction of library size
****************************************************************************/
void PID::ConstructorCommon(double *Input, double *Output, double *Setpoint, double Kc, double TauI, double TauD)
{
 PID::SetInputLimits(0, 1023);                        //default the limits to the
 PID::SetOutputLimits(0, 255);                        //full ranges of the I/O

 tSample = 1000;                              //default Controller Sample Time is 1 second

 PID::SetTunings(Kc, TauI, TauD);

 nextCompTime = millis();
 inAuto = false;
 myOutput = Output;
 myInput = Input;
 mySetpoint = Setpoint;
}


/* SetInputLimits(...)*****************************************************
*      I don't see this function being called all that much (other than from the
*  constructor.)  it needs to be here so we can tell the controller what it's
*  input limits are, and in most cases the 0-1023 default should be fine.  if
*  there's an application where the signal being fed to the controller is
*  outside that range, well, then this function's here for you.
**************************************************************************/
void PID::SetInputLimits(double INMin, double INMax)
{
     //after verifying that mins are smaller than maxes, set the values
     if(INMin >= INMax) return;

     //rescale the working variables to reflect the changes
     lastInput = multFix(lastInput,divFix(floatToFix(INMax - INMin),inSpan));
     accError = multFix(accError,divFix(floatToFix(INMax - INMin),inSpan));

     //make sure the working variables are
     //within the new limits
     if (lastInput > 1<<12) lastInput = 1<<12;
     else if (lastInput < 0L) lastInput = 0L;
     
     
           inMin = floatToFix(INMin);
     inSpan = floatToFix(INMax - INMin);
}

/* 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 OUTMin, double OUTMax)
{
     //after verifying that mins are smaller than maxes, set the values
     if(OUTMin >= OUTMax) return;

     //rescale the working variables to reflect the changes
     lastOutput = multFix(lastOutput,divFix(floatToFix(OUTMax - OUTMin),outSpan));

     //make sure the working variables are
     //within the new limits
     if (lastOutput > 1<<12) lastOutput = 1<<12;
     else if (lastOutput < 0L) lastOutput = 0L;

     outMin = floatToFix(OUTMin);
     outSpan = floatToFix(OUTMax - OUTMin);
}

/* 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
******************************************************************************/
void PID::SetTunings(double Kc, double TauI, double TauD)
{
     //verify that the tunings make sense
     if (Kc == 0.0 || TauI < 0.0 || TauD < 0.0) return;

     //we're going to do some funky things to the input numbers so all
     //our math works out, but we want to store the numbers intact
     //so we can return them to the user when asked.
     P_Param = Kc;
     I_Param = TauI;
     D_Param = TauD;
     
     //convert Reset Time into Reset Rate, and compensate for Calculation frequency
     double tSampleInSec = ((double)tSample / 1000.0);
     double tempTauR;
     if (TauI == 0)
           tempTauR = 0;
     else
           tempTauR = (1.0 / TauI) * tSampleInSec;  

     if (inAuto)
     {      //if we're in auto, and we just change the tunings, the integral term
           //will become very, very, confused (trust me.) to achieve "bumpless
           // transfer" we need to rescale the accumulated error.
           if(tempTauR != 0.0) //(avoid divide by 0)
                 accError = multFix(accError,divFix(multFix(kc,taur),floatToFix(Kc * tempTauR)));
           else
                 accError = 0L;
     }
     
     kc = floatToFix(Kc);
     taur = floatToFix(tempTauR);
     taud = floatToFix(TauD / tSampleInSec);
}

/* Reset()*********************************************************************
*      does all the things that need to happen to ensure a bumpless transfer
*  from manual to automatic mode.  this shouldn't have to be called from the
*  outside. In practice though, it is sometimes helpful to start from scratch,
*  so it was made publicly available
******************************************************************************/
void PID::Reset()
{

     if(UsingFeedForward)
       bias = divFix((*myBias - outMin),outSpan);
     else
       bias = divFix((floatToFix(*myOutput) - outMin),outSpan);
     
       lastOutput = (long)bias;
     lastInput = divFix((floatToFix(*myInput) - inMin),inSpan);

     // - clear any error in the integral
     accError = 0L;

}

/* 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)
{
     if (Mode!=0 && !inAuto)
     {      //we were in manual, and we just got set to auto.
           //reset the controller internals
           PID::Reset();
     }
     inAuto = (Mode!=0);
}

/* SetSampleTime(...)*******************************************************
* sets the frequency, in Milliseconds, with which the PID calculation is performed      
******************************************************************************/
void PID::SetSampleTime(int NewSampleTime)
{
     if (NewSampleTime > 0)
     {
           //convert the time-based tunings to reflect this change
           taur = multFix(taur,floatToFix((double)NewSampleTime/(double)tSample));
           accError = multFix(accError,floatToFix((double)tSample/(double)NewSampleTime));
           taud = multFix(taud,floatToFix((double)NewSampleTime/(double)tSample));
           tSample = (unsigned long)NewSampleTime;
     }
}

Tweaked

And here's the last portion (wouldn't all fit in one post):
Code: [Select]
/* 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
*
*  Some notes for people familiar with the nuts and bolts of PID control:
*  - I used the Ideal form of the PID equation.  mainly because I like IMC
*    tunings.  lock in the I and D, and then just vary P to get more
*    aggressive or conservative
*
*  - While this controller presented to the outside world as being a Reset Time
*    controller, when the user enters their tunings the I term is converted to
*    Reset Rate.  I did this merely to avoid the div0 error when the user wants
*    to turn Integral action off.
*    
*  - Derivative on Measurement is being used instead of Derivative on Error.  The
*    performance is identical, with one notable exception.  DonE causes a kick in
*    the controller output whenever there's a setpoint change. DonM does not.
*
*  If none of the above made sense to you, and you would like it to, go to:
*  http://www.controlguru.com .  Dr. Cooper was my controls professor, and is
*  gifted at concisely and clearly explaining PID control
*********************************************************************************/
void PID::Compute()
{
     justCalced=false;
     if (!inAuto) return; //if we're in manual just leave;

     unsigned long now = millis();

     //millis() wraps around to 0 at some point.  depending on the version of the
     //Arduino Program you are using, it could be in 9 hours or 50 days.
     //this is not currently addressed by this algorithm.

     //here's an attempt to address the millis() wrap issue:

     if (now < tSample) nextCompTime = tSample;
     
                                                     
     //...Perform PID Computations if it's time...
     if (now>=nextCompTime)                                          
     {
           
           //pull in the input and setpoint, and scale them into percent span
           long scaledInput = divFix((floatToFix(*myInput) - inMin),inSpan);
           if (scaledInput>(1<<12)) scaledInput = 1<<12;
           else if (scaledInput<0L) scaledInput = 0L;

           long scaledSP = divFix((floatToFix(*mySetpoint) - inMin),inSpan);
           if (scaledSP>(1<<12)) scaledSP = 1<<12;
           else if (scaledSP<0L) scaledSP = 0L;
           
           //compute the error
           long err = scaledSP - scaledInput;
           
           // check and see if the output is pegged at a limit and only
           // integrate if it is not. (this is to prevent reset-windup)
           if (!(lastOutput >= 1<<12 && err>0L) && !(lastOutput <= 0L && err<0L))
           {
                 accError = accError + err;
           }                                                

           // compute the current slope of the input signal
           long dMeas = (scaledInput - lastInput);      // we'll assume that dTime (the denominator) is 1 second.
                                         // if it isn't, the taud term will have been adjusted
                                         // in "SetTunings" to compensate

           //if we're using an external bias (i.e. the user used the
           //overloaded constructor,) then pull that in now
           if(UsingFeedForward)
           {
                 bias = divFix((*myBias - outMin),outSpan);
           }


           // perform the PID calculation.  
           long output = bias + multFix(kc,(err + multFix(taur,accError) - multFix(taud,dMeas)));

           //make sure the computed output is within output constraints
           if (output < 0L) output = 0L;
           else if (output > 1<<12) output = 1<<12;

           lastOutput = output;            // remember this output for the windup
                                   // check next time            
           lastInput = scaledInput;      // remember the Input for the derivative
                                   // calculation next time

           //scale the output from percent span back out to a real world number
               *myOutput = fixToFloat(multFix(output,outSpan) + outMin);

           nextCompTime += tSample;                        // determine the next time the computation
           if(nextCompTime < now) nextCompTime = now + tSample;      // should be performed      

           justCalced=true;  //set the flag that will tell the outside world that the output was just computed
     }                                                


}


/*****************************************************************************
* STATUS SECTION
* These functions allow the outside world to query the status of the PID
*****************************************************************************/

bool PID::JustCalculated()
{
     return justCalced;
}
int PID::GetMode()
{
     if(inAuto)return 1;
     else return 0;
}

double PID::GetINMin()
{
     return fixToFloat(inMin);
}
double PID::GetINMax()
{
     return fixToFloat(inMin + inSpan);
}
double PID::GetOUTMin()
{
     return fixToFloat(outMin);
}
double PID::GetOUTMax()
{
     return fixToFloat(outMin+outSpan);
}
int PID::GetSampleTime()
{
     return tSample;
}
double PID::GetP_Param()
{
     return P_Param;
}
double PID::GetI_Param()
{
     return I_Param;
}

double PID::GetD_Param()
{
     return D_Param;
}


ps. I also made a feeble stab at millis() rollover protection, not sure if it'll work...

Tweaked

And lastly, the modified header file:
Code: [Select]
#ifndef PID_Beta6b_h
#define PID_Beta6b_h

#define floatToFix(a) ((long)((a)*4096.0))                   // mult by 2^12 to use floats in fixed point math
#define fixToFloat(a) ((double)((a)/4096.0))                   // div by 2^12 to convert back to float
#define multFix(a,b) ((long)((((long)(a))*((long)(b)))>>12))    // bit-shift right 12 to maintain 12 assumed decimal places
#define divFix(a,b) ((long)(((long)(a)<<12)/((long)(b))))        // make Numerator 8:24 int before dividing

class PID
{


 public:

 #define AUTO      1
 #define MANUAL      0
 #define LIBRARY_VERSION      0.6b

 //commonly used functions **************************************************************************
   PID(double*, double*, double*,        // * constructor.  links the PID to the Input, Output, and
       double, double, double);          //   Setpoint.  Initial tuning parameters are also set here

   PID(double*, double*, double*,        // * Overloaded Constructor.  if the user wants to implement
       double*, double, double, double); //   feed-forward

   void SetMode(int Mode);               // * sets PID to either Manual (0) or Auto (non-0)

   void 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

   void SetInputLimits(double, double);  //Tells the PID what 0-100% are for the Input

   void SetOutputLimits(double, double); //Tells the PID what 0-100% are for the Output


 //available but not commonly used functions ********************************************************
   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 SetSampleTime(int);              // * sets the frequency, in Milliseconds, with which
                                         //   the PID calculation is performed.  default is 1000

   void Reset();                         // * reinitializes controller internals.  automatically
                                         //   called on a manual to auto transition

   bool JustCalculated();                // * in certain situations, it helps to know when the PID has
                                         //   computed this bit will be true for one cycle after the
                                         //   pid calculation has occurred
   

  //Status functions allow you to query current PID constants ***************************************
   int GetMode();
   double GetINMin();
   double GetINMax();
   double GetOUTMin();
   double GetOUTMax();
   int GetSampleTime();
   double GetP_Param();
   double GetI_Param();
   double GetD_Param();


 private:

   void ConstructorCommon(double*, double*, double*,           // * code that is shared by the constructors
       double, double, double);

  //scaled, tweaked parameters we'll actually be using
   long kc;                      // * (P)roportional Tuning Parameter
   long taur;                    // * (I)ntegral Tuning Parameter
   long taud;                    // * (D)erivative Tuning Parameter

  //nice, pretty parameters we'll give back to the user if they ask what the tunings are
   double P_Param;
   double I_Param;
   double D_Param;


   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.

   long *myBias;                 // * Pointer to the External FeedForward bias, only used
                                 //   if the advanced constructor is used
   bool UsingFeedForward;        // * internal flag that tells us if we're using FeedForward or not

   unsigned long nextCompTime;   // * Helps us figure out when the PID Calculation needs to
                                 //   be performed next
                                 //   to determine when to compute next
   unsigned long tSample;        // * the frequency, in milliseconds, with which we want the
                                 //   the PID calculation to occur.
   bool inAuto;                  // * Flag letting us know if we are in Automatic or not

   long lastOutput;              // * remembering the last output is used to prevent
                                 //   reset windup.
   long lastInput;               // * we need to remember the last Input Value so we can compute
                                 //   the derivative required for the D term
   long accError;                // * the (I)ntegral term is based on the sum of error over
                                 //   time.  this variable keeps track of that
   long bias;                    // * the base output from which the PID operates
   

   
   long inMin, inSpan;           // * input and output limits, and spans.  used convert
   long outMin, outSpan;         //   real world numbers into percent span, with which
                                 //   the PID algorithm is more comfortable.

   bool justCalced;              // * flag gets set for one cycle after the pid calculates
};
#endif


juliandasilva

Yesterday, we uploaded two post by mistake in the "Processing Front-End for the PID Library" thread. Those posts should be here:

http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1243714052/36#36
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1243714052/37#37

As explained there, we made minor modifications to the PID, reducing a bit the flash footprint, and adding some small improvements for the compiler (such as const and inlines), and eliminating one of the constructors and the ConstructorCommon member by adding a default value to a param.

Regards,
Julián
http://robotgroup.com.ar

Faik

#103
Nov 01, 2010, 02:12 pm Last Edit: Nov 01, 2010, 03:35 pm by kaya13 Reason: 1
     Hi guys, and Brett. First of all thank you for your huge contributions on PID library, it was just the thing needed. Im kind of new to Arduino and PID's too. I have designed PID compensators in control fundamentals class but newer used it as a code on a real system :)
    I have a little problem using PID lib. Its actually very user friendly but im trying to control my quadrotor using pid lib.  I use pitch, roll angle values from Ardu Imu V2 as an inputs of PID, trying to get output values as esc pwm's. I dont have problems reading from Imu but i dont get output changes. IMU values change like 300 times but output changes 1 or 2 times, or  never change sometimes. I have changed the sampiling period of PID a few times but i couldnt find the solution any way. Any suggestions on that ? Thanks...



Update :
 I think i found my mistake, sorry about that. I wasnt careful about PID input variables. I use integer for my pitch, roll, yaw values which is ok for me. My ( int ) pitch doesnt match PID ( double ) Input so PID.Compute calculate just once or a few times.

So, now the question is do we have stable working int version of PID lib ? Or i guess i need a conversion. Thanks...

GlennD

#104
Nov 06, 2010, 03:43 pm Last Edit: Nov 06, 2010, 03:47 pm by gadman58 Reason: 1
I would also like to say many thanks to Brett and all those who have commented and gave suggestion in this forum.
I am new as well just got my first Arduino UNO 3 weeks ago.

My wife bought a used manual kiln for doing fused glass and I was able to convert it to a controller base one, using your PID library.
I ended up using this formula to get the setpoint to work for both going up in temperature and down, for the annealing phase.

Code: [Select]
Setpoint = (((double)(millis()-RampStartTime))/(m_AmntTime*3600000)*m_TempDif)+m_StartTemp;


Thanks again
Glenn
"Some cause happiness wherever they go;
others, whenever they go.." - Oscar Wilde

Go Up