10 Digital Implementation of PID Controllers

10.1 Theory

In this book we work primarily with continuous time systems and continuous time controllers. However, almost all modern control strategies are implemented digitally on a micro-controller or a micro-processor, typically implemented using C-code. In this chapter, we describe how PID controllers can be implemented in a sampled data system using computer code.

A general PID control signal is given by \[u(t) = k_P e(t) + k_I\int_{-\infty}^t e(\tau)d\tau + k_D \frac{de}{dt}(t),\] where \(e(t)=r(t)-y(t)\) is the error between the desired reference output \(r(t)\) and the current output \(y(t)\). Define \(u_P(t) = e(t)\), \(u_I(t) = \int_{-\infty}^t e(\tau)d\tau\), and \(u_D(t) = \frac{de}{dt}(t)\). The PID controller is therefore given by \(u(t) = k_p u_P(t) + k_I u_I(t) + k_D u_D(t)\). In the following paragraphs we will derive discrete sampled-time equivalents for \(u_P(t)\), \(u_I(t)\), and \(u_D(t)\).

First consider a sampled time implementation of \(u_P(t)\). When \(t=nT_s\), where \(T_s\) is the sample period and \(n\) is the sample time, we have \(u_P(nT_s) = e(nT_s)\), or using discrete time notation \[u_P[n] = e[n].\]

To derive the sample-time implementation of an integrator, note that \[\begin{aligned} u_I(nT_s) &= \int_{-\infty}^{nT_s} e(\tau) d\tau \\ &= \int_{-\infty}^{(n-1)T_s} e(\tau)d\tau + \int_{(n-1)T_s}^{nT_s} e(\tau) d\tau \\ &= u_I((n-1)T_s) + \int_{(n-1)T_s}^{nT_s} e(\tau) d\tau. \end{aligned}\] As shown in Fig. 10.1, a trapezoidal approximation of the integral between \((n-1)T_s\) and \(nT_s\) is given by \[\int_{(n-1)T_s}^{n T_s} e(\tau) d\tau \approx \frac{T_s}{2}\left(e(nT_s)+e((n-1)T_s)\right).\]

Trapezoidal rule for digital integration.

Therefore \[u_I(nT_s) \approx u_I((n-1)T_s) + \frac{T_s}{2}\left(e(nT_s)+e((n-1)T_s)\right).\] Taking the z-transform we get \[U_I(z) = z^{-1}U_I(z) + \frac{T_s}{2}(E(z)+z^{-1}E(z)).\] Solving for \(U_I(z)\) gives \[\tag{10.1} U_I(z) = \frac{T_s}{2} \left(\frac{1+z^{-1}}{1-z^{-1}}\right)E(z).\] Since in the Laplace domain we have \[U_I(s) = \frac{1}{s}E(s),\] we have derived the so-called Tustin approximation where \(s\) in the Laplace domain is replaced with the \(z\)-transform approximation \[\tag{10.2} s \mapsto \frac{2}{T_s}\left(\frac{1-z^{-1}}{1+z^{-1}}\right),\] where \(T_s\) is the sample period [18]. Taking the inverse \(z\)-transform of Equation 10.1 gives the discrete time implementation of an integrator as \[\tag{10.3} u_I[n] = u_I[n-1] + \frac{T_s}{2}\left(e[n] + e[n-1]\right).\]

In the Laplace domain, a pure differentiator is given by \(U_D(s) = s E(s)\). However, since a pure differentiator is not causal, the standard approach is to use the band-limited, or dirty derivative \[U_D(s) = \frac{s}{\sigma s + 1}E(s),\] where \(\sigma\) is small, and \(\frac{1}{\sigma}\) defines the bandwidth of the differentiator. In practical terms, the dirty derivative differentiates signals with frequency content less than \(\frac{1}{\sigma}\) radians per second. Using the Tustin approximation 10.2, the dirty derivative in the \(z\)-domain becomes \[\begin{aligned} U_D(z) &= \frac{\frac{2}{T_s}\left(\frac{1-z^{-1}}{1+z^{-1}}\right)}{\frac{2\sigma}{T_s}\left(\frac{1-z^{-1}}{1+z^{-1}}\right)+1}E(z) = \frac{\left(\frac{2}{2\sigma+T_s}\right)(1-z^{-1})}{1-\left(\frac{2\sigma-T_s}{2\sigma+T_s}\right)z^{-1}}E(z). \end{aligned}\] Transforming to the time domain, we have \[\tag{10.4} u_D[n] = \left(\frac{2\sigma-T_s}{2\sigma+T_s}\right) u_D[n-1] + \left(\frac{2}{2\sigma+T_s}\right)\left(e[n]-e[n-1]\right).\]

Interactive Figure 10.I1

Dirty derivative noise filtering

