├── .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 | [](https://badge.fury.io/gh/PowerBroker2%2FArduPID) [](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 |
--------------------------------------------------------------------------------