Pages: [1]   Go Down
Author Topic: My first little PID: please comment  (Read 499 times)
0 Members and 1 Guest are viewing this topic.
Netherlands
Offline Offline
Jr. Member
**
Karma: 0
Posts: 93
Profile before you Optimize.
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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 / 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!
Logged


Massachusetts, USA
Offline Offline
Tesla Member
***
Karma: 178
Posts: 8064
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Since the AVR compiler uses the same size float for both float and double there is no real need to make that data type adjustable. 

Is your DeltaTime in seconds, milliseconds, or microseconds?

If you have multiple PID loops running at different sample rates, how do they get their individual DeltaTime from BaseT::getDeltaTime()?

I would use member functions to set setpoint, gainP, gainI, and gainD rather than pass them as arguments each time.
Logged

Send Bitcoin tips to: 1L3CTDoTgrXNA5WyF77uWqt4gUdye9mezN
Send Litecoin tips to : LVtpaq6JgJAZwvnVq3ftVeHafWkcpmuR1e

Netherlands
Offline Offline
Jr. Member
**
Karma: 0
Posts: 93
Profile before you Optimize.
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Since the AVR compiler uses the same size float for both float and double there is no real need to make that data type adjustable. 

Hahaha, didn't know that.

Is your DeltaTime in seconds, milliseconds, or microseconds?

Does it matter? The idea is that the user of the class uses what he/she needs. High sample rate probably uses microseconds, lower sample rate probably uses milliseconds. Don't think seconds would be to useful assuming you would want a sample rate of at least a couple a times a second...?

If you have multiple PID loops running at different sample rates, how do they get their individual DeltaTime from BaseT::getDeltaTime()?

The idea of the class is that it only implements the PID process algortithm, nothing else. You have to stack (template) classes on top of each other in order to get what you need. So in this case the base class would be responsible for managing the delta time.

I would use member functions to set setpoint, gainP, gainI, and gainD rather than pass them as arguments each time.

I purposely did not do that. The thinking here is that first I do not want to make that assumption for the user (taking up extra RAM bytes) - remember this is a library. The only bytes taken are needed for the algorithm. Second the method parameters are the input-values that can change, while the BaseT::Xxxx methods are the information it needs to perform the algorithm. This also allows specifying constants without permanently taking up RAM bytes.

Thanx. Did you see any errors in the math?
Logged


Pages: [1]   Go Up
Jump to: