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.