22 Feb 2022 - tsp

Last update 22 Feb 2022

10 mins

The following article provides just a short overview of what a PID loop is and how to implement one on discrete devices with manual tuning.

A proportional-integral-derivative (PID) loop is a control loop mechanism that consists of three parallel components - namely the following parts:

- A
*proportional*(P) term - An
*integral*(I) term - A
*differential*term

Note that the control loop might also contain a process, measurement and optional inverse process model. The basic idea is that one supplies a target $\vec{x}(t)$ value that a system should ideally evolve into or stay near at. This might be a simple setting like a temperature or angular position or a more complex state vector like drone or vehicle velocity, a magnetic field, etc.

The value $\vec{e}(t)$ denotes the error deviation between the requested target size $\vec{x}(t)$ and the current measures values - or the state estimated by the measurement ($\vec{w}(t)$). All components of a PID loop directly depend on the error:

- The proportional part is - as the name implied - proportional to the error value: $K_p * \vec{e}(t)$
- The differential component $K_d * \partial_t \vec{e}(t)$ depends on the rate of change of the error signal.
- The integral component $K_i * \int \vec{e}(t) d\tau$ depends on the sum of previous error values - either over the entire lifetime or better a given time window.

The sum of these components is then passed to the system - that’s then measured again at the next time step. The measurement $\vec{q}(t)$ is optionally passed through an inverse system model and leads to the actual state $\vec{w}(t)$.

As one can see this can be described by the following set of equations:

[ e(t) = x(t) - w(t) ] [ y(t) = K_p * e(t) + K_d * \frac{\partial e(t)}{\partial t} + K_i * \int_{t-\delta t_I}^{t} e(\tau) d\tau ]In discretized form the differential is just a subtraction (and optionally a division - but one can absorb that into the factor $K_d$) as well as a simple sum (again the multiplication with the time span of a proper integral can be absorbed into $K_i$ again)

[ y(t_i) = K_p * e(t_i) + K_d * \frac{e(t - \delta t_D) - e(t)}{\delta t_D} + K_i * \sum_{\tau = t - \delta t_I}^{t} e(\tau) ]When one wants to use a PID regulator for temperature control one can assume that the delivered power into heating elements is usually linear dependent with the PWM frequency and thus the output of the regulator is proportional to the missing power that should be pushed into the heated volume - since the temperature again is mostly linearly dependent on the electrical power. Of course the system also encounters losses (conduction, radiation, etc.) but that can to some extend be ignored and modeled with the integral term.

So what do the terms really do?

- $y(t)$ is the output of the regulator - in case of temperature regulation this
is the duty cycle and thus should be somehow clamped to the desired range. This
is done by simply limiting the value and of course also by tuning the loop
coefficients. This is most of the time also called
*control value*. - $e(t)$ is the deviation between the target value /
*set value*$x(t)$ and the current measured value $w(t)$ (*process value*). - $\frac{\partial e(t)}{\partial t}$ is the derivative that’s usually measured in discrete fashion and approximated as $\frac{e(t - \delta t_d) - e(t)}{\delta t_d}$
- $\int_{t - \delta t_I}^{t} e(\tau) d \tau$ is the sum of all previous error deviations inside a sliding time window of length $\delta t_I$
- $\delta t_{D}$ is the time window used for discrete differentiation. This is the basic time step size.
- $\delta t_{I}$ is the integration time window in multiples of $\delta t_{diff}$.

The whole control loop is characterized by the three coefficients - here I’ve shown them independent but sometimes they’re also specified in a dependent way so one can really call $K_p$ the gain.

- $K_p$ is the proportional factor. This drives the system into the target direction
with more force the farther it’s away from the target value. It usually pushes into
the target direction. Sometimes called
*gain*. - $K_d$ is an differential term. If you try to simply build a proportional regulator
you will see that this starts to oscillate - first it overshoots, then it undershoots
again. The differential term is proportional to the change of the error deviation
and thus dampens oscillations and slows down the whole process. This usually
pushes away from the target direction to dampen the proportional factor. Sometimes
it’s called
*preact*. - $K_i$ is the integral term. This is a term that sums up systematic error such
as constant averaged losses or differences between measurement system and
reality. This term also usually points against the direction of the proportional
term to compensate for any systematic differences. Limiting the integration
window $\delta t$ is required to be able to react to dynamic changes. Sometimes
called
*reset*.

The tuning of the PID parameters $K_p$, $K_d$ and $K_i$ is of course crucial and
the hardest task. Note that there is no *correct* answer on how to determine
these gains, there is a variety of procedures. This is because the targets of
being responsive and minimizing overshoot are in contradiction to each other.

The most simple and basic tuning procedure is the following:

- Take note if your system is changing fast (motor speed, attack angle of a plane, etc.) or slow (temperature, steering a large ship (you’d use something different than a PID regulator, won’t you?), etc.). Fast systems usually require small gain $K_p$ and high reset $K_i$, slow systems the other way round high gain $K_p$ and low reset $K_i$
- Then first set $K_i = 0$ and $K_d = 0$, so build a simple proportional regulator first.
- Increase $K_p$ by doubling till the system starts to oscillate. Then half $K_p$
- Start with a small integral $K_i$ term. Double till it oscillates, then half again.

