Skip to main content

core_s3/
motion.rs

1//! BMI270 accelerometer/gyroscope and BMM150 magnetometer helpers.
2//!
3//! Magnetometer readings are easily distorted by nearby magnets, speakers,
4//! batteries, metal, and board current. Calibrate in the final enclosure and do
5//! not treat heading as absolute without environmental validation.
6
7use embedded_hal::{delay::DelayNs, i2c::I2c};
8
9use crate::devices;
10
11const BMI270_CHIP_ID: u8 = 0x00;
12const BMI270_STATUS: u8 = 0x03;
13const BMI270_AUX_DATA: u8 = 0x04;
14const BMI270_ACC_DATA: u8 = 0x0C;
15const BMI270_GYR_DATA: u8 = 0x12;
16const BMI270_ACC_CONF: u8 = 0x40;
17const BMI270_ACC_RANGE: u8 = 0x41;
18const BMI270_GYR_CONF: u8 = 0x42;
19const BMI270_GYR_RANGE: u8 = 0x43;
20const BMI270_AUX_CONF: u8 = 0x44;
21const BMI270_AUX_DEV_ID: u8 = 0x4B;
22const BMI270_AUX_IF_CONF: u8 = 0x4C;
23const BMI270_AUX_RD_ADDR: u8 = 0x4D;
24const BMI270_AUX_WR_ADDR: u8 = 0x4E;
25const BMI270_AUX_WR_DATA: u8 = 0x4F;
26const BMI270_INT_STATUS_1: u8 = 0x1D;
27const BMI270_INTERNAL_STATUS: u8 = 0x21;
28const BMI270_INT_MAP_DATA: u8 = 0x58;
29const BMI270_INIT_CTRL: u8 = 0x59;
30const BMI270_INIT_ADDR_0: u8 = 0x5B;
31const BMI270_INIT_DATA: u8 = 0x5E;
32const BMI270_CONFIG_LEN: usize = 8192;
33const BMI270_IF_CONF: u8 = 0x6B;
34const BMI270_PWR_CONF: u8 = 0x7C;
35const BMI270_PWR_CTRL: u8 = 0x7D;
36const BMI270_CMD: u8 = 0x7E;
37const BMI270_EXPECTED_CHIP_ID: u8 = 0x24;
38
39const BMM150_CHIP_ID: u8 = 0x40;
40const BMM150_DATA_X_LSB: u8 = 0x42;
41const BMM150_POWER_CONTROL: u8 = 0x4B;
42const BMM150_OP_MODE: u8 = 0x4C;
43const BMM150_EXPECTED_CHIP_ID: u8 = 0x32;
44const AUX_BUSY_MASK: u8 = 0x04;
45
46#[repr(C, align(4))]
47struct AlignedBytes<const N: usize>([u8; N]);
48
49static BMI270_CONFIG_FILE: AlignedBytes<BMI270_CONFIG_LEN> =
50    AlignedBytes(*include_bytes!("../firmware/bmi270_config.bin"));
51
52#[cfg_attr(feature = "defmt", derive(defmt::Format))]
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub struct Vector3 {
55    pub x: i32,
56    pub y: i32,
57    pub z: i32,
58}
59
60impl Vector3 {
61    pub const fn new(x: i32, y: i32, z: i32) -> Self {
62        Self { x, y, z }
63    }
64
65    pub const fn offset(self, offsets: Self) -> Self {
66        Self {
67            x: self.x - offsets.x,
68            y: self.y - offsets.y,
69            z: self.z - offsets.z,
70        }
71    }
72}
73
74#[cfg_attr(feature = "defmt", derive(defmt::Format))]
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum AccelRange {
77    G2,
78    G4,
79    G8,
80    G16,
81}
82
83#[cfg_attr(feature = "defmt", derive(defmt::Format))]
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum GyroRange {
86    Dps125,
87    Dps250,
88    Dps500,
89    Dps1000,
90    Dps2000,
91}
92
93#[cfg_attr(feature = "defmt", derive(defmt::Format))]
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum SampleRate {
96    Hz25,
97    Hz50,
98    Hz100,
99    Hz200,
100    Hz400,
101}
102
103#[cfg_attr(feature = "defmt", derive(defmt::Format))]
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct Bmi270Config {
106    pub accel_range: AccelRange,
107    pub gyro_range: GyroRange,
108    pub sample_rate: SampleRate,
109}
110
111impl Bmi270Config {
112    pub const DEFAULT: Self = Self {
113        accel_range: AccelRange::G4,
114        gyro_range: GyroRange::Dps500,
115        sample_rate: SampleRate::Hz100,
116    };
117}
118
119#[cfg_attr(feature = "defmt", derive(defmt::Format))]
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct MotionThreshold {
122    pub accel_delta_mg: i32,
123}
124
125pub struct Bmi270<I2C> {
126    i2c: I2C,
127    address: u8,
128    accel_offset: Vector3,
129    gyro_offset: Vector3,
130}
131
132impl<I2C> Bmi270<I2C> {
133    pub const fn new(i2c: I2C) -> Self {
134        Self {
135            i2c,
136            address: devices::i2c::BMI270_IMU,
137            accel_offset: Vector3::new(0, 0, 0),
138            gyro_offset: Vector3::new(0, 0, 0),
139        }
140    }
141
142    pub fn set_accel_offset(&mut self, offset: Vector3) {
143        self.accel_offset = offset;
144    }
145    pub fn set_gyro_offset(&mut self, offset: Vector3) {
146        self.gyro_offset = offset;
147    }
148    pub fn release(self) -> I2C {
149        self.i2c
150    }
151}
152
153impl<I2C, Error> Bmi270<I2C>
154where
155    I2C: I2c<Error = Error>,
156{
157    /// Initializes the BMI270 register settings that do not require a delay provider.
158    ///
159    /// CoreS3 firmware should prefer [`Self::init_with_delay`] because BMI270
160    /// requires delays while resetting and loading its mandatory configuration file.
161    pub fn init(&mut self, config: Bmi270Config) -> Result<(), Error> {
162        self.write_register(BMI270_PWR_CONF, 0x00)?;
163        self.configure(config)?;
164        self.write_register(BMI270_PWR_CTRL, 0x0E)
165    }
166
167    pub fn init_with_delay(
168        &mut self,
169        config: Bmi270Config,
170        delay: &mut impl DelayNs,
171    ) -> Result<(), Error> {
172        self.write_register(BMI270_CMD, 0xB6)?;
173        delay.delay_ms(2);
174        self.write_register(BMI270_PWR_CONF, 0x00)?;
175        delay.delay_ms(2);
176        self.upload_config(delay)?;
177        self.write_register(BMI270_INT_MAP_DATA, 0xFF)?;
178        self.configure(config)?;
179        self.write_register(BMI270_PWR_CTRL, 0x0E)?;
180        delay.delay_ms(20);
181        Ok(())
182    }
183
184    pub fn configure(&mut self, config: Bmi270Config) -> Result<(), Error> {
185        self.write_register(BMI270_ACC_CONF, accel_conf_code(config.sample_rate))?;
186        self.write_register(BMI270_GYR_CONF, gyro_conf_code(config.sample_rate))?;
187        self.write_register(BMI270_ACC_RANGE, accel_range_code(config.accel_range))?;
188        self.write_register(BMI270_GYR_RANGE, gyro_range_code(config.gyro_range))
189    }
190
191    pub fn chip_id(&mut self) -> Result<u8, Error> {
192        self.read_register(BMI270_CHIP_ID)
193    }
194    pub fn is_expected_chip(&mut self) -> Result<bool, Error> {
195        Ok(self.chip_id()? == BMI270_EXPECTED_CHIP_ID)
196    }
197
198    /// Returns the BMI270 internal status register.
199    ///
200    /// After a successful config upload the lower nibble should report `0x01`
201    /// (`init_ok`). Other values indicate that acceleration/gyro data may stay
202    /// invalid or zero.
203    pub fn internal_status(&mut self) -> Result<u8, Error> {
204        self.read_register(BMI270_INTERNAL_STATUS)
205    }
206
207    /// Returns the BMI270 interrupt/status register used to observe data-ready bits.
208    pub fn interrupt_status_1(&mut self) -> Result<u8, Error> {
209        self.read_register(BMI270_INT_STATUS_1)
210    }
211
212    /// Returns the BMI270 power-control register.
213    pub fn power_control(&mut self) -> Result<u8, Error> {
214        self.read_register(BMI270_PWR_CTRL)
215    }
216
217    /// Initializes the CoreS3 BMM150 magnetometer through the BMI270 auxiliary I2C bus.
218    ///
219    /// CoreS3 wires BMM150 behind BMI270's AUX sensor-hub interface, so it is not
220    /// reachable as a normal device on the board internal I2C bus.
221    pub fn init_bmm150_aux(&mut self, delay: &mut impl DelayNs) -> Result<u8, Error> {
222        self.aux_setup_mode(devices::i2c::BMM150_MAGNETOMETER)?;
223        self.aux_write_register(BMM150_POWER_CONTROL, 0x83, delay)?;
224        delay.delay_ms(2);
225        let chip_id = self.aux_read_register(BMM150_CHIP_ID, delay)?;
226        if chip_id == BMM150_EXPECTED_CHIP_ID {
227            self.aux_write_register(BMM150_OP_MODE, 0x38, delay)?;
228            self.write_register(BMI270_AUX_IF_CONF, 0x4F)?;
229            self.write_register(BMI270_AUX_RD_ADDR, BMM150_DATA_X_LSB)?;
230            self.write_register(BMI270_AUX_CONF, 0x0B)?;
231            self.write_register(BMI270_PWR_CTRL, 0x0F)?;
232            delay.delay_ms(50);
233        }
234        Ok(chip_id)
235    }
236
237    /// Reads BMM150 raw magnetic data from BMI270's auxiliary data window.
238    pub fn bmm150_aux_magnetic_raw(&mut self) -> Result<Vector3, Error> {
239        let mut data = [0u8; 8];
240        self.i2c
241            .write_read(self.address, &[BMI270_AUX_DATA], &mut data)?;
242        Ok(decode_bmm150_mag_data(data))
243    }
244
245    /// Performs an explicit BMM150 data-register burst read through BMI270's AUX bus.
246    ///
247    /// This is useful for hardware validation because it does not depend on the
248    /// BMI270 sensor-hub shadow window updating automatically between samples.
249    pub fn bmm150_aux_magnetic_raw_manual(
250        &mut self,
251        delay: &mut impl DelayNs,
252    ) -> Result<Vector3, Error> {
253        self.write_register(BMI270_AUX_IF_CONF, 0x4F)?;
254        self.write_register(BMI270_AUX_RD_ADDR, BMM150_DATA_X_LSB)?;
255        self.wait_aux_ready(delay)?;
256        delay.delay_ms(4);
257        self.bmm150_aux_magnetic_raw()
258    }
259
260    pub fn acceleration_raw(&mut self) -> Result<Vector3, Error> {
261        let raw = self.read_vector(BMI270_ACC_DATA)?;
262        Ok(raw.offset(self.accel_offset))
263    }
264
265    pub fn gyroscope_raw(&mut self) -> Result<Vector3, Error> {
266        let raw = self.read_vector(BMI270_GYR_DATA)?;
267        Ok(raw.offset(self.gyro_offset))
268    }
269
270    fn upload_config(&mut self, delay: &mut impl DelayNs) -> Result<(), Error> {
271        self.write_register(BMI270_INIT_CTRL, 0x00)?;
272        delay.delay_ms(2);
273
274        let config_file = &BMI270_CONFIG_FILE.0;
275        let mut offset = 0usize;
276        while offset < config_file.len() {
277            let chunk_len = (config_file.len() - offset).min(16);
278            self.i2c.write(
279                self.address,
280                &[
281                    BMI270_INIT_ADDR_0,
282                    ((offset >> 1) & 0x0F) as u8,
283                    (offset >> 5) as u8,
284                ],
285            )?;
286
287            let mut packet = [0u8; 17];
288            packet[0] = BMI270_INIT_DATA;
289            packet[1..1 + chunk_len].copy_from_slice(&config_file[offset..offset + chunk_len]);
290            self.i2c.write(self.address, &packet[..1 + chunk_len])?;
291            offset += chunk_len;
292        }
293
294        self.write_register(BMI270_INIT_CTRL, 0x01)?;
295        delay.delay_ms(20);
296        Ok(())
297    }
298
299    fn aux_setup_mode(&mut self, i2c_address: u8) -> Result<(), Error> {
300        self.write_register(BMI270_IF_CONF, 0x20)?;
301        self.write_register(BMI270_PWR_CONF, 0x00)?;
302        self.write_register(BMI270_PWR_CTRL, 0x0E)?;
303        self.write_register(BMI270_AUX_IF_CONF, 0x80)?;
304        self.write_register(BMI270_AUX_DEV_ID, i2c_address << 1)
305    }
306
307    fn aux_write_register(
308        &mut self,
309        register: u8,
310        value: u8,
311        delay: &mut impl DelayNs,
312    ) -> Result<(), Error> {
313        self.write_register(BMI270_AUX_WR_DATA, value)?;
314        self.write_register(BMI270_AUX_WR_ADDR, register)?;
315        self.wait_aux_ready(delay)
316    }
317
318    fn aux_read_register(&mut self, register: u8, delay: &mut impl DelayNs) -> Result<u8, Error> {
319        self.write_register(BMI270_AUX_IF_CONF, 0x80)?;
320        self.write_register(BMI270_AUX_RD_ADDR, register)?;
321        self.wait_aux_ready(delay)?;
322        self.read_register(BMI270_AUX_DATA)
323    }
324
325    fn wait_aux_ready(&mut self, delay: &mut impl DelayNs) -> Result<(), Error> {
326        for _ in 0..3 {
327            if self.read_register(BMI270_STATUS)? & AUX_BUSY_MASK == 0 {
328                return Ok(());
329            }
330            delay.delay_ms(1);
331        }
332        Ok(())
333    }
334
335    fn read_vector(&mut self, start: u8) -> Result<Vector3, Error> {
336        let mut data = [0u8; 6];
337        self.i2c.write_read(self.address, &[start], &mut data)?;
338        Ok(Vector3::new(
339            i32::from(i16::from_le_bytes([data[0], data[1]])),
340            i32::from(i16::from_le_bytes([data[2], data[3]])),
341            i32::from(i16::from_le_bytes([data[4], data[5]])),
342        ))
343    }
344
345    fn read_register(&mut self, register: u8) -> Result<u8, Error> {
346        let mut value = [0u8];
347        self.i2c.write_read(self.address, &[register], &mut value)?;
348        Ok(value[0])
349    }
350
351    fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error> {
352        self.i2c.write(self.address, &[register, value])
353    }
354}
355
356pub struct Bmm150<I2C> {
357    i2c: I2C,
358    address: u8,
359    hard_iron_offset: Vector3,
360}
361
362impl<I2C> Bmm150<I2C> {
363    pub const fn new(i2c: I2C) -> Self {
364        Self {
365            i2c,
366            address: devices::i2c::BMM150_MAGNETOMETER,
367            hard_iron_offset: Vector3::new(0, 0, 0),
368        }
369    }
370
371    pub fn set_hard_iron_offset(&mut self, offset: Vector3) {
372        self.hard_iron_offset = offset;
373    }
374    pub fn release(self) -> I2C {
375        self.i2c
376    }
377}
378
379impl<I2C, Error> Bmm150<I2C>
380where
381    I2C: I2c<Error = Error>,
382{
383    pub fn init(&mut self) -> Result<(), Error> {
384        self.write_register(BMM150_POWER_CONTROL, 0x01)?;
385        self.write_register(BMM150_OP_MODE, 0x00)
386    }
387
388    pub fn chip_id(&mut self) -> Result<u8, Error> {
389        self.read_register(BMM150_CHIP_ID)
390    }
391    pub fn is_expected_chip(&mut self) -> Result<bool, Error> {
392        Ok(self.chip_id()? == BMM150_EXPECTED_CHIP_ID)
393    }
394
395    pub fn magnetic_raw(&mut self) -> Result<Vector3, Error> {
396        let mut data = [0u8; 6];
397        self.i2c
398            .write_read(self.address, &[BMM150_DATA_X_LSB], &mut data)?;
399        let raw = Vector3::new(
400            i32::from(i16::from_le_bytes([data[0] & 0xF8, data[1]]) >> 3),
401            i32::from(i16::from_le_bytes([data[2] & 0xF8, data[3]]) >> 3),
402            i32::from(i16::from_le_bytes([data[4] & 0xFE, data[5]]) >> 1),
403        );
404        Ok(raw.offset(self.hard_iron_offset))
405    }
406
407    fn read_register(&mut self, register: u8) -> Result<u8, Error> {
408        let mut value = [0u8];
409        self.i2c.write_read(self.address, &[register], &mut value)?;
410        Ok(value[0])
411    }
412
413    fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error> {
414        self.i2c.write(self.address, &[register, value])
415    }
416}
417
418pub fn motion_detected(previous: Vector3, current: Vector3, threshold: MotionThreshold) -> bool {
419    (current.x - previous.x).abs() >= threshold.accel_delta_mg
420        || (current.y - previous.y).abs() >= threshold.accel_delta_mg
421        || (current.z - previous.z).abs() >= threshold.accel_delta_mg
422}
423
424/// Returns heading in centidegrees using a small integer approximation.
425pub fn heading_centidegrees(magnetic: Vector3) -> Option<u16> {
426    if magnetic.x == 0 && magnetic.y == 0 {
427        return None;
428    }
429    let angle = atan2_centidegrees(magnetic.y, magnetic.x);
430    Some(if angle < 0 {
431        (angle + 36_000) as u16
432    } else {
433        angle as u16
434    })
435}
436
437fn sample_rate_code(rate: SampleRate) -> u8 {
438    match rate {
439        SampleRate::Hz25 => 0x06,
440        SampleRate::Hz50 => 0x07,
441        SampleRate::Hz100 => 0x08,
442        SampleRate::Hz200 => 0x09,
443        SampleRate::Hz400 => 0x0A,
444    }
445}
446
447fn accel_conf_code(rate: SampleRate) -> u8 {
448    0xA0 | sample_rate_code(rate)
449}
450
451fn gyro_conf_code(rate: SampleRate) -> u8 {
452    0xA0 | sample_rate_code(rate)
453}
454fn accel_range_code(range: AccelRange) -> u8 {
455    match range {
456        AccelRange::G2 => 0x00,
457        AccelRange::G4 => 0x01,
458        AccelRange::G8 => 0x02,
459        AccelRange::G16 => 0x03,
460    }
461}
462fn decode_bmm150_mag_data(data: [u8; 8]) -> Vector3 {
463    Vector3::new(
464        i32::from(i16::from_le_bytes([data[0], data[1]]) >> 2),
465        i32::from(i16::from_le_bytes([data[2], data[3]]) >> 2),
466        i32::from(i16::from_le_bytes([data[4], data[5]]) & !1),
467    )
468}
469
470fn gyro_range_code(range: GyroRange) -> u8 {
471    match range {
472        GyroRange::Dps2000 => 0x00,
473        GyroRange::Dps1000 => 0x01,
474        GyroRange::Dps500 => 0x02,
475        GyroRange::Dps250 => 0x03,
476        GyroRange::Dps125 => 0x04,
477    }
478}
479
480fn atan2_centidegrees(y: i32, x: i32) -> i32 {
481    // Fast integer approximation adequate for UI compass hints.
482    let abs_y = y.abs();
483    let angle = if x >= 0 {
484        let r = ((x - abs_y) * 1000) / (x + abs_y).max(1);
485        4500 - (4500 * r / 1000)
486    } else {
487        let r = ((x + abs_y) * 1000) / (abs_y - x).max(1);
488        13_500 - (4500 * r / 1000)
489    };
490    if y < 0 { -angle } else { angle }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn detects_motion_threshold() {
499        assert!(motion_detected(
500            Vector3::new(0, 0, 0),
501            Vector3::new(101, 0, 0),
502            MotionThreshold {
503                accel_delta_mg: 100
504            }
505        ));
506        assert!(!motion_detected(
507            Vector3::new(0, 0, 0),
508            Vector3::new(10, 0, 0),
509            MotionThreshold {
510                accel_delta_mg: 100
511            }
512        ));
513    }
514
515    #[test]
516    fn heading_handles_cardinal_directions() {
517        assert_eq!(heading_centidegrees(Vector3::new(1, 0, 0)), Some(0));
518        assert_eq!(heading_centidegrees(Vector3::new(0, 1, 0)), Some(9000));
519    }
520
521    #[test]
522    fn decodes_bmm150_aux_window() {
523        let data = [0x04, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00];
524        assert_eq!(decode_bmm150_mag_data(data), Vector3::new(1, 2, 6));
525    }
526}