├── .gitignore ├── LICENSE ├── README.md ├── examples └── example │ └── example.ino ├── library.properties └── src ├── ArduPID.cpp └── ArduPID.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PB2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArduPID 2 | [![GitHub version](https://badge.fury.io/gh/PowerBroker2%2FArduPID.svg)](https://badge.fury.io/gh/PowerBroker2%2FArduPID) [![arduino-library-badge](https://www.ardu-badge.com/badge/ArduPID.svg?)](https://www.ardu-badge.com/ArduPID)

3 | PID library for Arduinos with greater accuracy than the legacy Arduino PID library. 4 | 5 | # Why Use PID? 6 | PID stands for Proportional, Integral, and Derivative and is a simple type of controller. PID controllers can provide stable control of nearly any system such as the speed of a motor, position of a servo, speed of a car (cruise control), and much more. 7 | 8 | # How Does it Work? 9 | See the explanation video [here](https://www.youtube.com/watch?v=OqvrYNJvtaU) 10 | 11 | # How to Tune a PID: 12 | See [here](https://pidexplained.com/how-to-tune-a-pid-controller/) 13 | 14 | # How to Use the Library: 15 | First import the library, instantiate an ArduPID class, and create 6 doubles: 16 | - Setpoint 17 | - Input 18 | - Output 19 | - P Gain 20 | - I Gain 21 | - D Gain 22 | 23 | ```C++ 24 | #include "ArduPID.h" 25 | 26 | ArduPID myController; 27 | 28 | double setpoint = 512; 29 | double input; 30 | double output; 31 | double p = 1; 32 | double i = 0; 33 | double d = 0; 34 | ``` 35 | 36 | Next, within the `setup()` function, initialize the controller with the references to the input, output, and setpoint variables. Also pass the values of the PID gains. After initializing the controller within `setup()`, you can change the settings/configuration of the controller. 37 | 38 | ```C++ 39 | void setup() 40 | { 41 | Serial.begin(115200); 42 | myController.begin(&input, &output, &setpoint, p, i, d); 43 | 44 | // ADD MORE CONFIGURATION COMMANDS HERE <-------------------- 45 | } 46 | ``` 47 | 48 | Here are the different examples of configuration commands available via the library: 49 | 50 | - `reverse()` 51 | - Reverse direction of output 52 | - `setSampleTime(const unsigned int& _minSamplePeriodMs)` 53 | - Will ensure at least `_minSamplePeriodMs` have past between successful compute() calls - *this function is not necessary, but is included for convenience* 54 | - `setOutputLimits(const double& min, const double& max)` 55 | - Clip output to values to `min` and `max` 56 | - `setBias(const double& _bias)` 57 | - Output will have a constant offset of `_bias`, usually used in conjunction with `setOutputLimits()` 58 | - `setWindUpLimits(const double& min, const double& max)` 59 | - Clip integral term values to `min` and `max`to prevent [integral wind-up](https://www.youtube.com/watch?v=H4YlL3rZaNw) 60 | - `start()` 61 | - Enable/turns on the controller 62 | - `reset()` 63 | - Used for resetting the I and D terms - *only use this if you know what you're doing* 64 | - `stop()` 65 | - Disable/turn off the PID controller (`compute()` will not do anything until `start()` is called) 66 | 67 | Within the `loop()`, update the input variable via your sensor and then call `compute()`. The output variable will then be automatically updated and you can use that updated value to send an updated command to your actuator. You can also use the `debug()` command to print various status information about your controller to the serial plottor/monitor: 68 | 69 | ```C++ 70 | void loop() 71 | { 72 | input = analogRead(A0); // Replace with sensor feedback 73 | 74 | myController.compute(); 75 | myController.debug(&Serial, "myController", PRINT_INPUT | // Can include or comment out any of these terms to print 76 | PRINT_OUTPUT | // in the Serial plotter 77 | PRINT_SETPOINT | 78 | PRINT_BIAS | 79 | PRINT_P | 80 | PRINT_I | 81 | PRINT_D); 82 | 83 | analogWrite(3, output); // Replace with plant control signal 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /examples/example/example.ino: -------------------------------------------------------------------------------- 1 | #include "ArduPID.h" 2 | 3 | 4 | 5 | 6 | ArduPID myController; 7 | 8 | 9 | 10 | 11 | double input; 12 | double output; 13 | 14 | // Arbitrary setpoint and gains - adjust these as fit for your project: 15 | double setpoint = 512; 16 | double p = 10; 17 | double i = 1; 18 | double d = 0.5; 19 | 20 | 21 | 22 | 23 | void setup() 24 | { 25 | Serial.begin(115200); 26 | myController.begin(&input, &output, &setpoint, p, i, d); 27 | 28 | // myController.reverse() // Uncomment if controller output is "reversed" 29 | // myController.setSampleTime(10); // OPTIONAL - will ensure at least 10ms have past between successful compute() calls 30 | myController.setOutputLimits(0, 255); 31 | myController.setBias(255.0 / 2.0); 32 | myController.setWindUpLimits(-10, 10); // Groth bounds for the integral term to prevent integral wind-up 33 | 34 | myController.start(); 35 | // myController.reset(); // Used for resetting the I and D terms - only use this if you know what you're doing 36 | // myController.stop(); // Turn off the PID controller (compute() will not do anything until start() is called) 37 | } 38 | 39 | 40 | 41 | 42 | void loop() 43 | { 44 | input = analogRead(A0); // Replace with sensor feedback 45 | 46 | myController.compute(); 47 | myController.debug(&Serial, "myController", PRINT_INPUT | // Can include or comment out any of these terms to print 48 | PRINT_OUTPUT | // in the Serial plotter 49 | PRINT_SETPOINT | 50 | PRINT_BIAS | 51 | PRINT_P | 52 | PRINT_I | 53 | PRINT_D); 54 | 55 | analogWrite(3, output); // Replace with plant control signal 56 | } -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ArduPID 2 | version=0.2.1 3 | author=PowerBroker2 4 | maintainer=PowerBroker2 5 | sentence=PID library for Arduinos with greater accuracy than the legacy Arduino PID library 6 | paragraph=PID library for Arduinos with greater accuracy than the legacy Arduino PID library 7 | category=Signal Input/Output 8 | url=https://github.com/PowerBroker2/ArduPID 9 | architectures=* 10 | depends=FireTimer 11 | -------------------------------------------------------------------------------- /src/ArduPID.cpp: -------------------------------------------------------------------------------- 1 | #include "ArduPID.h" 2 | 3 | 4 | 5 | 6 | void ArduPID::begin(double* _input, 7 | double* _output, 8 | double* _setpoint, 9 | const double& _p, 10 | const double& _i, 11 | const double& _d, 12 | const pOn& _pOn, 13 | const dir& _direction, 14 | const unsigned int& _minSamplePeriodMs, 15 | const double& _bias) 16 | { 17 | input = _input; 18 | output = _output; 19 | setpoint = _setpoint; 20 | setCoefficients(_p, _i, _d); 21 | setPOn(_pOn); 22 | setBias(_bias); 23 | setDirection(_direction); 24 | setSampleTime(_minSamplePeriodMs); 25 | 26 | reset(); 27 | start(); 28 | } 29 | 30 | 31 | 32 | 33 | 34 | void ArduPID::start() 35 | { 36 | if (modeType != ON) 37 | { 38 | modeType = ON; 39 | reset(); 40 | } 41 | } 42 | 43 | 44 | 45 | 46 | void ArduPID::reset() 47 | { 48 | curError = 0; 49 | curSetpoint = 0; 50 | curInput = 0; 51 | 52 | lastError = 0; 53 | lastSetpoint = 0; 54 | lastInput = 0; 55 | 56 | pOut = 0; 57 | iOut = 0; 58 | dOut = 0; 59 | 60 | timer.start(); 61 | } 62 | 63 | 64 | 65 | 66 | 67 | void ArduPID::stop() 68 | { 69 | if (modeType != OFF) 70 | modeType = OFF; 71 | } 72 | 73 | 74 | 75 | 76 | void ArduPID::doCompute(ulong timeDiff) { 77 | kp = pIn; 78 | if (timeDiff > 0) { 79 | ki = iIn * (timeDiff / 1000.0); 80 | kd = dIn / (timeDiff / 1000.0); // go to inf if timeDiff == 0 81 | } else { 82 | ki = 0.0; 83 | kd = 0.0; 84 | } 85 | 86 | if (direction == BACKWARD) 87 | { 88 | kp *= -1; 89 | ki *= -1; 90 | kd *= -1; 91 | } 92 | 93 | lastInput = curInput; 94 | lastSetpoint = curSetpoint; 95 | lastError = curError; 96 | 97 | curInput = *input; 98 | curSetpoint = *setpoint; 99 | curError = curSetpoint - curInput; 100 | 101 | double dInput = *input - lastInput; 102 | 103 | if (pOnType == P_ON_E) 104 | pOut = kp * curError; 105 | else if (pOnType == P_ON_M) 106 | pOut = -kp * dInput; 107 | 108 | dOut = -kd * dInput; // Derrivative on measurement 109 | 110 | double iTemp = (iIn == 0.0) ? 0.0 : iOut + (ki * ((curError + lastError) / 2.0)); // Trapezoidal integration 111 | iTemp = constrain(iTemp, windupMin, windupMax); // Prevent integral windup 112 | 113 | double outTemp = bias + pOut + dOut; // Output without integral 114 | double iMax = constrain(outputMax - outTemp, 0, outputMax); // Maximum allowed integral term before saturating output 115 | double iMin = constrain(outputMin - outTemp, outputMin, 0); // Minimum allowed integral term before saturating output 116 | 117 | iOut = constrain(iTemp, iMin, iMax); 118 | 119 | outTemp += iOut; 120 | outTemp = constrain(outTemp, outputMin, outputMax); 121 | *output = outTemp; 122 | 123 | } 124 | 125 | 126 | 127 | 128 | void ArduPID::compute() 129 | { 130 | if (timer.fire() && modeType == ON) 131 | { 132 | doCompute(timer.timeDiff); 133 | } 134 | } 135 | 136 | 137 | 138 | 139 | void ArduPID::setOutputLimits(const double& min, const double& max) 140 | { 141 | if (max > min) 142 | { 143 | outputMax = max; 144 | outputMin = min; 145 | 146 | if (modeType == ON) 147 | *output = constrain(*output, outputMin, outputMax); 148 | } 149 | } 150 | 151 | 152 | 153 | 154 | void ArduPID::setWindUpLimits(const double& min, const double& max) 155 | { 156 | if (max > min) 157 | { 158 | windupMax = max; 159 | windupMin = min; 160 | } 161 | } 162 | 163 | 164 | 165 | 166 | void ArduPID::setDeadBand(const double& min, const double& max) 167 | { 168 | if (max >= min) 169 | { 170 | deadBandMax = max; 171 | deadBandMin = min; 172 | } 173 | } 174 | 175 | 176 | 177 | 178 | void ArduPID::setPOn(const pOn& _pOn) 179 | { 180 | pOnType = _pOn; 181 | } 182 | 183 | 184 | 185 | 186 | void ArduPID::setBias(const double& _bias) 187 | { 188 | bias = _bias; 189 | } 190 | 191 | 192 | 193 | 194 | void ArduPID::setCoefficients(const double& _p, const double& _i, const double& _d) 195 | { 196 | if (_p >= 0 && _i >= 0 && _d >= 0) 197 | { 198 | pIn = _p; 199 | iIn = _i; 200 | dIn = _d; 201 | } 202 | } 203 | 204 | 205 | 206 | 207 | void ArduPID::setDirection(const dir& _direction) 208 | { 209 | direction = _direction; 210 | 211 | if (modeType == ON) 212 | reset(); 213 | } 214 | 215 | 216 | 217 | 218 | void ArduPID::reverse() 219 | { 220 | if (direction == FORWARD) 221 | direction = BACKWARD; 222 | else if (direction == BACKWARD) 223 | direction = FORWARD; 224 | 225 | if (modeType == ON) 226 | reset(); 227 | } 228 | 229 | 230 | 231 | 232 | void ArduPID::setSampleTime(const unsigned int& _minSamplePeriodMs) 233 | { 234 | timer.begin(_minSamplePeriodMs); 235 | } 236 | 237 | 238 | 239 | 240 | double ArduPID::B() 241 | { 242 | return bias; 243 | } 244 | 245 | 246 | 247 | 248 | double ArduPID::P() 249 | { 250 | return pOut; 251 | } 252 | 253 | 254 | 255 | 256 | double ArduPID::I() 257 | { 258 | return iOut; 259 | } 260 | 261 | 262 | 263 | 264 | double ArduPID::D() 265 | { 266 | return dOut; 267 | } 268 | 269 | 270 | 271 | 272 | void ArduPID::debug(Stream* stream, const char* controllerName, const byte& mask) 273 | { 274 | if (mask & PRINT_INPUT) 275 | { 276 | stream->print(controllerName); 277 | stream->print("_input "); 278 | } 279 | 280 | if (mask & PRINT_OUTPUT) 281 | { 282 | stream->print(controllerName); 283 | stream->print("_output "); 284 | } 285 | 286 | if (mask & PRINT_SETPOINT) 287 | { 288 | stream->print(controllerName); 289 | stream->print("_setpoint "); 290 | } 291 | 292 | if (mask & PRINT_BIAS) 293 | { 294 | stream->print(controllerName); 295 | stream->print("_bias "); 296 | } 297 | 298 | if (mask & PRINT_P) 299 | { 300 | stream->print(controllerName); 301 | stream->print("_P "); 302 | } 303 | 304 | if (mask & PRINT_I) 305 | { 306 | stream->print(controllerName); 307 | stream->print("_I "); 308 | } 309 | 310 | if (mask & PRINT_D) 311 | { 312 | stream->print(controllerName); 313 | stream->print("_D "); 314 | } 315 | 316 | stream->println(); 317 | 318 | if (mask & PRINT_INPUT) 319 | { 320 | stream->print(*input); 321 | stream->print(" "); 322 | } 323 | 324 | if (mask & PRINT_OUTPUT) 325 | { 326 | stream->print(*output); 327 | stream->print(" "); 328 | } 329 | 330 | if (mask & PRINT_SETPOINT) 331 | { 332 | stream->print(*setpoint); 333 | stream->print(" "); 334 | } 335 | 336 | if (mask & PRINT_BIAS) 337 | { 338 | stream->print(bias); 339 | stream->print(" "); 340 | } 341 | 342 | if (mask & PRINT_P) 343 | { 344 | stream->print(pOut); 345 | stream->print(" "); 346 | } 347 | 348 | if (mask & PRINT_I) 349 | { 350 | stream->print(iOut); 351 | stream->print(" "); 352 | } 353 | 354 | if (mask & PRINT_D) 355 | { 356 | stream->print(dOut); 357 | stream->print(" "); 358 | } 359 | 360 | stream->println(); 361 | } 362 | -------------------------------------------------------------------------------- /src/ArduPID.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | #include "FireTimer.h" 4 | 5 | 6 | 7 | 8 | const byte PRINT_INPUT = 0b1; 9 | const byte PRINT_OUTPUT = 0b10; 10 | const byte PRINT_SETPOINT = 0b100; 11 | const byte PRINT_BIAS = 0b1000; 12 | const byte PRINT_P = 0b10000; 13 | const byte PRINT_I = 0b100000; 14 | const byte PRINT_D = 0b1000000; 15 | 16 | 17 | 18 | 19 | enum pOn { P_ON_E, P_ON_M }; 20 | enum mode { OFF, ON }; 21 | enum dir { FORWARD, BACKWARD }; 22 | 23 | 24 | 25 | 26 | class ArduPID 27 | { 28 | public: 29 | double* input; 30 | double* output; 31 | double* setpoint; 32 | 33 | 34 | 35 | 36 | virtual void begin(double* _input, 37 | double* _output, 38 | double* _setpoint, 39 | const double& _p = 0, 40 | const double& _i = 0, 41 | const double& _d = 0, 42 | const pOn& _pOn = P_ON_E, 43 | const dir& _direction = FORWARD, 44 | const unsigned int& _minSamplePeriodMs = 0, 45 | const double& _bias = 0); 46 | void start(); 47 | void reset(); 48 | void stop(); 49 | virtual void compute(); 50 | void doCompute(ulong timeDiff); 51 | void setOutputLimits(const double& min, const double& max); 52 | void setWindUpLimits(const double& min, const double& max); 53 | void setDeadBand(const double& min, const double& max); 54 | void setPOn(const pOn& _pOn); 55 | void setBias(const double& _bias); 56 | void setCoefficients(const double& _p, const double& _i, const double& _d); 57 | void setDirection(const dir& _direction); 58 | void reverse(); 59 | void setSampleTime(const unsigned int& _minSamplePeriodMs); 60 | 61 | double B(); 62 | double P(); 63 | double I(); 64 | double D(); 65 | 66 | void debug(Stream* stream = &Serial, 67 | const char* controllerName = "controller", 68 | const byte& mask = 0xFF); 69 | 70 | 71 | 72 | 73 | protected: 74 | double bias; 75 | 76 | double outputMax = 255; 77 | double outputMin = 0; 78 | 79 | double windupMax = 1000; 80 | double windupMin = -1000; 81 | 82 | double deadBandMax = 0; 83 | double deadBandMin = 0; 84 | 85 | double curError; 86 | double curSetpoint; 87 | double curInput; 88 | 89 | double lastError; 90 | double lastSetpoint; 91 | double lastInput; 92 | 93 | double pIn; 94 | double iIn; 95 | double dIn; 96 | 97 | double kp; 98 | double ki; 99 | double kd; 100 | 101 | double pOut; 102 | double iOut; 103 | double dOut; 104 | 105 | pOn pOnType; 106 | mode modeType; 107 | dir direction; 108 | 109 | FireTimer timer; 110 | }; 111 | --------------------------------------------------------------------------------