Up until now this is a process often seen in industry - one then often just stops with a PI regulator. In case one cannot tolerate overshoot one might then also tune the differential part:

- Then apply the damping term $K_d$ to suppress any remaining oscillations and reduce overshoot again.

Note that you cannot use this for stuff where you cannot do experimentation - and in case you have multiple processes depending on each other you really have to do Eigenmode analysis to prevent loops fighting each other (this requires at least some kind of working process model and usually is done using numerical analysis).

Why use a such complicated system? In the following simulations I’ve simulated a simple 100W heater in a small volume, a control signal from 0 to 100 percent duty cycle and included some loss (always 2 percent of the energy contained in the system will be lost due to bad thermal isolation). I’ve also superimposed some external random fluctuations that will come in handy later on when looking at diverging scenarios (if they’re going to happen). The start temperature is 0 Celsius, the target temperature 300 Celsius. I’ve modeled the slow increase in temperature just by modeling the sensor on one end of the volume and the heater on the other - which is basically a time delay.

So first having a way too fast proportional term for the given time delay and no integral and differential term one nicely sees the oscillations:

Reducing the gain by a huge factor one already sees the damping factor. Why is this when I previously said this is what the differential term does? Since I’ve modeled the energy loss of the temperature in an proportional way (not physically correct) I’ve got a differential damping term by the simulated system.

Further reducing the gain now shows massive overshoot, massive undershoot and then a dominant fluctuation by the simulated noise:

Reducing the gain too far delivers not enough power to reach the target temperature any more:

Thus I choose a gain that overshoots the target and then start with the integral part. Choosing a way too large integral term also leads to massive overshoot and oscillations:

Reducing the gain on the integral term now allows one to find already configurations that converge - the oscillations are still damped by the differential in the model and not by the regulator though:

Playing around with integral and proportional terms allows already to find a sufficient nice PI regulator for the given problem:

In case the system itself does not contain any terms that dampen oscillations it might be a good idea to introduce a damping term oneself - this can also be used to reduce the overshoot by rate-limiting.

The implementation of a PID controller basically is pretty simple. Each loop iteration requires a measurement value, a target value, the error value value and a finite history of error values for the integral term in addition to the three constants. A simple and naive implementation might look like the following (this is the code that has been used to generate the plots above).

The integral part is realized using a ringbuffer. It might be interesting to not initialize the ringbuffer using 0 as constant but the first measurement value so the integral term doesn’t produce garbage in the setup phase.

```
#define PID_INTEGRAL_SIZE 32
struct pidChannel {
double dTarget;
double dMeasuredValue;
double dErrorHistory[PID_INTEGRAL_SIZE]
double Kp;
double Ki;
double Kd;
unsigned long int dwErrorHistoryRB_Current;
double tDiff;
}
void pidInit(
struct pidChannel* lpChannel,
double Kp,
double Ki,
double Kd,
double tDiff
) {
unsigned long int i;
if(lpChannel == NULL) { return; }
lpChannel->dTarget = 0;
lpChannel->dMeasuredValue = 0;
for(i = 0; i < PID_INTEGRAL_SIZE; i=i+1) {
lpChannel->dErrorHistory[i] = 0;
}
lpChannel->Kp = Kp;
lpChannel->Ki = Ki;
lpChannel->Kd = Kd;
lpChannel->tDiff = tDiff;
lpChannel->dwErrorHistoryRB_Current = 0;
}
void pidSetMeasurement(
struct pidChannel* lpChannel,
double dNewMeasurement
) {
lpChannel->dMeasuredValue = dNewMeasurement;
}
void pidSetTarget(
struct pidChannel* lpChannel,
double dTarget
) {
lpChannel->dTarget = dTarget;
}
double pidUpdateOutput(struct pidChannel* lpChannel) {
unsigned long int i;
double e = lpChannel->dTarget - lpChannel->dMeasuredValue;
lpChannel->dErrorHistory[lpChannel->dwErrorHistoryRB_Current] = e;
lpChannel->dwErrorHistoryRB_Current = (lpChannel->dwErrorHistoryRB_Current + 1) % PID_INTEGRAL_SIZE;
double eLast = lpChannel->dErrorHistory[(lpChannel->dwErrorHistoryRB_Current + PID_INTEGRAL_SIZE - 1) % PID_INTEGRAL_SIZE];
double yProp = lpChannel->Kp * e;
double yDiff = lpChannel->Kd * (eLast - e) / lpChannel->tDiff;
double yInt = 0;
for(i = 0; i < PID_INTEGRAL_SIZE; i=i+1) {
yInt = yInt + lpChannel->dErrorHistory[i];
}
yInt = yInt * lpChannel->tDiff * lpChannel->Ki;
return yProp + yDiff + yInt;
}
```

This article is tagged:

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://jugujbrirx3irwyx.onion/