Skip to main content

oxstack_drivers/bmi270/
mod.rs

1//! Bosch BMI270 6-axis IMU (accelerometer + gyroscope) over I²C.
2//!
3//! The BMI270 boots without firmware for its feature engine; until the host
4//! uploads Bosch's 8 KB configuration blob it doesn't produce data. The
5//! blob ([`CONFIG_FILE`]) is linked into flash once and streamed to the chip
6//! in [`UPLOAD_CHUNK`]-byte pieces, so a shared bus is never held for long.
7//!
8//! Only polling is supported; the interrupt pins are not used.
9
10use embedded_hal_async::delay::DelayNs;
11use embedded_hal_async::i2c::{I2c, Operation};
12
13/// I²C address with SDO pulled high.
14pub const ADDRESS_SDO_HIGH: u8 = 0x69;
15/// I²C address with SDO pulled low.
16pub const ADDRESS_SDO_LOW: u8 = 0x68;
17/// Value of the `CHIP_ID` register.
18pub const CHIP_ID: u8 = 0x24;
19
20/// Bosch's configuration file for the BMI270 feature engine, from
21/// `BMI270_SensorAPI` commit `41129fcfe39c` (BSD-3-Clause, © Bosch
22/// Sensortec; see `LICENSE-BOSCH` next to this file).
23pub static CONFIG_FILE: &[u8; 8192] = include_bytes!("config.bin");
24
25/// Bytes written per I²C transaction during the config upload. Must be even
26/// (the chip addresses its init memory in 16-bit words).
27pub const UPLOAD_CHUNK: usize = 256;
28const _: () =
29    assert!(UPLOAD_CHUNK.is_multiple_of(2) && CONFIG_FILE.len().is_multiple_of(UPLOAD_CHUNK));
30
31mod reg {
32    pub const CHIP_ID: u8 = 0x00;
33    pub const ACC_X_LSB: u8 = 0x0C;
34    pub const INTERNAL_STATUS: u8 = 0x21;
35    pub const TEMPERATURE_0: u8 = 0x22;
36    pub const ACC_CONF: u8 = 0x40;
37    pub const ACC_RANGE: u8 = 0x41;
38    pub const GYR_CONF: u8 = 0x42;
39    pub const GYR_RANGE: u8 = 0x43;
40    pub const INIT_CTRL: u8 = 0x59;
41    pub const INIT_ADDR_0: u8 = 0x5B;
42    pub const INIT_DATA: u8 = 0x5E;
43    pub const PWR_CONF: u8 = 0x7C;
44    pub const PWR_CTRL: u8 = 0x7D;
45    pub const CMD: u8 = 0x7E;
46}
47
48const CMD_SOFT_RESET: u8 = 0xB6;
49/// `PWR_CTRL`: gyroscope, accelerometer and temperature sensor on.
50const PWR_CTRL_GYR_ACC_TEMP: u8 = 0x0E;
51/// `PWR_CONF`: advanced power save off, FIFO self-wakeup on (Bosch default
52/// for performance mode).
53const PWR_CONF_PERFORMANCE: u8 = 0x02;
54/// `INTERNAL_STATUS.message` value meaning the config upload was accepted.
55const STATUS_INIT_OK: u8 = 0x01;
56
57/// Accelerometer full-scale range.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub enum AccelRange {
61    /// ±2 g
62    G2,
63    /// ±4 g
64    G4,
65    /// ±8 g
66    G8,
67    /// ±16 g
68    G16,
69}
70
71impl AccelRange {
72    const fn lsb_per_g(self) -> f32 {
73        match self {
74            AccelRange::G2 => 16384.0,
75            AccelRange::G4 => 8192.0,
76            AccelRange::G8 => 4096.0,
77            AccelRange::G16 => 2048.0,
78        }
79    }
80}
81
82/// Gyroscope full-scale range.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84#[cfg_attr(feature = "defmt", derive(defmt::Format))]
85pub enum GyroRange {
86    /// ±2000 °/s
87    Dps2000,
88    /// ±1000 °/s
89    Dps1000,
90    /// ±500 °/s
91    Dps500,
92    /// ±250 °/s
93    Dps250,
94    /// ±125 °/s
95    Dps125,
96}
97
98impl GyroRange {
99    const fn lsb_per_dps(self) -> f32 {
100        match self {
101            GyroRange::Dps2000 => 16.384,
102            GyroRange::Dps1000 => 32.768,
103            GyroRange::Dps500 => 65.536,
104            GyroRange::Dps250 => 131.072,
105            GyroRange::Dps125 => 262.144,
106        }
107    }
108}
109
110/// Output data rate shared by both sensors.
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112#[cfg_attr(feature = "defmt", derive(defmt::Format))]
113#[allow(missing_docs)]
114pub enum DataRate {
115    Hz25 = 0x06,
116    Hz50 = 0x07,
117    Hz100 = 0x08,
118    Hz200 = 0x09,
119    Hz400 = 0x0A,
120    Hz800 = 0x0B,
121    Hz1600 = 0x0C,
122}
123
124/// Sensor configuration applied by [`Bmi270::init`].
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126#[cfg_attr(feature = "defmt", derive(defmt::Format))]
127pub struct Config {
128    /// Accelerometer range.
129    pub accel_range: AccelRange,
130    /// Gyroscope range.
131    pub gyro_range: GyroRange,
132    /// Sampling rate of both sensors.
133    pub rate: DataRate,
134}
135
136impl Default for Config {
137    /// ±8 g, ±2000 °/s, 100 Hz: the chip's own reset defaults.
138    fn default() -> Self {
139        Self {
140            accel_range: AccelRange::G8,
141            gyro_range: GyroRange::Dps2000,
142            rate: DataRate::Hz100,
143        }
144    }
145}
146
147impl Config {
148    /// `ACC_CONF`: performance filter, normal bandwidth, rate.
149    const fn acc_conf(&self) -> u8 {
150        0x80 | (0x02 << 4) | self.rate as u8
151    }
152
153    /// `GYR_CONF`: performance filter, normal bandwidth, rate.
154    const fn gyr_conf(&self) -> u8 {
155        0x80 | (0x02 << 4) | self.rate as u8
156    }
157}
158
159/// Raw two's-complement readings, X/Y/Z.
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
161#[cfg_attr(feature = "defmt", derive(defmt::Format))]
162pub struct RawSample {
163    /// Accelerometer.
164    pub accel: [i16; 3],
165    /// Gyroscope.
166    pub gyro: [i16; 3],
167}
168
169/// Scaled readings, X/Y/Z.
170#[derive(Clone, Copy, Debug, Default, PartialEq)]
171#[cfg_attr(feature = "defmt", derive(defmt::Format))]
172pub struct Sample {
173    /// Acceleration in g.
174    pub accel: [f32; 3],
175    /// Angular rate in degrees per second.
176    pub gyro: [f32; 3],
177}
178
179/// Driver errors.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181#[cfg_attr(feature = "defmt", derive(defmt::Format))]
182pub enum Error<E> {
183    /// Bus error.
184    I2c(E),
185    /// `CHIP_ID` was not [`CHIP_ID`].
186    WrongChip(u8),
187    /// The chip did not accept the config file; holds `INTERNAL_STATUS`.
188    InitFailed(u8),
189}
190
191impl<E> From<E> for Error<E> {
192    fn from(e: E) -> Self {
193        Error::I2c(e)
194    }
195}
196
197/// BMI270 driver.
198pub struct Bmi270<I2C> {
199    i2c: I2C,
200    addr: u8,
201    config: Config,
202}
203
204impl<I2C: I2c> Bmi270<I2C> {
205    /// Wraps a bus handle. Call [`init`](Self::init) before reading.
206    pub const fn new(i2c: I2C, addr: u8) -> Self {
207        Self {
208            i2c,
209            addr,
210            config: Config {
211                accel_range: AccelRange::G8,
212                gyro_range: GyroRange::Dps2000,
213                rate: DataRate::Hz100,
214            },
215        }
216    }
217
218    /// Reads `CHIP_ID`.
219    pub async fn chip_id(&mut self) -> Result<u8, I2C::Error> {
220        self.read_u8(reg::CHIP_ID).await
221    }
222
223    /// Resets the chip, uploads [`CONFIG_FILE`], and starts both sensors
224    /// with `config`. Takes roughly 250 ms at 400 kHz, mostly the upload.
225    pub async fn init(
226        &mut self,
227        config: Config,
228        delay: &mut impl DelayNs,
229    ) -> Result<(), Error<I2C::Error>> {
230        let id = self.chip_id().await?;
231        if id != CHIP_ID {
232            return Err(Error::WrongChip(id));
233        }
234
235        self.write(reg::CMD, CMD_SOFT_RESET).await?;
236        delay.delay_ms(2).await;
237        // Advanced power save must be off while loading (datasheet: 450 µs).
238        self.write(reg::PWR_CONF, 0x00).await?;
239        delay.delay_us(450).await;
240
241        self.write(reg::INIT_CTRL, 0x00).await?;
242        for (i, chunk) in CONFIG_FILE.chunks(UPLOAD_CHUNK).enumerate() {
243            let word = (i * UPLOAD_CHUNK / 2) as u16;
244            // INIT_ADDR_0 holds word bits 0..3, INIT_ADDR_1 bits 4..11.
245            let addr = [reg::INIT_ADDR_0, (word & 0x0F) as u8, (word >> 4) as u8];
246            self.i2c.write(self.addr, &addr).await?;
247            // Register byte and payload as one burst, without copying the
248            // chunk out of flash.
249            self.i2c
250                .transaction(
251                    self.addr,
252                    &mut [Operation::Write(&[reg::INIT_DATA]), Operation::Write(chunk)],
253                )
254                .await?;
255        }
256        self.write(reg::INIT_CTRL, 0x01).await?;
257
258        // The datasheet allows up to 20 ms for initialisation.
259        let mut status = 0;
260        for _ in 0..25 {
261            delay.delay_ms(1).await;
262            status = self.read_u8(reg::INTERNAL_STATUS).await? & 0x0F;
263            if status == STATUS_INIT_OK {
264                break;
265            }
266        }
267        if status != STATUS_INIT_OK {
268            return Err(Error::InitFailed(status));
269        }
270
271        self.configure(config).await
272    }
273
274    /// Changes ranges and rate. The chip must already be initialised.
275    pub async fn configure(&mut self, config: Config) -> Result<(), Error<I2C::Error>> {
276        self.write(reg::ACC_CONF, config.acc_conf()).await?;
277        self.write(reg::ACC_RANGE, config.accel_range as u8).await?;
278        self.write(reg::GYR_CONF, config.gyr_conf()).await?;
279        self.write(reg::GYR_RANGE, config.gyro_range as u8).await?;
280        self.write(reg::PWR_CTRL, PWR_CTRL_GYR_ACC_TEMP).await?;
281        self.write(reg::PWR_CONF, PWR_CONF_PERFORMANCE).await?;
282        self.config = config;
283        Ok(())
284    }
285
286    /// Reads both sensors in one 12-byte burst.
287    pub async fn read_raw(&mut self) -> Result<RawSample, I2C::Error> {
288        let mut b = [0u8; 12];
289        self.i2c
290            .write_read(self.addr, &[reg::ACC_X_LSB], &mut b)
291            .await?;
292        let v = |i: usize| i16::from_le_bytes([b[i], b[i + 1]]);
293        Ok(RawSample {
294            accel: [v(0), v(2), v(4)],
295            gyro: [v(6), v(8), v(10)],
296        })
297    }
298
299    /// Reads both sensors, scaled to g and °/s.
300    pub async fn read(&mut self) -> Result<Sample, I2C::Error> {
301        let raw = self.read_raw().await?;
302        Ok(self.scale(raw))
303    }
304
305    /// Converts a raw sample using the current ranges.
306    pub fn scale(&self, raw: RawSample) -> Sample {
307        let (a, g) = (
308            self.config.accel_range.lsb_per_g(),
309            self.config.gyro_range.lsb_per_dps(),
310        );
311        Sample {
312            accel: raw.accel.map(|v| f32::from(v) / a),
313            gyro: raw.gyro.map(|v| f32::from(v) / g),
314        }
315    }
316
317    /// Die temperature in °C, or `None` while no valid reading exists yet.
318    pub async fn temperature(&mut self) -> Result<Option<f32>, I2C::Error> {
319        let mut b = [0u8; 2];
320        self.i2c
321            .write_read(self.addr, &[reg::TEMPERATURE_0], &mut b)
322            .await?;
323        let raw = i16::from_le_bytes(b);
324        // 0x8000 marks "invalid"; otherwise 1/512 K per LSB around 23 °C.
325        Ok((raw != i16::MIN).then(|| 23.0 + f32::from(raw) / 512.0))
326    }
327
328    /// Returns the bus handle.
329    pub fn release(self) -> I2C {
330        self.i2c
331    }
332
333    async fn write(&mut self, reg: u8, value: u8) -> Result<(), I2C::Error> {
334        self.i2c.write(self.addr, &[reg, value]).await
335    }
336
337    async fn read_u8(&mut self, reg: u8) -> Result<u8, I2C::Error> {
338        let mut b = [0];
339        self.i2c.write_read(self.addr, &[reg], &mut b).await?;
340        Ok(b[0])
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    extern crate std;
347    use super::*;
348    use crate::test_util::block_on;
349    use core::convert::Infallible;
350    use std::vec::Vec;
351
352    /// Register-level model: tracks the init-memory upload and reports
353    /// INIT_OK only if the full blob arrived in order.
354    struct FakeImu {
355        regs: [u8; 128],
356        init_mem: Vec<u8>,
357        sample: [u8; 12],
358    }
359
360    impl FakeImu {
361        fn new() -> Self {
362            let mut regs = [0; 128];
363            regs[usize::from(reg::CHIP_ID)] = CHIP_ID;
364            Self {
365                regs,
366                init_mem: Vec::new(),
367                sample: [0; 12],
368            }
369        }
370
371        fn write(&mut self, bytes: &[u8]) {
372            let (start, data) = (bytes[0], &bytes[1..]);
373            if start == reg::INIT_DATA {
374                let word = usize::from(self.regs[0x5B] & 0x0F) | usize::from(self.regs[0x5C]) << 4;
375                assert_eq!(
376                    word * 2,
377                    self.init_mem.len(),
378                    "chunk written at the wrong offset"
379                );
380                self.init_mem.extend_from_slice(data);
381                return;
382            }
383            for (i, v) in data.iter().enumerate() {
384                self.regs[usize::from(start) + i] = *v;
385            }
386            if start == reg::INIT_CTRL && data == [0x01] && self.init_mem.as_slice() == CONFIG_FILE
387            {
388                self.regs[usize::from(reg::INTERNAL_STATUS)] = STATUS_INIT_OK;
389            }
390        }
391    }
392
393    impl embedded_hal_async::i2c::ErrorType for FakeImu {
394        type Error = Infallible;
395    }
396
397    impl I2c for FakeImu {
398        async fn transaction(
399            &mut self,
400            addr: u8,
401            ops: &mut [Operation<'_>],
402        ) -> Result<(), Infallible> {
403            assert_eq!(addr, ADDRESS_SDO_HIGH);
404            // Adjacent writes form one burst, as on a real bus.
405            let mut burst = Vec::new();
406            let mut ptr = 0;
407            for op in ops.iter_mut() {
408                match op {
409                    Operation::Write(b) => burst.extend_from_slice(b),
410                    Operation::Read(out) => {
411                        if !burst.is_empty() {
412                            ptr = burst[0];
413                            burst.clear();
414                        }
415                        for (i, o) in out.iter_mut().enumerate() {
416                            let r = usize::from(ptr) + i;
417                            *o = if (0x0C..0x18).contains(&r) {
418                                self.sample[r - 0x0C]
419                            } else {
420                                self.regs[r]
421                            };
422                        }
423                    }
424                }
425            }
426            if burst.len() > 1 {
427                self.write(&burst);
428            }
429            Ok(())
430        }
431    }
432
433    struct NoDelay;
434    impl DelayNs for NoDelay {
435        async fn delay_ns(&mut self, _ns: u32) {}
436    }
437
438    #[test]
439    fn init_uploads_whole_config_and_configures() {
440        let mut imu = Bmi270::new(FakeImu::new(), ADDRESS_SDO_HIGH);
441        let cfg = Config {
442            accel_range: AccelRange::G4,
443            gyro_range: GyroRange::Dps500,
444            rate: DataRate::Hz200,
445        };
446        block_on(imu.init(cfg, &mut NoDelay)).unwrap();
447        let chip = imu.release();
448        assert_eq!(chip.init_mem.len(), 8192);
449        assert_eq!(chip.regs[usize::from(reg::ACC_CONF)], 0xA9);
450        assert_eq!(chip.regs[usize::from(reg::ACC_RANGE)], 0x01);
451        assert_eq!(chip.regs[usize::from(reg::GYR_RANGE)], 0x02);
452        assert_eq!(chip.regs[usize::from(reg::PWR_CTRL)], 0x0E);
453    }
454
455    #[test]
456    fn init_rejects_other_chips() {
457        let mut fake = FakeImu::new();
458        fake.regs[0] = 0x26;
459        let mut imu = Bmi270::new(fake, ADDRESS_SDO_HIGH);
460        assert_eq!(
461            block_on(imu.init(Config::default(), &mut NoDelay)),
462            Err(Error::WrongChip(0x26))
463        );
464    }
465
466    #[test]
467    fn samples_are_little_endian_and_scaled() {
468        let mut fake = FakeImu::new();
469        // accel z = +4096 (1 g at ±8 g); gyro x = -164 (≈ -10 °/s at ±2000).
470        fake.sample[4..6].copy_from_slice(&4096i16.to_le_bytes());
471        fake.sample[6..8].copy_from_slice(&(-164i16).to_le_bytes());
472        let mut imu = Bmi270::new(fake, ADDRESS_SDO_HIGH);
473        let s = block_on(imu.read()).unwrap();
474        assert_eq!(s.accel, [0.0, 0.0, 1.0]);
475        assert!((s.gyro[0] + 10.0).abs() < 0.01);
476    }
477}