A pure finite-difference derivative reacts strongly to measurement noise. The dirty derivative keeps the derivative causal and band-limited; increasing sigma lowers the differentiator bandwidth and smooths the derivative estimate.

Interactive Figure 10.I1. The upper plot shows the noisy input signal v(t) and the true signal before measurement noise is added. The lower plot compares the true derivative with the raw finite-difference derivative and the dirty derivative. Use sigma to see the tradeoff between noise rejection and lag.

Interpretation

Bandwidth proxy: 1/sigma = rad/s; filter coefficient beta =

10.1.1 Integrator Anti-Windup

A potential problem with a straightforward implementation of PID controllers is integrator windup. When the error \(r-y\) is large and persists for an extended period, the value of the integrator can become large, or “wind up.” A large integrator will cause \(u\) to saturate, which will cause the system to push with maximum effort in the direction needed to correct the error. Since the value of the integrator will continue to wind up until the error signal changes sign, the control signal may not come out of saturation until well after the error has changed sign, which can cause a large overshoot and may destabilize the system.

Since integrator windup can destabilize the system, it is important to add an anti-windup scheme. A number of different anti-windup schemes are possible. In this section we will discuss two schemes that are commonly used in practice. The first scheme is based on the fact that integrators are typically used to correct for steady-state error and the fact that overshoot is caused by integrating error during the transients of the step response. Therefore, the integrator can be turned off when the derivative of the error is large. This anti-windup scheme is implemented by an if-statement conditioned on the size of \(\dot{e}\).

The second anti-windup scheme is to subtract from the integrator exactly the amount needed to keep \(u\) at the saturation bound. In particular, let \[u_{\text{unsat}}=k_P e + k_D u_D + k_I u_I\] denote the unsaturated control value before applying the anti-windup scheme. After the saturation block we get \[u = \text{sat}(u_{\text{unsat}}) = \text{sat}(k_P e + k_D u_D + k_I u_I).\] The idea of anti-windup is to modify the output of the integrator from \(u_I\) to \(u_I^{+}\) so that \[k_P e + k_D u_D + k_I u_I^{+} = \text{sat}(k_P e + k_D u_D + k_I u_I) = u.\] Subtracting \(u_{\text{unsat}}\) from \(u\) gives \[\begin{aligned} u-u_{\text{unsat}} &= (k_P e + k_D u_D + k_I u_I^{+})-(k_P e + k_D u_D + k_I u_I) \\ &= k_I(u_I^{+}-u_I). \end{aligned}\] Solving for \(u_I^{+}\) gives \[u_I^{+} = u_I + \frac{1}{k_I}(u-u_{\text{unsat}}),\] which is the updated value of the integrator that ensures that the control signal remains just out of saturation. Note that if the control value is not in saturation then \(u=u_{\text{unsat}}\) and the integrator value remains unchanged.

Interactive Figure 10.I2

Anti-windup response under actuator saturation

Compare a saturated PI controller with and without anti-windup on an intentionally underdamped second-order plant. The dashed traces show the no-anti-windup baseline; the colored traces show the selected controller. Use the actuator limit and integral gain to make windup more or less severe.

Interactive Figure 10.I2. The top plot shows the step response, the middle plot shows uunsat and usat, and the bottom plot shows the integrator state. Without anti-windup, the controller keeps integrating error while the actuator is saturated, so uunsat stays far beyond the actuator limit and the stored integrator drives a large overshoot. The axes rescale so this growth remains visible.

Overshoot
Time in saturation
Final error
Peak integrator state

Interpretation: windup is visible when uunsat remains far outside the actuator limit and the integrator state keeps growing. Anti-windup changes the stored integrator state, so the controller leaves saturation sooner and overshoots less.

10.1.2 Implementation

Python code that implements a general class for a PID loop is shown below.

import numpy as np

