# Different PI controller implementations and their respective discrete transfer functions

So I need to implement a PI-controller and I found an Implementation of an PID-controller with some background explanation. I adapted the implementation to an PI-controller, implemented it and got the desired behavior.

I then proceed to read up on the topic in a text book (which is not publicly available, but I will provide the necessary math) and the approach show there is different in some point and results in a different Implementation.

First controller, shorted Version, for long version Source

In this example, the PID and my PI controller respectively are split up in the two (three) parts proportional integral and derivative, which will be ignored from here on.

To discretize the Integral part, the forward Euler method is used and the transfer function of the Integral Part is presented as:

$$F_i(s) = \frac{X_a(s)}{X_e(s)} = K_i \cdot \frac{1}{s}$$

$$D_i(z) = F_i(s) \bigg \vert _{\frac{z-1}{T_s}} = \frac{K_i T_s}{z-1} = \frac{K_I \cdot T_s \cdot z^{-1}}{1-z^{-^1}}$$

It is then further written as a difference equation

$$X_{ai}(z) = z^{-1} X_{ai}(z) + z^{-1}K_i Ts X_{e}(z)$$

It is not directly stated, but the logical consequence of the following implementation is that the difference equation of the PI-controller results in:

$$X_{aPI} = z^{-1} X_{ai}(z) + z^{-1}K_i Ts X_{e}(z) + X_e(z) K_p$$

For the implementation in c/c++ I will only focus on the relevant update function.

float PI_Controller::update(float input)
{
// The error is the difference between the reference (setpoint) and the
// actual position (input)
// error = X_e
int16_t error = setpoint - input;
// The integral or sum of current and previous errors
int32_t newIntegral = integral + error;

// Standard PI rule
float output = kp * error + ki * Ts * integral;

// Clamp and anti-windup
if (output > maxOutput)
output = maxOutput;
else if (output < minOutput)
output = minOutput;
else
integral = newIntegral;

return output; // X_aPI
}



The first thing the textbook does different is treating the transfer function of the PI-controller as a whole. It's also using the forward Euler method.

$$F_{pi}(s) = Kp + \frac{K_i}{s}$$

$$D(z) = F_{pi}(s) \bigg \vert _{s= \frac{z-1}{T}} = K_p + \frac{K_i T_s z^{-1}}{1-z^{-1} } = \frac{K_p - K_p z^{-1} + K_i T_s z^{-1} }{1- z^{-1}}$$

As a result the difference equation is also different and results in:

$$X_a(z) = X_a(z)z^{-1} + K_p X_e(z) - K_p X_e(z) z^{-1} + K_i Ts X_e(z) z^{-1}$$ simplified $$X_a(z) = X_a(z)z^{-1} + K_p X_e(z) +z^{-1} X_e(z)( K_i Ts - K_p)$$

The implementation thus will also be different.

float PI_Controller::update(float input)
{
// error = X_e
int16_t error = setpoint - input;
int32_t newIntegral = integral + error;

output += kp * error + integral * ki * kp * ts;

// Clamp and anti-windup
if (output > maxOutput)
output = maxOutput;
else if (output < minOutput)
output = minOutput;
else
integral = newIntegral;

return output;
}



My Questions are:

Why are these approaches and their resulting transfer functions different?

Is one or are both approaches wrong, what would be the or a correct one and its implementation?

Why do both controllers work nonetheless? (I tested the first one myself, and the book also provides graphs, which leads me to believe the approach is correct as well)

Are they actually the same, and I just don't see it?

I would be really grateful for an answer which also sheds some light on the backgrounds. I know that the bilinear transform is superior, but I really would like to understand it using the forward Euler method.

• The C code of the second one doesn't look correct to me. Or, rather, it looks like it has a double "integration" (summation) action which does a different thing from the first.
– Peter K.
Commented Sep 13, 2022 at 0:56
• Thank you for pointing that out, I will try to correct it. However, I'm not quite sure if its really wrong because the math leads me to this result but i don't spot a mistake there Commented Sep 13, 2022 at 13:40

On the 2nd approach, your transfer function looks fine but translating it into C code appears wrong. To re-write your z-domain as a discrete finite difference equation where $$y$$ is the output and $$a=K_p$$ , $$b=K_i T_s$$ we have

$$y[n] = y[n-1] + a\cdot e[n] + b\cdot e[n-1] - a\cdot e[n-1]$$

or

$$y[n] = y[n-1] + a\cdot (e[n]-e[n-1]) + b\cdot e[n-1]$$

Where if you inspect it, you'll find that the $$-a\cdot e[n-1]$$ term will always cancel the same term in the $$y[n-1]$$. We can check this by stepping through:

$$y[0] = a \cdot e[0]$$

$$y[1] = a\cdot e[0] + a\cdot (e[1]-e[0]) + b\cdot e[0] = a\cdot e[1] + b\cdot e[0]$$

$$y[2] = a\cdot e[1] + b\cdot e[0] + a\cdot (e[2]-e[1]) + b\cdot e[1] = a\cdot e[2] + b\cdot (e[1] + e[0])$$

So pretty clearly we can see the output will consist of the proportional gain $$a$$ times the current error plus the integral gain $$b$$ times the summation of the previous errors, and this makes sense for the PI controller.

This line in your c code is not doing this

output += kp * error + integral * (ki*Ts*Kp);

You would need to do something like if you really want to keep it as a summation:

output += kp*error - kp*errorprev + integral*(ki*Ts)

But really - as alluded to by Peter K n the comments, ths extra summation is unnecessary and you should just do:

output = kp*error + integral*(ki*Ts)

which ends up being identical to the form in the first approach.

• Thank you for this detailed answer, it clarified a lot for me! Commented Sep 14, 2022 at 10:21