Society of Robots - Robot Forum

Software => Software => Topic started by: obiwanjacobi on May 12, 2013, 01:44:47 AM

Title: My first little PID: please comment
Post by: obiwanjacobi on May 12, 2013, 01:44:47 AM
Hi,

I just finished my first PID code I wrote from scratch (from the theory) in order to fully understand it. Because I am also a noob in math, I thought I would be fun to let you guys comment on how I am doing.

I did study some other implementations of PID algorithms and each had small (and not so small) variation in how the math was done. That's why I decided to give my interpretation.

This class is part of my Arduino Template Library (http://atl.codeplex.com/). It is written specifically to cover one responsibility only and work with the other (template) classes I have in the library. So for instance, the PID class does not do time tracking, I have other classes in the library for that. The user of the class has to typedef its own class hierarchies for the situation the classes are used in.

Code: [Select]
/*
BaseT is used as a base class and implements:
T getFeedback()
unsigned int getDeltaTime()
T getSmallestAcceptableError()

T is the data type that hold the values. Either float or double.

Algorithm:
Error = SetPoint - Feedback
P = Error * gainP
I = Sum(previous-I's) + ((Error * deltaTime) * gainI)
D = ((previous-Error - Error) / deltaTime) * gainD

PI = P + I
PD = P + D
PID = P + I + D
*/
template<class BaseT, typename T>
class PID : public BaseT
{
public:
T P(T setPoint, T gainP)
{
T input = BaseT::getFeedback();
T error = CalcError(setPoint, input);

return CalcP(error, gainP);
}

T P_D(T setPoint, T gainP, T gainD)
{
T input = BaseT::getFeedback();
T error = CalcError(setPoint, input);
unsigned int deltaTime = BaseT::getDeltaTime();

return CalcP(error, gainP) + CalcD(error, deltaTime, gainD);
}

T P_I(T setPoint, T gainP, T gainI)
{
T input = BaseT::getFeedback();
T error = CalcError(setPoint, input);
unsigned int deltaTime = BaseT::getDeltaTime();

return CalcP(error, gainP) + CalcI(error, deltaTime, gainI);
}

T P_I_D(T setPoint, T gainP, T gainI, T gainD)
{
T input = BaseT::getFeedback();
T error = CalcError(setPoint, input);
unsigned int deltaTime = BaseT::getDeltaTime();

return CalcP(error, gainP) + CalcI(error, deltaTime, gainI) + CalcD(error, deltaTime, gainD);
}

private:
T _integralAcc;
T _lastError;

inline T CalcError(T setPoint, T input)
{
T error = setPoint - input;

if (error < BaseT::getSmallestAcceptableError() && error > 0 ||
error > -BaseT::getSmallestAcceptableError() && error < 0)
{
error = 0;
}

return error;
}

inline T CalcP(T error, T gain)
{
return error * gain;
}

inline T CalcI(T error, unsigned int deltaTime, T gain)
{
_integralAcc += (error * deltaTime) * gain;

return _integralAcc;
}

inline T CalcD(T error, unsigned int deltaTime, T gain)
{
T value = ((_lastError - error) / deltaTime) * gain;

_lastError = error;

return value;
}
};

So please comment on the correctness of the math especially.

Thanx!
Title: Re: My first little PID: please comment
Post by: jkerns on May 12, 2013, 06:33:02 AM
Math looks OK.

On the Derivitive control - that tends to get jumpy due to noise on the input - one often does a little filtering of the input to the derivitive. But that complicates the tuning, too much filtering and the D does nothing. YMMV
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 12, 2013, 07:15:15 AM
Math looks OK.

On the Derivitive control - that tends to get jumpy due to noise on the input - one often does a little filtering of the input to the derivitive. But that complicates the tuning, too much filtering and the D does nothing. YMMV

Thanx for taking the time to look at my math.

How would you do such filtering on the D input?
Title: Re: My first little PID: please comment
Post by: jkerns on May 12, 2013, 10:19:57 AM
A simple digitial approximation of a first order exponential filter can be done by:

filtered_error = (1 - filter_constant)*filtered_error + filter_constant*error

Where the filter_constant can be a constant value between 0 and 1 (filter as a function of the number of samples) or can be calculated from an actual time constant (in seconds) by:

filter_constant = deltaTime/(deltaTime + time_constant)

You want to pick a time constant that is faster than the expected time for the physical input to be changing, but longer than a few A/D samples.
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 14, 2013, 01:24:23 AM
Thank you. It's all about (tweaking) constants, I see  :P

In
filtered_error = (1 - filter_constant)*filtered_error + filter_constant*error

Is the bold 'filtered_error' a previous filtered_error value or...?
Otherwise I don't understand how you can use something that you're in the process of calculating...

Also: is it likely you would dynamically (at runtime) switch using P, I or D terms for a single controller process? I mean do I need to take into account that all methods update the delta-time, I-accumulator and last-error variables in order to allow mixed calling of the P, PI, PD and PID methods?
Title: Re: My first little PID: please comment
Post by: jkerns on May 14, 2013, 07:47:14 AM
Thank you. It's all about (tweaking) constants, I see  :P

In
filtered_error = (1 - filter_constant)*filtered_error + filter_constant*error

Is the bold 'filtered_error' a previous filtered_error value or...?

Yes. Your software is running in a loop so the variable "filtered_error" has a value from last time and now you use the old value in the calculation and it ends up in the new value. Much like using ++i or similar in C.

Quote
Also: is it likely you would dynamically (at runtime) switch using P, I or D terms for a single controller process? I mean do I need to take into account that all methods update the delta-time, I-accumulator and last-error variables in order to allow mixed calling of the P, PI, PD and PID methods?

I've never switched controller types in mid stream. It isn't uncommon to modify the gains as a function of operating conditions (referred to as gain scheduling) but switching between PI and PD or whatever would be unusual.

OH! I forgot to look! Do you have anti-windup on the I term? You need that. Really.  If the output of your controller is at the limit of the physical variable that you are sending to the actuator (e.g. 100% duty cycle)
you need to freeze the integrator so it doesn't keep integrating to some unobtainable value that has to be un-integraged some time later on - that is very destabilizing.  Typically you would compare the output of the controller with the actual limits of whatever your physical output is and either not call the integrator sub-function or just substitute zero for the error in the input to the integrator. I guess you could consider that a dynamic switch but that is not a common terminology.

If you are having trouble sleeping, I drone on and on for about an hour on PID control here... http://www.youtube.com/playlist?list=PL3ea3YrO-K5gxpOrrgKytFBvIzBv_WsXs (http://www.youtube.com/playlist?list=PL3ea3YrO-K5gxpOrrgKytFBvIzBv_WsXs)
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 14, 2013, 08:21:21 AM
Yes, I wondered about that (about I).

It keeps building up and up and you would have to undo all that. To my naive mind that would cause overshoot and oscillation.

So if the I-term reaches the max value it just stops accumulating? Or when the combined output (return value from my methods) reaches max value? I would need some value range management in my class anyway, especially when mulitple P/I/D Terms are added together...

Thinking on the I-term some more, I would even say that if your error is 0 (or within the acceptable error-band) I would simply stop using the I-term altogether and perhaps reset it (to what value I don't know - some % of its current value?) for when it is used later on (when we travel outside the error band) it would otherwise make the system 'jerk'.

Does that make sense?

PS: Had already found your YT channel and I have no trouble sleeping ;-)  Thanx.
Title: Re: My first little PID: please comment
Post by: jkerns on May 14, 2013, 08:41:56 AM
 
Yes, I wondered about that (about I).

It keeps building up and up and you would have to undo all that. To my naive mind that would cause overshoot and oscillation.

So if the I-term reaches the max value it just stops accumulating? Or when the combined output (return value from my methods) reaches max value? I would need some value range management in my class anyway, especially when mulitple P/I/D Terms are added together...

Stop the integrator when the combined output reaches it's limit.


Quote
Thinking on the I-term some more, I would even say that if your error is 0 (or within the acceptable error-band) I would simply stop using the I-term altogether and perhaps reset it (to what value I don't know - some % of its current value?) for when it is used later on (when we travel outside the error band) it would otherwise make the system 'jerk'.

Leave it running. The point of using the I is pretty much to drive the error to zero.  Turning it off or resetting it will just introduce some error to the sytem.

Quote
PS: Had already found your YT channel and I have no trouble sleeping ;-)  Thanx.
;D
Title: Re: My first little PID: please comment
Post by: jwatte on May 14, 2013, 10:45:26 AM
Another option to deal with constantly accumulating I error is to use a leaky integrator for the I term, just like you do for the D term. Except for the D term, it's a single-pole low-pass filter; for the I term it's a single-term high-pass filter.

Also, yes, with an I term you will almost always get a little bit of overshoot. The point of the D term is to limit the slew rate of the response, so that the overshoot can be dampened. With a well tuned system, your I/D control terms will end up being "critically damped" which is a nice place to start from by way of trading responsiveness and damping.
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 14, 2013, 11:16:56 AM
Another option to deal with constantly accumulating I error is to use a leaky integrator for the I term, just like you do for the D term. Except for the D term, it's a single-pole low-pass filter; for the I term it's a single-term high-pass filter.

I understand what low-pass and high-pass mean (from audio) but I have no idea how to build code for it...


Another thing I noticed is that I see people mix up the way the delta error is calculated. Some do
Code: [Select]
previousError - newErrorOthers do
Code: [Select]
newError - previousError
Is this something that you will trim/tune out of the controller (using gains) or is one correct and the other one not?
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 14, 2013, 12:38:18 PM
I've never switched controller types in mid stream. It isn't uncommon to modify the gains as a function of operating conditions (referred to as gain scheduling) but switching between PI and PD or whatever would be unusual.

This gives me an idea. If you put a gain of zero in for a specific term you basically shut it of. I could optimize the code to keep performing the maintenance on the variables but not calculate anything when gain equals zero. That would simplify the public interface of this class considerably.


Code: [Select]
/*
BaseT is used as a base class and implements:
T getFeedback()
unsigned int getDeltaTime()
T getSmallestAcceptableError()

T is the data type that hold the values. Either float or double.

Algorithm:
Error = SetPoint - Feedback
P = Error * gainP
I = Sum(previous-I's) + ((Error * deltaTime) * gainI)
D = ((Error - previous-Error) / deltaTime) * gainD

PI = P + I
PD = P + D
PID = P + I + D
*/
template<class BaseT, typename T>
class PID : public BaseT
{
public:
// pass a gain of zero to bypass a specific term.
T Process(T setPoint, T gainP, T gainI, T gainD)
{
T input = BaseT::getFeedback();
T error = CalcError(setPoint, input);
unsigned int deltaTime = BaseT::getDeltaTime();

return CalcP(error, gainP) + CalcI(error, deltaTime, gainI) + CalcD(error, deltaTime, gainD);
}

private:
T _integralAcc;
T _lastError;

inline T CalcError(T setPoint, T input)
{
T error = setPoint - input;

if ((error < BaseT::getSmallestAcceptableError() && error > 0) ||
(error > -BaseT::getSmallestAcceptableError() && error < 0))
{
error = 0;
                        _integralAcc = 0; // Good idea??
}

return error;
}

inline T CalcP(T error, T gain)
{
if (gain == 0) return 0;

return error * gain;
}

inline T CalcI(T error, unsigned int deltaTime, T gain)
{
if (gain != 0)
{
_integralAcc += (error * deltaTime) * gain;
}

return _integralAcc;
}

inline T CalcD(T error, unsigned int deltaTime, T gain)
{
T value = 0;

if (gain != 0)
{
value = ((error - _lastError) / deltaTime) * gain;
}

_lastError = error;

return value;
}
};
Title: Re: My first little PID: please comment
Post by: jwatte on May 14, 2013, 04:58:27 PM
newError - prevError is just -(prevError - newError) so you just flip the sign of the D gain.

High-pass and low-pass in these signals is exactly the same as for audio. The simplest way is a single accumulator variable and a time constant; more complex filters use multiple delay stages, IIR or FIR style.

In brief
low pass: value = newValue + oldValue * c
high pass: value = newValue - oldValue * c

The range of "c" is 0 .. 1, and the gain of the filter is approimately varying like 1/(1 - c).
You may be more familiar with the form:
value = (newValue * (1 - c') + oldValue * c')
This is equivalent to a gain-compensated filter with a warping of the c factor to c'.
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 15, 2013, 01:20:59 AM
newError - prevError is just -(prevError - newError) so you just flip the sign of the D gain.

But does 'just flipping the sign' have a big effect on the process and the end value that is calculated? As I understood it, the D-term is small(ish) to begin with - because it susceptible to noise and you don't want noise to disrupt your regulator. But flipping the sign sounds like a meaningful change to the algorithm.

I interpreted the whole PID thing as a big summation of all the terms. So Error - lastError sound correct to me - same way (order) you calculate delta time.
Title: Re: My first little PID: please comment
Post by: jkerns on May 15, 2013, 07:04:58 AM
Depending on which way you subtract (old-new or new-old) you get a different sign on the error term so you would need a different sign on the D gain to get the correction to work in the proper direction.

Let's look at an example, if the error is increasing positive, you want the output from the controller to go negative to compensate. Right?  So, if you want to use a positive number for the gain (to match the other gains), you would select old - new to make the error term negitive which will make the output of the D part of the controller to be negative.

The "algorithm" works the same either way, you just need to make sure that the output of the D term is working in the right direction. It's more of an algebra / book keeping problem than anything else.

Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 15, 2013, 08:04:02 AM
Let's look at an example, if the error is increasing positive, you want the output from the controller to go negative to compensate. Right?

I would think that the sign of the error only indicates if you need to 'accelerate' (pos) or 'de-accelerate' (neg). The size/amount/value of the error indicates if a small or large correction is needed.

But if the D-term purpose is to have a dampening effect, I would think the formula would be (P + I) - D (I think I saw that somewhere used in code like that). Than it would be clear(er) what the purpose of the D-term is.

Depending on which way you subtract (old-new or new-old) you get a different sign on the error term so you would need a different sign on the D gain to get the correction to work in the proper direction.

I understand that you can play around with signs all day. I just want to know how the formula was meant originally and why it works that way. I think I get most of it (I hope) ;-)

The whole D-term should be positive, then?


I am sorry if I test your patience and I do really appreciate the time you guys take to answer my questions. I am just trying to get this.

Thanks a lot.
Title: Re: My first little PID: please comment
Post by: jkerns on May 15, 2013, 09:53:07 AM
Bottom line - the objective of the D is to counteract any changes in the error. How you combine the + and - doesn't really matter just as long as when the error is going positive, the controller output is going negative.
Title: Re: My first little PID: please comment
Post by: jwatte on May 15, 2013, 09:54:58 AM
I don't think there is a "how it was meant originally" regarding the sign of the D term (or coefficient.) Once you work with math, you realize that sign is just an interpretation detail, as as long as you get it right (compared to the physical system you model,) which way it goes really doesn't matter. Except perhaps for aesthetics -- so, if you want a particular aesthetics, then choose that direction of the sign! Just be aware that not everyone chooses the same, so be clear to document your assumption when you write, and understand what assumption other authors use when they write.

Pet peeve: Even though it's well known that there are many conventions in various ways, authors tend to always forget to document what conventions they use in their particular writing/paper/tutorial. Row vectors on left, or column vectors on right? Row major or column major matrix storage? Left-handed or right-handed coordinate interpretation of the world? W first or W last in quaternions? Y up or Z up or Z down? Positive or negative "d" in a plane equation? The list goes on and on ...
Title: Re: My first little PID: please comment
Post by: jkerns on May 15, 2013, 11:43:08 AM

Pet peeve: Even though it's well known that there are many conventions in various ways, authors tend to always forget to document what conventions they use in their particular writing/paper/tutorial.

Units - that's a big one. Ignoring the g in F=(M/g) *A. That tends to work OK if you are working with KG / Newtons, but if you are working in pounds mass / pounds force, the answers tend to be off by 32.2  :)


Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 16, 2013, 01:32:57 AM
Okay. It's becoming guesswork at my part at this point, so I think I will start experimenting with this further until I have some more practical experience.

I am just ready to setup a PID controller for managing speed. I have my motor control software in place and my wheel encoders working, so now I have to feed the wheel encoder pulses into the PID and drive the motors with the output. Not sure yet how to get these values to mean the same thing, but I will do some more research first.

I want to thank you both very much for helping me out.
Title: Re: My first little PID: please comment
Post by: jkerns on May 16, 2013, 01:09:43 PM
You can translate your desired speed to the necessary pulse duration or pulses per second (which ever fits the time scale of your controller). Or, you can take the pulse duration / rate and convert it to speed.  And away you go.
Title: Re: My first little PID: please comment
Post by: waltr on May 17, 2013, 07:39:50 AM
This has been one of the better PID discussions here.

While experimenting I have found that outputting several of the PID input and output parameters useful and almost necessary to determine the PID loop response and for tuning the PID coefficients.
For this I use the u-controllers UART and send the data to a PC for collection and analysis. On my Bot I use XBees in transparent mode as a wireless serial line.

Data includes:
 1- Wheel speed (I use hardware timer counts between encoder ticks, The smaller the count the faster the wheel speed)
 2- The PWM duty cycle (actually the timer count that produces the pulse width)
 3- the three error terms in the PID.

Once collected on the PC I then import the data into Excel and plot the data on a graph. This gives a nice visual of how the PID loop is responding.
Do this for several physical conditions:
 1- steady state, running with no acceleration, turns.
 2- Starting and stopping.
 3- load changes due to slight inclines
 4- extreme load changes like when Bot runs into a non-detected object
Title: Re: My first little PID: please comment
Post by: obiwanjacobi on May 17, 2013, 10:47:24 AM
Currently I am devoting a good chunk of CPU time to outputting trace data.
Nothing structured enough though that I could import into Excel (that would make sense).

I have my PID experiment on hold at this moment while I'm combating some strange 'crashes' (http://www.societyofrobots.com/robotforum/index.php?topic=16940.0) of my car.