Software > Software

My first little PID: please comment

(1/5) > >>

obiwanjacobi:
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. 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: ---/*
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;
}
};

--- End code ---

So please comment on the correctness of the math especially.

Thanx!

jkerns:
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

obiwanjacobi:

--- Quote from: 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

--- End quote ---

Thanx for taking the time to look at my math.

How would you do such filtering on the D input?

jkerns:
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.

obiwanjacobi:
Thank you. It's all about (tweaking) constants, I see  :P

In

--- Quote from: jkerns on May 12, 2013, 10:19:57 AM ---filtered_error = (1 - filter_constant)*filtered_error + filter_constant*error

--- End quote ---

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?