omnitiles/control/pid.rs
1// SPDX-License-Identifier: MIT
2// © 2025–2026 Christopher Liu
3
4//! Generic PID controller for closed-loop control.
5//!
6//! Works in `no_std` and does not allocate memory.
7
8/// PID controller with tunable gains and output clamping.
9pub struct Pid {
10 /// Proportional gain
11 kp: f32,
12 /// Integral gain
13 ki: f32,
14 /// Derivative gain
15 kd: f32,
16
17 /// Integrator state
18 integral: f32,
19 /// Last process variable (for derivative term)
20 prev_measurement: f32,
21
22 /// Output clamp
23 out_min: f32,
24 out_max: f32,
25
26 /// Integral anti-windup clamp
27 int_min: f32,
28 int_max: f32,
29
30 first_update: bool,
31}
32
33impl Pid {
34 /// Create a new PID controller.
35 ///
36 /// `kp`, `ki`, `kd` are the gain constants.
37 pub fn new(kp: f32, ki: f32, kd: f32) -> Self {
38 Self {
39 kp,
40 ki,
41 kd,
42
43 integral: 0.0,
44 prev_measurement: 0.0,
45
46 out_min: -1.0,
47 out_max: 1.0,
48
49 int_min: -1.0,
50 int_max: 1.0,
51
52 first_update: true,
53 }
54 }
55
56 /// Set output limits.
57 pub fn with_output_limits(mut self, min: f32, max: f32) -> Self {
58 self.out_min = min;
59 self.out_max = max;
60 self
61 }
62
63 /// Set integral limits for anti-windup.
64 pub fn with_integral_limits(mut self, min: f32, max: f32) -> Self {
65 self.int_min = min;
66 self.int_max = max;
67 self
68 }
69
70 /// Reset integrator + derivative history.
71 pub fn reset(&mut self) {
72 self.integral = 0.0;
73 self.prev_measurement = 0.0;
74 self.first_update = true;
75 }
76
77 /// Update the controller.
78 ///
79 /// `setpoint` — desired value
80 /// `measurement` — current value
81 /// `dt` — timestep in seconds (e.g. 0.02 for 50 Hz control loop)
82 ///
83 /// Returns a normalized command in [`out_min`, `out_max`] which can be mapped to motor drive.
84 pub fn update(&mut self, setpoint: f32, measurement: f32, dt: f32) -> f32 {
85 let error = setpoint - measurement;
86
87 // ----- P term -----
88 let p = self.kp * error;
89
90 // ----- I term -----
91 self.integral += error * dt * self.ki;
92
93 // Anti-windup clamp
94 if self.integral > self.int_max {
95 self.integral = self.int_max;
96 }
97 if self.integral < self.int_min {
98 self.integral = self.int_min;
99 }
100
101 let i = self.integral;
102
103 // ----- D term (on measurement to reduce noise sensitivity) -----
104 let d = if self.first_update {
105 self.first_update = false;
106 0.0
107 } else {
108 let dv = self.prev_measurement - measurement;
109 self.kd * (dv / dt)
110 };
111 self.prev_measurement = measurement;
112
113 // ----- Output clamp -----
114 let mut out = p + i + d;
115 if out > self.out_max {
116 out = self.out_max;
117 }
118 if out < self.out_min {
119 out = self.out_min;
120 }
121
122 out
123 }
124}