PID control loop in a nutshell

22 Feb 2022 - tsp
Last update 22 Feb 2022
Reading time 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.

What is a PID control loop?

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

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.

Components of a PID control loop

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 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?

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.

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:

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:

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).

A simulated example (temperature controller)

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:

Oscillating proportional regulator

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.

Oscillating proportional regulator

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

Oscillating proportional regulator

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

Oscillating proportional regulator

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:

Oscillating proportional regulator

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:

Oscillating proportional regulator

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

Oscillating proportional regulator

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.

Implementation

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:


Data protection policy

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

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

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support