Following some comments below and some further thought I have very slightly amended the code and the example. Unless you are interested in reading the history from top to bottom I suggest you skip ahead to Reply #8
A while back I wanted to use the Arduino PID library to control the speed of a small DC motor in a model train but I could not figure out how to choose suitable K values. Eventually I figured out a simple program to control my motor using the core calculations from the PID library.
Recently I got a cheap 3D printer and while exploring how to control the extruder nozzle temperature I gained a lot more experience with PID control as the heater responds very slowly compared to the motor.
This has enabled me to encapsulate my code in the attached EasyPID.h file. I have also written the code so it only uses integer maths for better performance.
I hope my code may make it easier to incorporate PID control into projects, and maybe also make it easier to explore the internal workings of the system
As well as the attached motor control program I have successfully tested EasyPID.h with my 3D printer nozzle heater and a DIY servo in which a low geared DC motor adjusts the position of a potentiometer.
In the motor program there is a function called setupEasyPID() which provides a simple way to set the weightings (equivalent to the K values in the PID library).
To incorporate this into your own program just put a copy of the EasyPID.h file in the same folder as your .ino file and add this line near the top of your .ino file
#include "EasyPID.h"
and then include the function setupEasyPID() in your .ino file and call it from setup().
For convenience this is the code in EasyPID.h
// data and code for EasyPID
#include <Arduino.h>
class EasyPID {
public:
// the control variables
long errorRange;
long outputRange;
int errorOutOfRangeSlowOutput;
int errorOutOfRangeFastOutput;
int iTermWeightingSlow;
int pTermWeightingSlow;
int dTermWeightingSlow;
int iTermWeightingFast;
int pTermWeightingFast;
int dTermWeightingFast;
// private: // having these public is easier for debugging
// but they would not normally need to accessed by the user's program XXXXX
// variables internal to the function
long scaledError ;
long scaledErrorRange;
long scaledOutputRange;
long totalOutput;
int iTermWeighting;
int pTermWeighting;
int dTermWeighting;
long unweightedErrorOutput;
long prevUnweightedErrorOutput = 0;
byte precisionScaleFactor = 10;
long oorTerm;
long iTerm;
long iTermChange;
long eTerm;
long dTerm;
//==============
public:
long calc(long errVal) {
// scale up the values to retain precision during calculations
scaledError = errVal << precisionScaleFactor;
scaledErrorRange = errorRange << precisionScaleFactor;
scaledOutputRange = outputRange << precisionScaleFactor;
// if the error is outside the range for EasyPID control
// use the special values and not EasyPID control
if (scaledError > scaledErrorRange) {
oorTerm = errorOutOfRangeSlowOutput;
iTerm = 0;
eTerm = 0;
}
else if (scaledError < -scaledErrorRange) {
oorTerm = errorOutOfRangeFastOutput;
iTerm = 0;
eTerm = 0;
}
// if the error is within the range apply EasyPID
else {
oorTerm = 0;
// use different weghtings for positive or negative errors
if (scaledError >= 0) {
iTermWeighting = iTermWeightingSlow;
pTermWeighting = pTermWeightingSlow;
dTermWeighting = dTermWeightingSlow;
}
else {
iTermWeighting = iTermWeightingFast;
pTermWeighting = pTermWeightingFast;
dTermWeighting = dTermWeightingFast;
}
// now we are ready for the EasyPID calcs
// calculate the unweighted error output for the current error
if (scaledError >= 0) {
unweightedErrorOutput = scaledError / errorRange;
}
else {
// this avoids problems with division of a negative integer
unweightedErrorOutput = -(abs(scaledError) / errorRange);
}
// update the iTerm
iTermChange = unweightedErrorOutput * iTermWeighting;
iTerm += iTermChange;
if (iTerm > scaledOutputRange) {
iTerm = scaledOutputRange;
}
if (iTerm < -scaledOutputRange) {
iTerm = -scaledOutputRange;
}
// calculate the eTerm
eTerm = unweightedErrorOutput * pTermWeighting;
// calculate the dTerm
dTerm = (unweightedErrorOutput - prevUnweightedErrorOutput) * dTermWeighting;
prevUnweightedErrorOutput = unweightedErrorOutput;
}
totalOutput = iTerm + eTerm + dTerm;
// scale it back to real-world values and add oorTerm
totalOutput = (totalOutput >> precisionScaleFactor) + oorTerm;
return totalOutput;
}
//=================
public:
void printKeyVars() {
Serial.print(" scaledError "); Serial.print(scaledError);
Serial.print(" unweightedErrorOutput "); Serial.print(unweightedErrorOutput);
Serial.print(" oorTerm "); Serial.print(oorTerm);
Serial.print(" iTerm "); Serial.print(iTerm);
Serial.print(" eTerm "); Serial.print(eTerm);
Serial.print(" dTerm "); Serial.print(dTerm);
Serial.println();
}
};
And this is the code for the function setupEasyPID() as it appears in the attached EasyPIDMotorDemo.ino
void setupEasyPID() {
myEasyPID.errorRange = targetMicros;
myEasyPID.outputRange = 255;
// values to use when the error is out of range
myEasyPID.errorOutOfRangeSlowOutput = 255;
myEasyPID.errorOutOfRangeFastOutput = 0;
// weightings to use when speed is below target
// values should be in range 0 - 255 - for example 32 represents 12.5%
myEasyPID.iTermWeightingSlow = 32; // equivalent to kI
myEasyPID.pTermWeightingSlow = 32; // equivalent to kP
myEasyPID.dTermWeightingSlow = 0; // equivalent to kD
// and when speed is above target (in this case they are the same)
myEasyPID.iTermWeightingFast = 32;
myEasyPID.pTermWeightingFast = 32;
myEasyPID.dTermWeightingFast = 0;
}
Have fun.
As usual, comments are welcome.
EasyPID.h (3.37 KB)
EasyPIDMotorDemo.ino (4.38 KB)