INTEGER P.I.D. FUNCTIONS

DO NOT USE FOR NEW DESIGNS

these work but have been supplanted by the floating point SRPID. they are functinoally the same but method-wise somewhat incompatible. this integer version is slightly faster, given that it's integer, but unless you enjoy scaling your numerica values ala the 1950's, when calculation values are numerically small the output "cogs", eg. instead of varying smoothly they jump, simply because a change from 1 to 2 is 200%, etc. when values are large enough that incremental change is a small portion of the value, they're fine.

the library source is available from my github repository.

simple algorithms are used to calculate integrals and derivatives by assuming that the sample rate (time) is constant between samples/invokations. eg. differentiation here is a simple sample-to-sample differencing, which is equivalent to Newton's difference quotient when h is constant (eg. 1). this is intended to be used with with my timed-event system SRTimer.

copyright tom jennings 2015, 2016, 2017

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.

SUMMARY

this object contains three independent classes: integrator, differentiator, and PID. probably violating good taste, i've exposed some of the internal storage for fast/easy access. this is a tiny microcontroller, not a desktop. you really can remember all the symbol names, and have the discipline to not piddle in RAM.

they are all instantiated (declared) thusly, often globally. instantiation does not allocate space; this is done by the begin() method below. this allows determining size at run time.

this skeletal example shows all three objects in use, for my convenience; they are all independent.

#include SRPID P; Integrator I; Differentiator D;

they must be initialized, once, in setup(), to allocate memory for the objects.

void setup () { P.begin (16, 2); // integrator depth 16, differentiator 2 P.threshhold= 11; // access to innards I.begin (100); // integrator of 100 items D.begin (4); }

invoke these periodically (eg. timed) via:

void loop () { int n= someInputValueFromSomewhere(); int x= P.pid (n); // calcs PID, int y= I.integ (n); int z= D.diff (n); int P.pid (int); // }

SRPID

-----------------------------------------------------------------------------

PID calculates a proportional integral deriviative function with each invokation of the pid() method with some input value, which is assumed to be a time series, eg. when called periodically using SRTimer or equivalent.

PID tracks changes in it's single input data value, and returns a value that represents information about changes to that data over time. the return value indicates in sign and magnitude how fast and how much the input data is changing. the emphasis here is on change, not it's numeric value.

a little-exploited use of PID will hopefully both illustrate what PID does and how to think about it.

PID is incredibly useful for deriving change information from one of the simplest of all analog sensors: the CdS photoresistor. the default settings for SRPID are in fact setup for this use. the resistance of the CdS cell is quite arbitrary, in that it depends on manufacturing details and the number of photons impinging on it at any given moment. we attach CdS cells to microcontrollers not to measure photons but to detect motion -- change.

deriving change information is what PID does. the fact that a given CdS cell and resistor attached to an analog input on an Arduino is returning "543" tells us literally nothing. intuitively you know that change to that value may mean something -- if the CdS cell is occluded, less light, resistance increases, current decreases, and the number returned by analogRead increases (in the setup below); conversely more light means lower numbers. it is change that is interesting, not numbers.

the following runnable example program will print out changes to the light impinging on a CdS cell attached to the A0 input pin and ground:

#include SRDPID P; void setup() { Serial.begin (9600); P.begin (16, 2); // setup PID P.threshhold= 10; // OPTIONAL THRESHHOLD FOR TUNING digitalWrite (A0, 1); // turn on internal pullup resistor } void loop () { int c= P.pid (analogRead (A0)); // pass the CdS result through PID if (c != 0) Serial.println (c); // print out changes delay (10); // example only; use a timer! }

when first run the program will print a bunch of numbers as it determines what is initially the steady-state value from the CdS cell. it will settle within a second or so. from then on it will print out only when light on the sensor changes. the sign indicates the direction (light vs. dark) and the magnitude (absolute value) represents rate of change -- small value for slow, large value for fast -- regardless of almost and ambient light level, try changing the threshhold and the delay number which is sample rate.

the PID calculation for the instantaneous input E is the sum S of the three terms:

S = ( E * propGain + // proportion integral (E) * integGain + // integral difference (E) * diffGain + // derivative ) * outGain // overall gain

in addition to the above, if S is less than the threshhold value, 0 is returned. the default value for threshhold is 0, meaning all change is reported, however small. increasing threshhold to a small value causes very small changes -- generally called noise -- to be ignored.

SAMPLE RATE

a major PID term not reflected in this code is the time series sample rate -- eg. how often you pass input to PID. it is assumed that this is a constant periodic rate, and it is a major tuning parameter. here i call this sample rate time T, units are milliseconds. SRTimer was designed in parallel with PID. the crude example above uses delay(), but that function is a ruinously bad thing to use in any real-time code; please refer to my other writing on this subject.

a brief mention of how SRTimer is used to invoke PID or any other time-based function is:

if (T.timer (0)) { int c= P.pid (analogRead (A0)); ... }

this technique supports any number of "simultaneous" events to run at once.

DEFAULT VALUES

the default settings for the PID function are:

P.outGain= 1; P.integGain= 1; // integrator gain times P.propGain= -1; // proportional gain == -1 P.diffGain= 1; P.threshhold = 1;

these defaults have the effect of driving the output (PID "error term") to zero when input is unchanging, regardless of the input value. this is nearly ideal for extracting information from electrical-proportional sensors (potentiometer or CdS photoresistor, etc).

for the example above, a CdS photoresistor attached to analog input A0 on an Arduino controller, global value S reflects the time-dependent behavior of the sensor. with no change in the light impinging on the CdS cell, the output, S, will settle to 0 in approximately (T * integrator size), or in this example, 10 mS rate * 10-deep integrator or approx. 0.1 sec.

the magnitude of the PID output value reflects the rate-of-change of the input value. the sign of the output is the same as the sign of the input.

INTERMEDIATE VALUES

two internal, intermediate terms of the calculation are broken out for access; it's occasionally useful to run inputs through PID, but extract the integral portion of that calculation.

int v= P.integral; // most recent integral calc, int v= P.differential; // most recent differential calc

Integrator

#include Integrator I; ... I.begin (unsigned N); // one time, allocate and init the integrator int x= ... // for example, the datum that will be integrated I.fill (x); // pre-fill the integrator's buffers ... int v= I.integ (int input); // integrate input, periodically

instantiates an Integrator object named I, with a buffer size of N. two methods are available. integ() is essentially an averager, but invoked with a timer it becomes a time series. it returns the average/integral of the most recent N input values.

the fill() method is used to initialize the integrator buffer with a value, eg. at startup.

Differentiator

#include Differentiator D; ... D.begin (unsigned N); // allocate history N deep D.fill (x); // fill history with x ... int v= D.diff (int input); D.clear();

instantiate a differentiator of the given size. the smallest value, 2, is surprisingly effective. beyond 8 is a waste of time and RAM. larger numbers tend to emphasize the effect of differences. diff(input) returns the sum of differences of the most recent N input values.

fill(x) installs value x into every slot in the differentiator's history. clear() fills history with 0.

Website contents, unless otherwise specified, © 2023 by Tom Jennings is licensed under CC BY-SA 4.0