class PIDControl:
    def __init__(self, kp, ki, kd, limit, sigma, Ts, flag=True):
        self.kp = kp  # Proportional control gain
        self.ki = ki  # Integral control gain
        self.kd = kd  # Derivative control gain
        self.limit = limit  # The output saturates at this limit
        self.sigma = sigma  # dirty derivative bandwidth is 1/sigma
        self.beta = (2.0*sigma-Ts)/(2.0*sigma+Ts)
        self.Ts = Ts          # sample rate
        self.flag = flag
        # if flag == True, then returns
        #    u = kp * error + ki * integral(error) + kd * error_dot.
        # else returns
        # u = kp * error + ki * integral(error) - kd * y_dot.
        self.y_dot = 0.0  # estimated derivative of y
        self.y_d1 = 0.0  # Signal y delayed by one sample
        self.error_dot = 0.0  # estimated derivative of error
        self.error_d1 = 0.0  # Error delayed by one sample
        self.integrator = 0.0  # integrator

    def PID(self, r, y):
        # Compute the current error
        error = r - y
        # integrate error using trapazoidal rule
        self.integrator = self.integrator 
            + (self.Ts/2) * (error + self.error_d1)
        # PID Control
        if flag is True:
            # differentiate error
            self.error_dot = self.beta * self.error_dot 
                + (1-self.beta)/self.Ts * (error - self.error_d1)
            # PID control
            u_unsat = self.kp*error 
                      + self.ki*self.integrator 
                      + self.kd*self.error_dot
        else:
            # differentiate y
            self.y_dot = self.beta * self.y_dot 
                + (1-self.beta)/self.Ts * (y - self.y_d1)
            # PID control
            u_unsat = self.kp*error 
                      + self.ki*self.integrator 
                      - self.kd*self.y_dot
        # return saturated control signal
        u_sat = self.saturate(u_unsat)
        # integrator anti - windup
        if self.ki != 0.0:
            self.integrator = self.integrator 
                + self.Ts / self.ki * (u_sat - u_unsat)
        # update delayed variables
        self.error_d1 = error
        self.y_d1 = y
        return u_sat

    def PD(self, r, y):
        # Compute the current error
        error = r - y
        # PD Control
        if flag is True:
            # differentiate error
            self.error_dot = self.beta * self.error_dot 
                + (1-self.beta)/self.Ts * (error - self.error_d1)
            # PD control
            u_unsat = self.kp*error 
                      + self.kd*self.error_dot
        else:
            # differentiate y
            self.y_dot = self.beta * self.y_dot 
                + (1-self.beta)/self.Ts * (y - self.y_d1)
            # PD control
            u_unsat = self.kp*error 
                      - self.kd*self.y_dot
        # return saturated control signal
        u_sat = self.saturate(u_unsat)
        # update delayed variables
        self.error_d1 = error
        self.y_d1 = y
        return u_sat

    def saturate(self,u):
        if abs(u) > self.limit:
            u = self.limit*np.sign(u)
        return u



The PID controller class is initialized on lines 4–21, where kp, ki, and kd are the control gains, limit is the saturation limit for the control signal, sigma is the dirty derivative gain, Ts is the sample rate, and flag is a flag that indicates whether the controller differentiates the error or the output. The internal memory used to implement the differentiators and integrators is initialized to zero in lines 17–21, where y_d1 is the signal \(y\) delayed by one sample and error_d1 is the error delayed by one sample. PID control is implemented in lines 23–55 and PD control is implemented in lines 57–80. The integrator update is shown in lines 50–51.

10.1.3 Gain Selection for PID control

Gain selection for PID control usually proceeds in the following manner. The proportional and derivative gains \(k_P\) and \(k_D\) are first selected according to the methods discussed in Chapter 11. The integral gain \(k_I\) is then tuned by starting with zero and then slowly increasing the gain until the steady-state error is removed. The parameters associated with the anti-windup scheme, especially \(\bar{v}\), are also adjusted during this phase. The proportional and derivative gains can then be adjusted if necessary by retuning \(\omega_n\) and \(\zeta\). In the case of successive loop closure, integrators are only added on the outer loop.

The addition of an integrator has a destabilizing effect on the feedback system. This effect is most easily understood by using the root locus method described in Appendix 30, where it can be seen that for second-order systems, a large \(k_I\) will always result in closed-loop poles in the right half-plane. The root locus can also be used to aid in the selection of the integral gain \(k_I\), where the value of \(k_I\) is selected so that integrator has minimal impact on the closed-loop poles designed using PD control. A more effective method for selecting integral gains will be discussed in Chapter 15.

10.2.1 image Example Problem 3.1.13

The objective of this problem is to implement the PID controller using only measured outputs of the system.

(a) Modify the system dynamics file so that the parameters vary by up to 20% each time they are run (uncertainty parameter = 0.2).

(b) Change the simulation files so that the input to the controller is the output and not the state. The controller should only assume knowledge of the angle \(\theta\) and the reference angle \(r_\theta\).

(c) Implement the PID controller designed in Problems 3.1.11. Use the dirty derivative gain of \(\sigma=0.05\). Tune the integrator to remove the steady-state error caused by the uncertain parameters.

Design Study A guided exercise

Assemble the digital PID controller

Identify the implementation pieces that belong in the sampled controller class.

Step 1. Available feedback signal

Step 2. Discrete implementation choices

Step 3. Why simulate with uncertainty?

Pseudocode

The complete solution includes PID implementation code. Before opening it, outline how you would update the integrator, compute the dirty derivative, apply anti-windup, and return the control torque.

This prompt is ungraded. It is intended to connect the digital PID concepts to the implementation code in the complete solution.

10.3 Design Study B. Inverted Pendulum

