imu_fusion/
fusion_ahrs_impl.rs

1use libm::{atan2f, cosf, fabsf, sinf};
2use crate::{fusion_degrees_to_radians, FusionAhrs, FusionAhrsFlags, FusionAhrsSettings, FusionConvention, FusionQuaternion, FusionVector};
3use crate::FusionConvention::NWU;
4
5/**
6 * Initial gain used during the initialisation.
7 */
8const INITIAL_GAIN: f32 = 10.0f32;
9/**
10 * Initialisation period in seconds.
11 */
12const INITIALISATION_PERIOD: f32 = 3.0f32;
13
14impl FusionAhrs {
15    pub fn new() -> Self {
16        let settings = FusionAhrsSettings::new();
17        let gain = settings.gain;
18        let recovery_trigger_period = settings.recovery_trigger_period;
19        Self {
20            settings,
21            quaternion: FusionQuaternion::identity(),
22            acc: FusionVector::zero(),
23            initialising: true,
24            ramped_gain: INITIAL_GAIN,
25            ramped_gain_step: (INITIAL_GAIN - gain) / INITIALISATION_PERIOD,
26            angular_rate_recovery: false,
27            half_accelerometer_feedback: FusionVector::zero(),
28            half_magnetometer_feedback: FusionVector::zero(),
29            accelerometer_ignored: false,
30            acceleration_recovery_trigger: 0,
31            acceleration_recovery_timeout: recovery_trigger_period,
32            magnetometer_ignored: false,
33            magnetic_recovery_trigger: 0,
34            magnetic_recovery_timeout: recovery_trigger_period,
35        }
36    }
37
38    pub fn update_settings(&mut self, settings: FusionAhrsSettings) {
39        self.settings.convention = settings.convention;
40        self.settings.gain = settings.gain;
41        self.settings.gyr_range = if settings.gyr_range == 0.0f32 { f32::MAX } else { 0.98f32 * settings.gyr_range };
42        self.settings.acc_rejection = if settings.acc_rejection == 0.0f32 {
43            f32::MAX
44        } else {
45            let v = 0.5f32 * sinf(fusion_degrees_to_radians(settings.acc_rejection));
46            v * v
47        };
48        self.settings.mag_rejection = if settings.mag_rejection == 0.0f32 {
49            f32::MAX
50        } else {
51            let v = 0.5f32 * sinf(fusion_degrees_to_radians(settings.mag_rejection));
52            v * v
53        };
54        self.settings.recovery_trigger_period = settings.recovery_trigger_period;
55        self.acceleration_recovery_timeout = self.settings.recovery_trigger_period;
56        self.magnetic_recovery_timeout = self.settings.recovery_trigger_period;
57        if settings.gain == 0.0f32 || settings.recovery_trigger_period == 0 { // disable acceleration and magnetic rejection features if gain is zero
58            self.settings.acc_rejection = f32::MAX;
59            self.settings.mag_rejection = f32::MAX;
60        }
61        if !self.initialising {
62            self.ramped_gain = settings.gain;
63        }
64        self.ramped_gain_step = (INITIAL_GAIN - self.settings.gain) / INITIALISATION_PERIOD;
65    }
66
67    pub fn update_no_mag(&mut self, gyr: FusionVector, acc: FusionVector, dt: f32) {
68        self.update(gyr, acc, FusionVector::zero(), dt);
69        // Zero heading during initialisation
70        if self.initialising == true {
71            self.set_heading(0.0f32);
72        }
73    }
74
75    pub fn update_external_heading(&mut self, gyr: FusionVector, acc: FusionVector, heading: f32, dt: f32) {
76        let q = self.quaternion;
77        // Calculate roll
78        let roll = atan2f(q.w * q.x + q.y * q.z, 0.5f32 - q.y * q.y - q.x * q.x);
79        // Calculate magnetometer// Calculate magnetometer
80        let heading_radians = fusion_degrees_to_radians(heading);
81        let sin_heading_radians = sinf(heading_radians);
82        let magnetometer = FusionVector {
83            x: cosf(heading_radians),
84            y: -1.0f32 * cosf(roll) * sin_heading_radians,
85            z: sin_heading_radians * sinf(roll),
86        };
87        // Update AHRS algorithm
88        self.update(gyr, acc, magnetometer, dt);
89    }
90
91    pub fn update(&mut self, gyr: FusionVector, acc: FusionVector, mag: FusionVector, dt: f32) {
92        // Store accelerometer
93        self.acc = acc;
94        // Reinitialise if gyroscope range exceeded
95        if fabsf(gyr.x) > self.settings.gyr_range || fabsf(gyr.y) > self.settings.gyr_range || fabsf(gyr.z) > self.settings.gyr_range {
96            let quaternion = self.quaternion;
97            self.reset();
98            self.quaternion = quaternion;
99            self.angular_rate_recovery = true;
100        }
101        // Ramp down gain during initialisation
102        if self.initialising == true {
103            self.ramped_gain -= self.ramped_gain_step * dt;
104            if self.ramped_gain < self.settings.gain || self.settings.gain == 0.0f32 {
105                self.ramped_gain = self.settings.gain;
106                self.initialising = false;
107                self.angular_rate_recovery = false;
108            }
109        }
110        // Calculate direction of gravity indicated by algorithm
111        let half_gravity = self.calculate_half_gravity();
112
113        // Calculate accelerometer feedback
114        let mut half_accelerometer_feedback = FusionVector::zero();
115        self.accelerometer_ignored = true;
116        if !self.acc.is_zero() {
117            // Calculate accelerometer feedback scaled by 0.5
118            self.half_accelerometer_feedback = self.feedback(self.acc.normalize(), half_gravity);
119            // Don't ignore accelerometer if acceleration error below threshold
120            if self.initialising == true || self.half_accelerometer_feedback.magnitude() <= self.settings.acc_rejection {
121                self.accelerometer_ignored = false;
122                self.acceleration_recovery_trigger -= 9;
123            } else {
124                self.acceleration_recovery_trigger += 1;
125            }
126            // Don't ignore accelerometer during acceleration recovery
127            if self.acceleration_recovery_trigger > self.acceleration_recovery_timeout {
128                self.acceleration_recovery_timeout = 0;
129                self.accelerometer_ignored = false;
130            } else {
131                self.acceleration_recovery_timeout = self.settings.recovery_trigger_period;
132            }
133            self.acceleration_recovery_trigger = clamp(self.acceleration_recovery_trigger, 0, self.settings.recovery_trigger_period);
134            // Apply accelerometer feedback
135            if !self.accelerometer_ignored {
136                half_accelerometer_feedback = self.half_accelerometer_feedback;
137            }
138        }
139        // Calculate magnetometer feedback
140        let mut half_magnetometer_feedback = FusionVector::zero();
141        self.magnetometer_ignored = true;
142        if !mag.is_zero() {
143            // Calculate direction of magnetic field indicated by algorithm
144            let half_magnetic = self.calculate_half_magnetic();
145            // Calculate magnetometer feedback scaled by 0.5
146
147            self.half_magnetometer_feedback = self.feedback(half_gravity.cross_product(&mag).normalize(), half_magnetic);
148            // Don't ignore magnetometer if magnetic error below threshold
149            if self.initialising == true || self.half_magnetometer_feedback.magnitude() <= self.settings.mag_rejection {
150                self.magnetometer_ignored = false;
151                self.magnetic_recovery_trigger -= 9;
152            } else {
153                self.magnetic_recovery_trigger += 1;
154            }
155            // Don't ignore magnetometer during magnetic recovery
156            if self.magnetic_recovery_trigger > self.magnetic_recovery_timeout {
157                self.magnetic_recovery_timeout = 0;
158                self.magnetometer_ignored = false;
159            } else {
160                self.magnetic_recovery_timeout = self.settings.recovery_trigger_period;
161            }
162            self.magnetic_recovery_trigger = clamp(self.magnetic_recovery_trigger, 0, self.settings.recovery_trigger_period);
163            // Apply magnetometer feedback
164            if !self.magnetometer_ignored {
165                half_magnetometer_feedback = self.half_magnetometer_feedback;
166            }
167        }
168
169        // Convert gyroscope to radians per second scaled by 0.5
170        let half_gyroscope = gyr * fusion_degrees_to_radians(0.5f32);
171        // Apply feedback to gyroscope
172        let adjusted_half_gyroscope = half_gyroscope + (half_accelerometer_feedback + half_magnetometer_feedback) * self.ramped_gain;
173        // Integrate rate of change of quaternion
174        self.quaternion = self.quaternion + self.quaternion * (adjusted_half_gyroscope * dt);
175        // Normalise quaternion
176        self.quaternion = self.quaternion.normalize();
177    }
178
179
180    fn feedback(&self, sensor: FusionVector, reference: FusionVector) -> FusionVector {
181        if sensor.dot_product(&reference) < 0.0f32 { // if error is >90 degrees
182            sensor.cross_product(&reference).normalize()
183        } else {
184            sensor.cross_product(&reference)
185        }
186    }
187
188    fn set_heading(&mut self, heading: f32) {
189        let q = self.quaternion;
190        let yaw = atan2f(q.w * q.z + q.x * q.y, 0.5f32 - q.y * q.y - q.z * q.z);
191        let half_yaw_minus_heading = 0.5f32 * (yaw - fusion_degrees_to_radians(heading));
192        let rotation = FusionQuaternion {
193            w: cosf(half_yaw_minus_heading),
194            x: 0.0f32,
195            y: 0.0f32,
196            z: -1.0f32 * sinf(half_yaw_minus_heading),
197        };
198        self.quaternion = rotation * self.quaternion;
199    }
200
201    pub fn reset(&mut self) {
202        self.quaternion = FusionQuaternion::identity();
203        self.acc = FusionVector::zero();
204        self.initialising = true;
205        self.ramped_gain = INITIAL_GAIN;
206        self.angular_rate_recovery = false;
207        self.half_accelerometer_feedback = FusionVector::zero();
208        self.half_magnetometer_feedback = FusionVector::zero();
209        self.accelerometer_ignored = false;
210        self.acceleration_recovery_trigger = 0;
211        self.acceleration_recovery_timeout = self.settings.recovery_trigger_period;
212        self.magnetometer_ignored = false;
213        self.magnetic_recovery_trigger = 0;
214        self.magnetic_recovery_timeout = self.settings.recovery_trigger_period;
215    }
216
217    pub fn calculate_half_gravity(&self) -> FusionVector {
218        let q = self.quaternion;
219        match self.settings.convention {
220            FusionConvention::ENU | FusionConvention::NWU => {
221                FusionVector {
222                    x: q.x * q.z - q.w * q.y,
223                    y: q.y * q.z + q.w * q.x,
224                    z: q.w * q.w - 0.5f32 + q.z * q.z,
225                }
226            }
227            FusionConvention::NED => {
228                FusionVector {
229                    x: q.w * q.y - q.x * q.z,
230                    y: -1.0f32 * (q.y * q.z + q.w * q.x),
231                    z: 0.5f32 - q.w * q.w - q.z * q.z,
232                }
233            }
234        }
235    }
236
237    pub fn calculate_half_magnetic(&self) -> FusionVector {
238        let q = self.quaternion;
239        match self.settings.convention {
240            FusionConvention::NWU => {
241                let half_magnetic = FusionVector {
242                    x: q.x * q.y + q.w * q.z,
243                    y: q.w * q.w - 0.5f32 + q.y * q.y,
244                    z: q.y * q.z - q.w * q.x,
245                };
246                half_magnetic
247            }
248            FusionConvention::ENU => {
249                let half_magnetic = FusionVector {
250                    x: 0.5f32 - q.w * q.w - q.x * q.x,
251                    y: q.w * q.z - q.x * q.y,
252                    z: -1.0f32 * (q.x * q.z + q.w * q.y),
253                };
254                half_magnetic
255            }
256            FusionConvention::NED => {
257                let half_magnetic = FusionVector {
258                    x: -1.0f32 * (q.x * q.y + q.w * q.z),
259                    y: 0.5f32 - q.w * q.w - q.y * q.y,
260                    z: q.w * q.x - q.y * q.z,
261                };
262                half_magnetic
263            }
264        }
265    }
266    pub fn earth_acc(&self) -> FusionVector {
267        // Calculate accelerometer measurement in the Earth coordinate frame
268        let q = self.quaternion;
269        let a = self.acc;
270        let qwqw = q.w * q.w; // calculate common terms to avoid repeated operations
271        let qwqx = q.w * q.x;
272        let qwqy = q.w * q.y;
273        let qwqz = q.w * q.z;
274        let qxqy = q.x * q.y;
275        let qxqz = q.x * q.z;
276        let qyqz = q.y * q.z;
277        let mut accelerometer = FusionVector {
278            x: 2.0f32 * ((qwqw - 0.5f32 + q.x * q.x) * a.x + (qxqy - qwqz) * a.y + (qxqz + qwqy) * a.z),
279            y: 2.0f32 * ((qxqy + qwqz) * a.x + (qwqw - 0.5f32 + q.y * q.y) * a.y + (qyqz - qwqx) * a.z),
280            z: 2.0f32 * ((qxqz - qwqy) * a.x + (qyqz + qwqx) * a.y + (qwqw - 0.5f32 + q.z * q.z) * a.z),
281        };
282        // Remove gravity from accelerometer measurement
283        match self.settings.convention {
284            FusionConvention::ENU | FusionConvention::NWU => {
285                accelerometer.z -= 1.0f32;
286                accelerometer
287            }
288            FusionConvention::NED => {
289                accelerometer.z += 1.0f32;
290                accelerometer
291            }
292        }
293    }
294
295    pub fn linear_acc(&self) -> FusionVector {
296        // Calculate accelerometer measurement in the Earth coordinate frame
297        let q = self.quaternion;
298        // Calculate gravity in the sensor coordinate frame
299        let gravity = FusionVector {
300            x: 2.0f32 * (q.x * q.z - q.w * q.y),
301            y: 2.0f32 * (q.y * q.z + q.w * q.x),
302            z: 2.0f32 * (q.w * q.w - 0.5f32 + q.z * q.z),
303        };
304
305        // Remove gravity from accelerometer measurement
306        match self.settings.convention {
307            FusionConvention::ENU | FusionConvention::NWU => {
308                self.acc - gravity
309            }
310            FusionConvention::NED => {
311                self.acc + gravity
312            }
313        }
314    }
315
316    pub fn flags(&self) -> FusionAhrsFlags {
317        FusionAhrsFlags {
318            initializing: self.initialising,
319            angular_rate_recovery: self.angular_rate_recovery,
320            acceleration_recovery: self.acceleration_recovery_trigger
321                > self.acceleration_recovery_timeout,
322            magnetic_recovery: self.magnetic_recovery_trigger > self.magnetic_recovery_timeout,
323        }
324    }
325}
326
327impl FusionAhrsSettings {
328    pub fn new() -> Self {
329        Self {
330            convention: NWU,
331            gain: 0.5f32,
332            gyr_range: 0.0f32,
333            acc_rejection: 90.0f32,
334            mag_rejection: 90.0f32,
335            recovery_trigger_period: 0,
336        }
337    }
338}
339
340fn clamp<T: Ord>(value: T, min: T, max: T) -> T {
341    if value < min {
342        min
343    } else if value > max {
344        max
345    } else {
346        value
347    }
348}