free_flight_stabilization/stabilizer/
angle2.rs

1// src/stabilizer/angle2.rs
2
3//! # Angle2 Cascade PID Flight Stabilization Controller
4//!
5//! This is a angle and rate-based cascade syle PID flight control.
6//! Note that yaw relies on rate-based stabilization.
7//! It requires three configuration files- one of the angle-based PID,
8//! one for the rate-based PID, and one for the blending.
9
10use crate::pid::{compute_cascade_angle, compute_rate, CascadeAngleControlData, RateControlData};
11use crate::{CascadeBlendingConfig, FlightStabilizer, FlightStabilizerConfig, Number};
12use piddiy::PidController;
13
14/// Struct representing the Angle2 PID Flight Stabilization Controller.
15/// This is a cascade PID controller that combines angle and rate.
16pub struct Angle2Stabilizer<T: Number> {
17    angle_roll_pid: PidController<T, CascadeAngleControlData<T>>,
18    angle_pitch_pid: PidController<T, CascadeAngleControlData<T>>,
19    angle_i_limit: T,
20    angle_scale: T,
21    rate_roll_pid: PidController<T, RateControlData<T>>,
22    rate_pitch_pid: PidController<T, RateControlData<T>>,
23    rate_yaw_pid: PidController<T, RateControlData<T>>,
24    rate_i_limit: T,
25    rate_scale: T,
26    kl: T,             // blending factor for angle and rate
27    beta_roll: T,      // blending factor for angle and rate
28    beta_pitch: T,     // blending factor for angle and rate
29    blending_limit: T, // blending factor for angle and rate
30    prev_set_point_roll: T,
31    prev_set_point_pitch: T,
32    prev_imu_roll: T,
33    prev_imu_pitch: T,
34}
35
36impl<T: Number> Default for Angle2Stabilizer<T> {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl<T: Number> Angle2Stabilizer<T> {
43    /// Creates a new controller using the provided configuration
44    pub fn with_config(
45        angle_config: FlightStabilizerConfig<T>,
46        rate_config: FlightStabilizerConfig<T>,
47        blending_config: CascadeBlendingConfig<T, 2>,
48    ) -> Self {
49        let mut angle_roll_pid = PidController::new();
50        angle_roll_pid
51            .compute_fn(compute_cascade_angle)
52            .set_point(angle_config.set_point_roll)
53            .kp(angle_config.kp_roll)
54            .ki(angle_config.ki_roll)
55            .kd(angle_config.kd_roll);
56
57        let mut angle_pitch_pid = PidController::new();
58        angle_pitch_pid
59            .compute_fn(compute_cascade_angle)
60            .set_point(angle_config.set_point_pitch)
61            .kp(angle_config.kp_pitch)
62            .ki(angle_config.ki_pitch)
63            .kd(angle_config.kd_pitch);
64
65        let mut rate_roll_pid = PidController::new();
66        rate_roll_pid
67            .compute_fn(compute_rate)
68            .set_point(rate_config.set_point_roll)
69            .kp(rate_config.kp_roll)
70            .ki(rate_config.ki_roll)
71            .kd(rate_config.kd_roll);
72
73        let mut rate_pitch_pid = PidController::new();
74        rate_pitch_pid
75            .compute_fn(compute_rate)
76            .set_point(rate_config.set_point_pitch)
77            .kp(rate_config.kp_pitch)
78            .ki(rate_config.ki_pitch)
79            .kd(rate_config.kd_pitch);
80
81        let mut rate_yaw_pid = PidController::new();
82        rate_yaw_pid
83            .compute_fn(compute_rate)
84            .set_point(rate_config.set_point_yaw)
85            .kp(rate_config.kp_yaw)
86            .ki(rate_config.ki_yaw)
87            .kd(rate_config.kd_yaw);
88
89        Angle2Stabilizer {
90            angle_roll_pid,
91            angle_pitch_pid,
92            angle_i_limit: angle_config.i_limit,
93            angle_scale: angle_config.scale,
94            rate_roll_pid,
95            rate_pitch_pid,
96            rate_yaw_pid,
97            rate_i_limit: rate_config.i_limit,
98            rate_scale: rate_config.scale,
99            kl: blending_config.k,
100            beta_roll: blending_config.beta[0],
101            beta_pitch: blending_config.beta[1],
102            blending_limit: blending_config.limit,
103            prev_set_point_roll: angle_config.set_point_roll,
104            prev_set_point_pitch: angle_config.set_point_pitch,
105            prev_imu_roll: T::zero(),
106            prev_imu_pitch: T::zero(),
107        }
108    }
109
110    /// Creates a new controller with default settings
111    pub fn new() -> Self {
112        Self::with_config(
113            FlightStabilizerConfig::new(),
114            FlightStabilizerConfig::new(),
115            CascadeBlendingConfig::new(),
116        )
117    }
118
119    fn blend(&self, set_point: T, prev_set_point: T, beta: T) -> T {
120        let result = (set_point * self.kl).clamp(-self.blending_limit, self.blending_limit);
121        beta * result + (T::one() - beta) * prev_set_point
122    }
123}
124
125impl<T: Number> FlightStabilizer<T> for Angle2Stabilizer<T> {
126    fn control(
127        &mut self,
128        set_point: (T, T, T),
129        imu_attitude: (T, T, T),
130        gyro_rate: (T, T, T),
131        dt: T,
132        low_throttle: bool,
133    ) -> (T, T, T) {
134        // Set the setpoints for roll, pitch, and yaw
135        let (set_point_roll, set_point_pitch, set_point_yaw) = set_point;
136        self.angle_roll_pid.set_point(set_point_roll);
137        self.angle_pitch_pid.set_point(set_point_pitch);
138
139        // Prepare control data for roll and pitch
140        let (imu_roll, imu_pitch, _) = imu_attitude;
141        let (gyro_roll, gyro_pitch, gyro_yaw) = gyro_rate;
142        let angle_roll_data = CascadeAngleControlData {
143            measurement: imu_roll,
144            prev_measurement: self.prev_imu_roll,
145            rate: gyro_roll,
146            dt,
147            integral_limit: self.angle_i_limit,
148            reset_integral: low_throttle,
149        };
150        let angle_pitch_data = CascadeAngleControlData {
151            measurement: imu_pitch,
152            prev_measurement: self.prev_imu_pitch,
153            rate: gyro_pitch,
154            dt,
155            integral_limit: self.angle_i_limit,
156            reset_integral: low_throttle,
157        };
158
159        // Compute outputs for roll, pitch, and yaw
160        let mut adjusted_set_point_roll =
161            self.angle_scale * self.angle_roll_pid.compute(angle_roll_data);
162        let mut adjusted_set_point_pitch =
163            self.angle_scale * self.angle_pitch_pid.compute(angle_pitch_data);
164
165        //Apply blending gain, clamp, and LP filter for artificial damping
166        adjusted_set_point_roll = self.blend(
167            adjusted_set_point_roll,
168            self.prev_set_point_roll,
169            self.beta_roll,
170        );
171        adjusted_set_point_pitch = self.blend(
172            adjusted_set_point_pitch,
173            self.prev_set_point_pitch,
174            self.beta_pitch,
175        );
176
177        // Set the rate set points for roll, pitch, and yaw
178        self.rate_roll_pid.set_point(adjusted_set_point_roll);
179        self.rate_pitch_pid.set_point(adjusted_set_point_pitch);
180        self.rate_yaw_pid.set_point(set_point_yaw);
181
182        // Prepare rate control data for roll, pitch, and yaw
183        let rate_roll_data = RateControlData {
184            rate: gyro_roll,
185            dt,
186            integral_limit: self.rate_i_limit,
187            reset_integral: low_throttle,
188        };
189        let rate_pitch_data = RateControlData {
190            rate: gyro_pitch,
191            dt,
192            integral_limit: self.rate_i_limit,
193            reset_integral: low_throttle,
194        };
195        let rate_yaw_data = RateControlData {
196            rate: gyro_yaw,
197            dt,
198            integral_limit: self.rate_i_limit,
199            reset_integral: low_throttle,
200        };
201        // Compute outputs for roll, pitch, and yaw
202        let roll_output = self.rate_scale * self.rate_roll_pid.compute(rate_roll_data);
203        let pitch_output = self.rate_scale * self.rate_pitch_pid.compute(rate_pitch_data);
204        let yaw_output = self.rate_scale * self.rate_yaw_pid.compute(rate_yaw_data);
205
206        // Store prevous values
207        self.prev_set_point_roll = adjusted_set_point_roll;
208        self.prev_set_point_pitch = adjusted_set_point_pitch;
209        self.prev_imu_roll = imu_roll;
210        self.prev_imu_pitch = imu_pitch;
211
212        (roll_output, pitch_output, yaw_output)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::test_utils::*;
220
221    /// Default test configuration.
222    fn default_config() -> (
223        FlightStabilizerConfig<f32>,
224        FlightStabilizerConfig<f32>,
225        CascadeBlendingConfig<f32, 2>,
226    ) {
227        // Define angle config with shared values
228        let mut angle_config = FlightStabilizerConfig::new();
229
230        // Set the angle PID gains for roll, pitch, and yaw.
231        angle_config.kp_roll = 0.2;
232        angle_config.ki_roll = 0.3;
233        angle_config.kd_roll = 0.0; // zero out angle derivative
234
235        angle_config.kp_pitch = angle_config.kp_roll;
236        angle_config.ki_pitch = angle_config.ki_roll;
237        angle_config.kd_pitch = angle_config.kd_roll;
238
239        angle_config.kp_yaw = 0.3;
240        angle_config.ki_yaw = 0.05;
241        angle_config.kd_yaw = 0.00015;
242
243        // Do not scale the initial angle PID output.
244        angle_config.scale = 1.0;
245
246        // Set the shared initial setpoints for roll, pitch, and yaw.
247        // These default to zero.
248        angle_config.set_point_roll = 0.0;
249        angle_config.set_point_pitch = 0.0;
250        angle_config.set_point_yaw = 0.0;
251
252        // Set the shared upper limit for the integral term to prevent windup.
253        angle_config.i_limit = 25.0;
254
255        // Copy shared values for rate config
256        let mut rate_config = angle_config;
257
258        // Set the rate PID gains for roll, pitch, and yaw.
259        rate_config.kp_roll = 0.15;
260        rate_config.ki_roll = 0.2;
261        rate_config.kd_roll = 0.0002;
262
263        rate_config.kp_pitch = rate_config.kp_roll;
264        rate_config.ki_pitch = rate_config.ki_roll;
265        rate_config.kd_pitch = rate_config.kd_roll;
266
267        rate_config.kp_yaw = 0.3;
268        rate_config.ki_yaw = 0.05;
269        rate_config.kd_yaw = 0.00015;
270
271        // Set the final scale to adjust the PID outputs to the actuator range.
272        rate_config.scale = 0.01;
273
274        // define blending config
275        let mut blending_config = CascadeBlendingConfig::new();
276
277        blending_config.beta = [0.9; 2];
278        blending_config.k = 30.0;
279        blending_config.limit = 240.0;
280
281        (angle_config, rate_config, blending_config)
282    }
283
284    /// Test the initialization of the Angle2Stabilizer with a default configuration.
285    #[test]
286    fn test_stabilizer_angle2_initialization_with_default_config() {
287        let (angle_config, rate_config, blending_config) = default_config();
288        let stabilizer = Angle2Stabilizer::with_config(angle_config, rate_config, blending_config);
289
290        assert_eq!(stabilizer.angle_roll_pid.kp, angle_config.kp_roll);
291        assert_eq!(stabilizer.angle_pitch_pid.kp, angle_config.kp_pitch);
292        assert_eq!(stabilizer.rate_roll_pid.kp, rate_config.kp_roll);
293        assert_eq!(stabilizer.rate_pitch_pid.kp, rate_config.kp_pitch);
294        assert_eq!(stabilizer.rate_yaw_pid.kp, rate_config.kp_yaw);
295        assert_eq!(stabilizer.beta_roll, blending_config.beta[0]);
296    }
297
298    /// Test that the integrator saturation works as expected by the DEFAULT_I_LIMIT.
299    #[test]
300    fn test_stabilizer_angle2_integrator_saturation() {
301        let (angle_config, rate_config, blending_config) = default_config();
302        let mut stabilizer =
303            Angle2Stabilizer::with_config(angle_config, rate_config, blending_config);
304
305        // Simulated sensor inputs and desired setpoints
306        let set_point = (500.0, -500.0, 50.0); // desired roll, pitch, yaw
307        let imu_attitude = (0.0, 0.0, 0.0); // current roll, pitch, yaw
308        let gyro_rate = (0.0, 0.0, 0.0); // current roll rate, pitch rate, yaw rate
309        let dt = 0.01; // time step
310        let low_throttle = false;
311
312        // Apply consistent error over multiple cycles to force integrator saturation
313        for _ in 0..100 {
314            let _ = stabilizer.control(set_point, imu_attitude, gyro_rate, dt, low_throttle);
315        }
316
317        let angle_integrals = (
318            stabilizer.angle_roll_pid.integral,
319            stabilizer.angle_pitch_pid.integral,
320            0.0,
321        );
322        let expected_angle_integrals = (angle_config.i_limit, -angle_config.i_limit, 0.0);
323        assert!(
324            vector_close(expected_angle_integrals, angle_integrals),
325            "Integrals should be capped."
326        );
327        let rate_integrals = (
328            stabilizer.rate_roll_pid.integral,
329            stabilizer.rate_pitch_pid.integral,
330            stabilizer.rate_yaw_pid.integral,
331        );
332        let expected_rate_integrals = (
333            rate_config.i_limit,
334            -rate_config.i_limit,
335            rate_config.i_limit,
336        );
337        assert!(
338            vector_close(expected_rate_integrals, rate_integrals),
339            "Integrals should be capped."
340        );
341    }
342
343    /// Test to ensure integrators are reset when PWM is below threshold.
344    #[test]
345    fn test_stabilizer_angle2_low_throttle_integrator_reset() {
346        let (angle_config, rate_config, blending_config) = default_config();
347        let mut stabilizer =
348            Angle2Stabilizer::with_config(angle_config, rate_config, blending_config);
349
350        // Simulated sensor inputs and desired setpoints
351        let set_point = (10.0, 0.0, 10.0); // desired roll, pitch, yaw
352        let imu_attitude = (5.0, 5.0, 0.0); // current roll, pitch, yaw
353        let gyro_rate = (1.0, -1.0, -1.0); // current roll rate, pitch rate, yaw rate
354        let dt = 0.01; // time step
355
356        // Allow integrators to build up
357        let _ = stabilizer.control(set_point, imu_attitude, gyro_rate, dt, false);
358        let angle_integrals = (
359            stabilizer.angle_roll_pid.integral,
360            stabilizer.angle_pitch_pid.integral,
361            1.0, // not calculated
362        );
363        let unexpected_angle_integrals = (0.0, 0.0, 0.0);
364        assert!(
365            vector_not_close(unexpected_angle_integrals, angle_integrals),
366            "Integrals should not be zero."
367        );
368        let rate_integrals = (
369            stabilizer.rate_roll_pid.integral,
370            stabilizer.rate_pitch_pid.integral,
371            stabilizer.rate_yaw_pid.integral,
372        );
373        let unexpected_rate_integrals = (0.0, 0.0, 0.0);
374        assert!(
375            vector_not_close(unexpected_rate_integrals, rate_integrals),
376            "Integrals should not be zero."
377        );
378
379        // Apply low throttle, which should reset integrators
380        let _ = stabilizer.control(set_point, imu_attitude, gyro_rate, dt, true);
381        let angle_integrals = (
382            stabilizer.angle_roll_pid.integral,
383            stabilizer.angle_pitch_pid.integral,
384            0.0, // not calculated
385        );
386        let expected_angle_integrals = (0.0, 0.0, 0.0);
387        assert!(
388            vector_close(expected_angle_integrals, angle_integrals),
389            "Integrals should be zero."
390        );
391        let rate_integrals = (
392            stabilizer.rate_roll_pid.integral,
393            stabilizer.rate_pitch_pid.integral,
394            stabilizer.rate_yaw_pid.integral,
395        );
396        let expected_rate_integrals = (0.0, 0.0, 0.0);
397        assert!(
398            vector_close(expected_rate_integrals, rate_integrals),
399            "Integrals should be zero."
400        );
401    }
402
403    /// Test the no error contidion.
404    #[test]
405    fn test_stabilizer_angle2_no_error() {
406        let (angle_config, rate_config, blending_config) = default_config();
407        let mut stabilizer =
408            Angle2Stabilizer::with_config(angle_config, rate_config, blending_config);
409
410        // Simulated sensor inputs and desired setpoints
411        let set_point = (0.0, 0.0, 0.0); // desired roll, pitch, yaw
412        let imu_attitude = (0.0, 0.0, 0.0); // current roll, pitch, yaw
413        let gyro_rate = (0.0, 0.0, 0.0); // current roll rate, pitch rate, yaw rate
414        let dt = 0.01; // time step
415        let low_throttle = false;
416
417        // Perform the control computation
418        let output = stabilizer.control(set_point, imu_attitude, gyro_rate, dt, low_throttle);
419        let expected_output = (0.0, 0.0, 0.0);
420
421        assert!(
422            vector_close(expected_output, output),
423            "Outputs should be zero as there is no error."
424        );
425    }
426
427    /// Test with specific inputs to calculate expected PID outputs.
428    #[test]
429    fn test_stabilizer_angle2_specific_pid_output() {
430        // Configuration
431        const PREV_ANGLE_ROLL_INTEGRAL: f32 = 5.0;
432        const PREV_ANGLE_PITCH_INTEGRAL: f32 = -5.0;
433        const PREV_RATE_ROLL_INTEGRAL: f32 = 0.2;
434        const PREV_RATE_PITCH_INTEGRAL: f32 = -0.2;
435        const PREV_RATE_YAW_INTEGRAL: f32 = 0.2;
436        const PREV_SET_POINT_ROLL: f32 = 50.0;
437        const PREV_SET_POINT_PITCH: f32 = -50.0;
438        let (angle_config, rate_config, blending_config) = default_config();
439
440        // Simulated sensor inputs and desired setpoints
441        let set_point = (10.0, 0.0, 10.0); // desired roll, pitch, yaw
442        let imu_attitude = (5.0, 5.0, 0.0); // current roll, pitch, yaw
443        let gyro_rate = (1.0, -1.0, -1.0); // current roll rate, pitch rate, yaw rate
444        let dt = 0.01; // time step
445        let low_throttle = false;
446
447        let mut stabilizer =
448            Angle2Stabilizer::with_config(angle_config, rate_config, blending_config);
449        stabilizer.angle_roll_pid.integral = PREV_ANGLE_ROLL_INTEGRAL;
450        stabilizer.angle_pitch_pid.integral = PREV_ANGLE_PITCH_INTEGRAL;
451        stabilizer.rate_roll_pid.integral = PREV_RATE_ROLL_INTEGRAL;
452        stabilizer.rate_pitch_pid.integral = PREV_RATE_PITCH_INTEGRAL;
453        stabilizer.rate_yaw_pid.integral = PREV_RATE_YAW_INTEGRAL;
454        stabilizer.prev_set_point_roll = PREV_SET_POINT_ROLL;
455        stabilizer.prev_set_point_pitch = PREV_SET_POINT_PITCH;
456
457        // Test the first part of the cascade.
458        let angle_roll_data = CascadeAngleControlData {
459            measurement: imu_attitude.0,
460            prev_measurement: imu_attitude.0,
461            rate: gyro_rate.0,
462            dt: dt,
463            integral_limit: angle_config.i_limit,
464            reset_integral: low_throttle,
465        };
466        let angle_pitch_data = CascadeAngleControlData {
467            measurement: imu_attitude.1,
468            prev_measurement: imu_attitude.1,
469            rate: gyro_rate.1,
470            dt: dt,
471            integral_limit: angle_config.i_limit,
472            reset_integral: low_throttle,
473        };
474
475        // Compute the adjusted roll setpoint and internal values
476        stabilizer.angle_roll_pid.set_point(set_point.0);
477        let (roll_error, roll_integral, roll_derivative) =
478            compute_cascade_angle(&mut stabilizer.angle_roll_pid, angle_roll_data);
479        let mut adjusted_set_point_roll = angle_config.scale
480            * (angle_config.kp_roll * roll_error
481                + angle_config.ki_roll * roll_integral
482                + angle_config.kd_roll * roll_derivative);
483        adjusted_set_point_roll = stabilizer.blend(
484            adjusted_set_point_roll,
485            PREV_SET_POINT_ROLL,
486            blending_config.beta[0],
487        );
488
489        // Compute the adjusted pitch setpoint and internal values
490        stabilizer.angle_pitch_pid.set_point(set_point.1);
491        let (pitch_error, pitch_integral, pitch_derivative) =
492            compute_cascade_angle(&mut stabilizer.angle_pitch_pid, angle_pitch_data);
493        let mut adjusted_set_point_pitch = angle_config.scale
494            * (angle_config.kp_pitch * pitch_error
495                + angle_config.ki_pitch * pitch_integral
496                + angle_config.kd_pitch * pitch_derivative);
497        adjusted_set_point_pitch = stabilizer.blend(
498            adjusted_set_point_pitch,
499            PREV_SET_POINT_PITCH,
500            blending_config.beta[1],
501        );
502
503        // Internal values should be inverted
504        assert_eq!(roll_error, -pitch_error);
505        assert_eq!(roll_integral, -pitch_integral);
506        assert_eq!(roll_derivative, -pitch_derivative);
507
508        // Adjusted set points should match standard numbers
509        let adjusted_set_points = (adjusted_set_point_roll, adjusted_set_point_pitch, 0.0);
510        let expected_adjusted_set_points = (72.905, -72.905, 0.0);
511        assert!(
512            vector_close(expected_adjusted_set_points, adjusted_set_points),
513            "Adjusted set points should match specific values."
514        );
515
516        // Perform the control computation
517        let output = stabilizer.control(set_point, imu_attitude, gyro_rate, dt, low_throttle);
518        let expected_output = (0.124076605, -0.124076605, 0.034805);
519        assert!(
520            vector_close(expected_output, output),
521            "PID outputs should match specific values."
522        );
523    }
524}