10.3.1 image Example Problem 3.2.13

The objective of this problem is to implement the PID controller using only measured outputs of the system.

(a) Modify the system dynamics file so that the parameters \(m_1\), \(m_2\), \(\ell\) and \(b\) vary by up to 20% of their nominal value each time they are run (uncertainty parameter = 0.2).

(b) Change the simulation files so that the input to the controller is the output and not the state. The controller should only assume knowledge of the position \(z\) and the angle \(\theta\), as well as the reference position \(r_z\). Track a square wave with amplitude of 0.5 meters and a frequency of 0.04 Hz.

(c) Implement the nested PID loops designed in Problems 3.2.11. Use the dirty derivative gain of \(\sigma=0.05\). Tune the integrator to remove the steady-state error caused by the uncertain parameters.

Design Study B guided exercise

Prepare the nested digital PID implementation

Identify measured signals, dirty derivatives, uncertainty, and integral tuning.

Step 1. Available controller inputs

Step 2. Derivative implementation

Step 3. Robustness check

Pseudocode

The complete solution includes a nested PID controller class and a zero-canceling filter class. Before opening it, outline each component separately.

These prompts are ungraded. They are intended to connect the derivation to the implementation code in the complete solution.

10.4 Design Study C. Satellite Attitude Control

10.4.1 image Example Problem 3.3.13

The objective of this problem is to implement the PID controller using only measured outputs of the system.

(a) Modify the system dynamics file so that the parameters \(J_s\), \(J_p\), \(k\) and \(b\) vary by up to 20% of their nominal value each time they are run (uncertainty parameter = 0.2).

(b) Change the simulation files so that the input to the controller is the output and not the state. Assume that the controller only has knowledge of the angles \(\phi\) and \(\theta\) as well as the reference angle \(r_\phi\).

(c) Implement the nested PID loops designed in Problems 3.3.11. Use the dirty derivative gain of \(\sigma=0.05\). Tune the integrator to remove the steady-state error caused by the uncertain parameters.

Design Study C guided exercise

Plan the measured-output satellite PID implementation

This implementation uses uncertain plant parameters and measured angles rather than full state feedback.

Step 1. Plant uncertainty

Step 2. Measured signals

Step 3. Integrator purpose

Pseudocode

10.5 Interactive Implementation Check

Chapter 10 activity

Sample-time and discrete PID terms

Vary the sample time and consecutive error samples. The readouts show the trapezoidal integrator increment, a finite-difference derivative, and the dirty-derivative coefficient.

Integrator increment Finite difference Dirty derivative beta Sample-time effect

Coding-lab preparation: a discrete PID controller must store the previous error, previous measurement, differentiator state, and integrator state. Without that memory, the trapezoidal integral, derivative estimate, and anti-windup correction cannot be implemented correctly.

10.6 Coding Lab: Discrete PID Implementation

Objective: implement a sampled-data PID controller using the Chapter 10 update equations.

Evaluation criteria: the lab checks proportional action, trapezoidal integration, derivative-on-output, saturation, anti-windup behavior, and step-response tracking.

Use the code editor to complete the controller class, then use the automatic checks and plots to debug the implementation.

Figure 10.1 (Interactive). Discrete PID coding lab with automatic implementation checks.

10.7 Knowledge Check

Q1. Controller update timing

A digital PID controller updates at:

Q2. Saturation effect

Actuator saturation can cause:

Q3. Trapezoidal integral increment

Compute the trapezoidal integrator increment for these samples.

  • Ts = 0.1
  • ek = 1.0
  • ek-1 = 0.6

Show solution

Answer: 0.08 because 0.1(1.0 + 0.6)/2 = 0.08.

Q4. Finite-difference derivative

Compute the finite-difference derivative estimate for these samples.

  • Ts = 0.1
  • ek = 1.0
  • ek-1 = 0.6

Show solution

Answer: 4 because (1.0 - 0.6)/0.1 = 4.

Q5. Implementation memory

What must a digital PID controller keep between sample updates to compute integrals and derivatives?

Show solution

Answer: Stored state, memory, or previous samples. The controller needs delayed error/measurement values plus integrator and differentiator state.

Q6. Dirty derivative purpose

Why is a dirty derivative used instead of an ideal derivative in a digital PID implementation?

Q7. Integral action role

What is the main purpose of the integral term in a PID controller?

Q8. Saturation and stored integral state

If an actuator saturates while the integral state continues to grow, what problem is the controller experiencing?

10.8 Notes and References

A standard reference for digital implementation of PID controllers is [18]. Simple anti-wind-up schemes are discussed in [4, 18].

10.9 Notes and References

A standard reference for digital implementation of PID controllers is [18]. Simple anti-wind-up schemes are discussed in [4, 18].