control/pid.rs
1use num_traits::float::FloatCore;
2
3/// Implementation of a proportional–integral–derivative controller.
4pub struct PID<T> {
5 /// Desired setpoint.
6 pub setpoint: T,
7
8 /// Error value from the previous update.
9 error: T,
10 /// Integral value from the previous update.
11 integral: T,
12 /// Derivative value from the previous update.
13 derivative: T,
14 /// Previous measurement value.
15 measurement: T,
16
17 /// Proportional coefficient.
18 p: T,
19 /// Integral coefficient.
20 i: T,
21 /// Derivative coefficient.
22 d: T,
23 /// Coefficient for the derivative low-pass filter.
24 t: T,
25
26 /// Lower bound of the integral term.
27 imin: T,
28 /// Upper bound of the integral term.
29 imax: T,
30
31 /// Lower bound of the controller output.
32 omin: T,
33 /// Upper bound of the controller output.
34 omax: T,
35}
36
37impl<T: FloatCore> PID<T> {
38 /// Creates a new `PID` with a proportional gain of `kp`, integral gain of `ki`
39 /// and derivative gain of `kd`.
40 ///
41 /// `tau` represents the time constant of the derivative low-pass filter.
42 ///
43 /// `sampling_time` is the time difference in seconds between two consecutive
44 /// step operations.
45 ///
46 /// # Examples
47 ///
48 /// ```
49 ///
50 /// ```
51 pub fn new(kp: T, ki: T, kd: T, tau: T, sampling_time: T, setpoint: T) -> Self {
52 let two = T::from(2.0_f32).expect("Unable to cast from 2.0");
53 let half = T::from(0.5_f32).expect("Unable to cast from 0.5");
54
55 Self {
56 setpoint,
57
58 error: T::zero(),
59 integral: T::zero(),
60 derivative: T::zero(),
61 measurement: T::zero(),
62
63 p: kp,
64 i: half * ki * sampling_time,
65 d: -two * kd,
66 t: (two * tau - sampling_time) / (two * tau + sampling_time),
67
68 imin: T::neg_infinity(),
69 imax: T::infinity(),
70
71 omin: T::neg_infinity(),
72 omax: T::infinity(),
73 }
74 }
75
76 /// Indicates that the integral term should be restricted to a certain interval.
77 /// Useful to prevent [integral windup].
78 ///
79 /// [integral windup]: https://en.wikipedia.org/wiki/PID_controller#Integral_windup
80 ///
81 /// # Panics
82 ///
83 /// Panics if `min` > `max`.
84 ///
85 /// # Examples
86 ///
87 /// ```
88 ///
89 /// ```
90 pub fn bound_integral(&mut self, min: T, max: T) -> &mut Self {
91 assert!(min <= max);
92 self.imin = min;
93 self.imax = max;
94 self
95 }
96
97 /// Indicates that the controller output should be restricted to a certain interval.
98 ///
99 /// # Panics
100 ///
101 /// Panics if `min` > `max`.
102 ///
103 /// # Examples
104 ///
105 /// ```
106 ///
107 /// ```
108 pub fn bound_output(&mut self, min: T, max: T) -> &mut Self {
109 assert!(min <= max);
110 self.omin = min;
111 self.omax = max;
112 self
113 }
114
115 /// Performs a single step of the control loop. It should be called exactly
116 /// once every `sampling_time` seconds.
117 pub fn step(&mut self, measurement: T) -> T {
118 let error = self.setpoint - measurement;
119
120 let proportional = self.p * error;
121 // Calculate integral term and clamp it to prevent windup.
122 let integral = self.i * (error + self.error) + self.integral;
123 self.integral = num_traits::clamp(integral, self.imin, self.imax);
124 // Derivative on measurement to prevent a kick during setpoint changes.
125 self.derivative = self.d * (measurement - self.measurement) + self.t * self.derivative;
126
127 let output = proportional + self.integral + self.derivative;
128 num_traits::clamp(output, self.omin, self.omax)
129 }
130}