I just started making my own PID library and was wondering if anyone could point out any glaring issues/bugs or possible improvements. The reason I'm creating a custom library is because I plan on adding special control features such as coefficient auto-tuning and others. I have already looked at the source code for the arduino PID library here, but the source code is a little hard to follow. I did notice some differences, but I don't know if that means my code is better or worse. Anyone see anything wrong with the logic in my code?
I would like to test the algorithm, but I don't want to spend a lot of time setting up the test. Does anyone know of a really simple and easy way to test PID algorithms?
//test
class BasicPID
{
private:
double currentTime;
double previousTime;
double sampleTime;
double P;
double I;
double D;
double P_Result;
double I_Result;
double D_Result;
double setpoint;
double error;
double previousError;
double integratedError;
double output;
double computeP();
double computeI();
double computeD();
public:
begin(double passedP, double passedI, double passedD, double passedSetpoint);
void changeP(double passedP);
void changeI(double passedI);
void changeD(double passedD);
void changeSetpoint(double passedSetpoint);
double computePID(double input);
};
BasicPID::begin(double passedP, double passedI, double passedD, double passedSetpoint)
{
currentTime = millis();
previousTime = currentTime;
P = passedP;
I = passedI;
D = passedD;
P_Result = 0;
I_Result = 0;
D_Result = 0;
setpoint = passedSetpoint;
error = 0;
previousError = 0;
integratedError = 0;
output = 0;
}
void BasicPID::changeP(double passedP)
{
P = passedP;
return;
}
void BasicPID::changeI(double passedI)
{
I = passedI;
return;
}
void BasicPID::changeD(double passedD)
{
D = passedD;
return;
}
void BasicPID::changeSetpoint(double passedSetpoint)
{
setpoint = passedSetpoint;
return;
}
double BasicPID::computePID(double input)
{
//find the amount of time since last sample was taken
currentTime = millis();
sampleTime = currentTime - previousTime;
previousTime = currentTime;
//calculate errors
previousError = error;
error = input - setpoint;
integratedError = integratedError + (error * (sampleTime / 1000));
//calculate P, I, and D components
P_Result = computeP();
I_Result = computeI();
D_Result = computeD();
//sum P, I, and D components to find total output to plant
output = P_Result + I_Result + D_Result;
return output;
}
double BasicPID::computeP()
{
double result;
result = error * P;
return result;
}
double BasicPID::computeI()
{
double result;
result = integratedError * I;
return result;
}
double BasicPID::computeD()
{
double result;
result = ((error - previousError) / (sampleTime / 1000)) * D;
return result;
}
//initialize PID class
BasicPID planePID;
//user defined variables
float P = 0.5;
float I = 0.5;
float D = 0.5;
float setpoint = 5;
void setup()
{
//initialize PID loop
planePID.begin(P, I, D, setpoint);
}
void loop()
{
//do nothing for now
}
Don't use floating point maths. It makes the calculation very slow.
This is the code I use to control the speed of a small DC motor. I think all the variables are unsigned long. The units are the number of microseconds for 1 revolution - varying between about 30,000 and 8,000
// This is the PID part - actually just PI
// now figure the new pwm value
error = revMicros - setPoint;
// this is essentially the regular PID approach
// first the PTerm
errorPWM = calcPWM(error, locoData.kP);
// attenuate the negative values?
if (errorPWM < 0) {
errorPWM -= ((errorPWM * locoData.kPScaleFactor) >> 4);
}
// then the ITerm
basePWM += calcPWM(error, locoData.kI);
if (basePWM > 255) {
basePWM = 255;
}
if (basePWM < 0) {
basePWM = 0;
}
}
}
//====================
long calcPWM(long microsVal, byte kFactor) {
// note that division of a negative number does funny things
long pwmCalc = microsVal * kFactor;
if (pwmCalc < 0) {
pwmCalc = -(abs(pwmCalc) / setPoint);
}
else {
pwmCalc = pwmCalc / setPoint;
}
return pwmCalc;
}
BEWARE - I am an atheist when it comes to PID. Most of it is window dressing for a suck-it-and-see system
Robin2:
Don't use floating point maths. It makes the calculation very slow.
I wish I could get away with not using floating point math, but it will be necessary (the usual arduino PID library also uses floating point math).
Robin2:
This is the code I use to control the speed of a small DC motor. I think all the variables are unsigned long. The units are the number of microseconds for 1 revolution - varying between about 30,000 and 8,000
Interesting, what is your feedback input? As in how do you update revMicros?
Robin2:
BEWARE - I am an atheist when it comes to PID. Most of it is window dressing for a suck-it-and-see system
Robin2:
I would take a lot of convincing that floating point maths is necessary or adds any value - apart from convenience for the programmer.
Well, I've seen from many other people who use PID loops that their coefficients are as low as 0.2-0.5 and usually have an precision of 0.5 (i.e. P = 0.5 or P = 1.5, etc). I guess I could still work around it, but that is something I'll have to think about.
I can't sacrifice precision either way since this will be part of my flight controller code for an RC airplane - it has to be as robust as possible or I will be spending money on replacement parts after a crash.
coefficients are as low as 0.2-0.5 and usually have an precision of 0.5 (i.e. P = 0.5 or P = 1.5, etc)
It is utterly trivial to get around this with integer-only PID calculations. I do this by expressing Kp etc. as integers, multiplied by a factor of 10.
To implement multiplication of the error term by Kp = 0.2, multiply instead by 2 and then divide the result by 10.
For proper rounding of positive terms, instead do (Kp*error +5)/10
Power_Broker:
Well, I've seen from many other people who use PID loops that their coefficients are as low as 0.2-0.5 and usually have an precision of 0.5 (i.e. P = 0.5 or P = 1.5, etc). I guess I could still work around it, but that is something I'll have to think about.
Which is neither here nor there. The scale of the coefficients is a function of the range of the input and output values, the required process control resolution, the update interval, the response characteristics and other factors. Different processes will result in radically different coefficients.
Regards,
Ray L.
Thanks for all of your input on getting around floats with ints, I will definitely look into it.
But what about the logic? Have I made any idiot mistakes in the logic? For instance, should the error be setpoint minus the input or the other way around?