Skip to main content

bmp390/
lib.rs

1//! The BMP390 is a digital sensor with pressure and temperature measurement based on proven sensing principles. The
2//! sensor is more accurate than its predecessor BMP380, covering a wider measurement range. It offers new interrupt
3//! functionality, lower power consumption, and a new FIFO functionality. The integrated 512 byte FIFO buffer supports
4//! low power applications and prevents data loss in non-real-time systems.
5//!
6//! [`Bmp390`] is a driver for the BMP390 sensor. It provides methods to read the temperature and pressure from the
7//! sensor over [I2C](https://en.wikipedia.org/wiki/I%C2%B2C). It is built on top of the [`embedded_hal_async::i2c`]
8//! traits to be compatible with a wide range of embedded platforms. Measurements utilize the [`uom`] crate to provide
9//! automatic, type-safe, and zero-cost units of measurement for [`Measurement`].
10//!
11//! # Example
12//! ```no_run
13//! # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
14//! # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
15//! use bmp390::Bmp390;
16//! let config = bmp390::Configuration::default();
17//! # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
18//! # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
19//! let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
20//! let measurement = sensor.measure().await?;
21//! defmt::info!("Measurement: {}", measurement);
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! # Datasheet
27//! The [BMP390 Datasheet](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp390-ds002.pdf)
28//! contains detailed information about the sensor's features, electrical characteristics, and registers. This package
29//! implements the functionality described in the datasheet and references the relevant sections in the documentation.
30//!
31//! # Synchronous API
32//! The synchronous API is available behind the `sync` feature flag. It's driver is [`sync::Bmp390`] and functions
33//! similarly to the asynchronous driver, but with synchronous methods.
34//!
35//! By default, the synchronous API is disabled.
36
37#![no_std]
38#![cfg_attr(docsrs, feature(doc_cfg))]
39
40use defmt::{debug, trace, Format};
41use embedded_hal_async::{delay::DelayNs, i2c::I2c};
42use libm::powf;
43use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
44use uom::si::length::{foot, meter};
45use uom::si::pressure::{hectopascal, pascal};
46use uom::si::thermodynamic_temperature::degree_celsius;
47
48mod registers;
49
50#[cfg(feature = "sync")]
51pub mod sync;
52
53pub use registers::*;
54
55/// Errors that can occur when communicating with the BMP390 barometer.
56#[derive(Debug, Clone, Copy, Format)]
57pub enum Error<E> {
58    /// An error occurred while communicating with the BMP390 over I2C. The inner error contains the specific error.
59    I2c(E),
60
61    /// The BMP390's chip ID did not match the expected value of `0x60`. The actual chip ID is provided.
62    WrongChip(u8),
63
64    /// A fatal error occurred on the BMP390. See [`ErrReg`] for more.
65    Fatal,
66
67    /// A command error occurred on the BMP390. See [`ErrReg`] for more.
68    Command,
69
70    /// A configuration error occurred on the BMP390. See [`ErrReg`] for more.
71    Configuration,
72}
73
74/// Note: [`embedded_hal_async::i2c::ErrorKind`] is an alias for [`embedded_hal::i2c::ErrorKind`], so the one impl
75/// covers both.
76impl From<embedded_hal_async::i2c::ErrorKind> for Error<embedded_hal_async::i2c::ErrorKind> {
77    fn from(error: embedded_hal_async::i2c::ErrorKind) -> Self {
78        Error::I2c(error)
79    }
80}
81
82/// A single measurement from the [`Bmp390`] barometer.
83///
84/// Measurements utilize the [`uom`] crate to provide automatic, type-safe, and zero-cost units of measurement.
85///
86/// # Example
87/// ```
88/// # use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
89/// # use uom::si::pressure::pascal;
90/// # use uom::si::length::meter;
91/// # use uom::si::thermodynamic_temperature::degree_celsius;
92/// let measurement = bmp390::Measurement {
93///    pressure: Pressure::new::<pascal>(90_240.81),
94///    temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
95///    altitude: Length::new::<meter>(1000.0),
96/// };
97///
98/// defmt::info!("Measurement: {}", measurement);
99/// ```
100///
101/// Note: these examples show creation of [`Measurement`] structs directly. In practice you would receive these from
102/// [`Bmp390::measure`].
103///
104/// Conversion between units is easy with the [`uom`] crate. For example, to convert to imperial units:
105/// ```
106/// # use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
107/// # use uom::si::pressure::pascal;
108/// # use uom::si::length::meter;
109/// # use uom::si::thermodynamic_temperature::degree_celsius;
110/// # let measurement = bmp390::Measurement {
111/// #    pressure: Pressure::new::<pascal>(90_240.81),
112/// #    temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
113/// #    altitude: Length::new::<meter>(1000.0),
114/// # };
115/// use uom::si::pressure::millimeter_of_mercury;
116/// use uom::si::thermodynamic_temperature::degree_fahrenheit;
117/// use uom::si::length::foot;
118///
119/// // "Pressure: 676.9753 mmHg, Temperature: 77 °F, Altitude: 3280.84 feet"
120/// defmt::info!("Pressure: {} mmHg, temperature: {} °F, altitude: {} feet",
121///     measurement.pressure.get::<millimeter_of_mercury>(),
122///     measurement.temperature.get::<degree_fahrenheit>(),
123///     measurement.altitude.get::<foot>());
124/// ```
125#[derive(Debug, Clone, Copy)]
126pub struct Measurement {
127    /// The pressure as a [`Pressure`], allowing for easy conversion to any unit of pressure.
128    ///
129    /// # Example
130    /// ```
131    /// # use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
132    /// # use uom::si::pressure::pascal;
133    /// # use uom::si::length::meter;
134    /// # use uom::si::thermodynamic_temperature::degree_celsius;
135    /// use uom::si::pressure::millimeter_of_mercury;
136    /// let measurement = bmp390::Measurement {
137    ///    pressure: Pressure::new::<pascal>(90_240.81),
138    ///    temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
139    ///    altitude: Length::new::<meter>(1000.0),
140    /// };
141    ///
142    /// // "Pressure: 676.9753 mmHg"
143    /// defmt::info!("Pressure: {} mmHg", measurement.pressure.get::<millimeter_of_mercury>());
144    /// ```
145    pub pressure: Pressure,
146
147    /// The temperature as a [`ThermodynamicTemperature`], allowing for easy conversion to any unit of temperature.
148    ///
149    /// # Example
150    /// ```
151    /// # use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
152    /// # use uom::si::pressure::pascal;
153    /// # use uom::si::length::meter;
154    /// # use uom::si::thermodynamic_temperature::degree_celsius;
155    /// use uom::si::thermodynamic_temperature::degree_fahrenheit;
156    /// let measurement = bmp390::Measurement {
157    ///    pressure: Pressure::new::<pascal>(90_240.81),
158    ///    temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
159    ///    altitude: Length::new::<meter>(1000.0),
160    /// };
161    ///
162    /// // "Temperature: 77 °F"
163    /// defmt::info!("Temperature: {} °F", measurement.temperature.get::<degree_fahrenheit>());
164    /// ```
165    pub temperature: ThermodynamicTemperature,
166
167    /// The altitude as a [`Length`], allowing for easy conversion to any unit of length.
168    ///
169    /// # Example
170    /// ```
171    /// # use uom::si::f32::{Length, Pressure, ThermodynamicTemperature};
172    /// # use uom::si::pressure::pascal;
173    /// # use uom::si::length::meter;
174    /// # use uom::si::thermodynamic_temperature::degree_celsius;
175    /// use uom::si::length::foot;
176    /// let measurement = bmp390::Measurement {
177    ///    pressure: Pressure::new::<pascal>(90_240.81),
178    ///    temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
179    ///    altitude: Length::new::<meter>(1000.0),
180    /// };
181    ///
182    /// // "Length: 3280.84 feet"
183    /// defmt::info!("Length: {} feet", measurement.altitude.get::<foot>());
184    /// ```
185    pub altitude: Length,
186}
187
188impl Format for Measurement {
189    fn format(&self, f: defmt::Formatter) {
190        defmt::write!(
191            f,
192            "Pressure: {} Pa, Temperature: {} °C, Altitude: {} m",
193            self.pressure.get::<pascal>(),
194            self.temperature.get::<degree_celsius>(),
195            self.altitude.get::<meter>()
196        );
197    }
198}
199
200impl core::fmt::Display for Measurement {
201    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
202        write!(
203            f,
204            "Pressure: {} Pa, Temperature: {} °C, Altitude: {} m",
205            self.pressure.get::<pascal>(),
206            self.temperature.get::<degree_celsius>(),
207            self.altitude.get::<meter>(),
208        )
209    }
210}
211
212/// The BMP390 barometer's I2C addresses, either `0x76` or `0x77`.
213///
214///  The BMP390 can be configured to use two different addresses by either pulling the `SDO` pin down to `GND`
215/// (`0x76` via [`Address::Down`]) or up to `V_DDIO` (`0x77` via [`Address::Up`]).
216#[derive(Debug, Clone, Copy, Format)]
217pub enum Address {
218    /// `0x76`: The BMP390's address when `SDO` is pulled up to `GND`.
219    Down = 0x76,
220
221    /// `0x77`: The BMP390's address when `SDO` is pulled down to `V_DDIO`
222    Up = 0x77,
223}
224
225impl From<Address> for u8 {
226    /// Convert the address to a [`u8`] for I2C communication.
227    fn from(address: Address) -> u8 {
228        address as u8
229    }
230}
231
232/// Output from the BMP390 consists of ADC outputs.
233///
234/// These must be compensated using formulas from the datasheet to obtain the actual temperature and pressure values,
235/// using coefficients stored in non-volatile memory (NVM).
236///
237/// # Datasheet
238/// - Section 3.11 Output compensation.
239/// - Appendix A: Computation formulae reference implementation.
240#[derive(Debug, Clone, Copy, Format)]
241struct CalibrationCoefficients {
242    par_t1: f32,
243    par_t2: f32,
244    par_t3: f32,
245    par_p1: f32,
246    par_p2: f32,
247    par_p3: f32,
248    par_p4: f32,
249    par_p5: f32,
250    par_p6: f32,
251    par_p7: f32,
252    par_p8: f32,
253    par_p9: f32,
254    par_p10: f32,
255    par_p11: f32,
256}
257
258impl CalibrationCoefficients {
259    /// Read the calibration coefficients from the BMP390's NVM registers and convert them to into a set of
260    /// floating-point calibration coefficients for the formulas implemented in the compensation functions.
261    async fn try_from_i2c<I: I2c>(address: Address, i2c: &mut I) -> Result<Self, Error<I::Error>> {
262        let mut calibration_coefficient_regs = [0; 21];
263        i2c.write_read(
264            address.into(),
265            &Self::write_read_write_transaction(),
266            &mut calibration_coefficient_regs,
267        )
268        .await
269        .map_err(Error::I2c)?;
270
271        Ok(Self::from_registers(&calibration_coefficient_regs))
272    }
273
274    /// Calculate the calibration coefficients from the raw register data in registers [`Register::NVM_PAR_T1_0`] to
275    /// [`Register::NVM_PAR_P11`].
276    ///
277    /// # Datasheet
278    /// Apendix A, Section 8.4
279    fn from_registers(data: &[u8; 21]) -> Self {
280        trace!("NVM_PAR: {=[u8]:#04x}", *data);
281        let nvm_par_t1: u16 = (data[1] as u16) << 8 | data[0] as u16;
282        let nvm_par_t2: u16 = (data[3] as u16) << 8 | data[2] as u16;
283        let nvm_par_t3: i8 = data[4] as i8;
284        let nvm_par_p1: i16 = (data[6] as i16) << 8 | data[5] as i16;
285        let nvm_par_p2: i16 = (data[8] as i16) << 8 | data[7] as i16;
286        let nvm_par_p3: i8 = data[9] as i8;
287        let nvm_par_p4: i8 = data[10] as i8;
288        let nvm_par_p5: u16 = (data[12] as u16) << 8 | data[11] as u16;
289        let nvm_par_p6: u16 = (data[14] as u16) << 8 | data[13] as u16;
290        let nvm_par_p7: i8 = data[15] as i8;
291        let nvm_par_p8: i8 = data[16] as i8;
292        let nvm_par_p9: i16 = (data[18] as i16) << 8 | data[17] as i16;
293        let nvm_par_p10: i8 = data[19] as i8;
294        let nvm_par_p11: i8 = data[20] as i8;
295
296        Self {
297            par_t1: (nvm_par_t1 as f32) / 0.003_906_25,    // 2^-8
298            par_t2: (nvm_par_t2 as f32) / 1_073_741_824.0, // 2^30
299            par_t3: (nvm_par_t3 as f32) / 281_474_976_710_656.0, // 2^48
300            par_p1: ((nvm_par_p1 as f32) - 16_384.0) / 1_048_576.0, // 2^14 / 2^20
301            par_p2: ((nvm_par_p2 as f32) - 16_384.0) / 536_870_912.0, // 2^14 / 2^29
302            par_p3: (nvm_par_p3 as f32) / 4_294_967_296.0, // 2^32
303            par_p4: (nvm_par_p4 as f32) / 137_438_953_472.0, // 2^37
304            par_p5: (nvm_par_p5 as f32) / 0.125,           // 2^-3
305            par_p6: (nvm_par_p6 as f32) / 64.0,            // 2^6
306            par_p7: (nvm_par_p7 as f32) / 256.0,           // 2^8
307            par_p8: (nvm_par_p8 as f32) / 32768.0,         // 2^15
308            par_p9: (nvm_par_p9 as f32) / 281_474_976_710_656.0, //2^48
309            par_p10: (nvm_par_p10 as f32) / 281_474_976_710_656.0, // 2^48
310            par_p11: (nvm_par_p11 as f32) / 36_893_488_147_419_103_232.0, // 2^65
311        }
312    }
313
314    /// Compensate a temperature reading according to calibration coefficients.
315    ///
316    /// # Datasheet
317    /// Apendix A, Section 8.5
318    fn compensate_temperature(&self, temperature_uncompensated: u32) -> ThermodynamicTemperature {
319        // This could be done in fewer expressions, but it's broken down for clarity and to match the datasheet
320        let uncompensated = temperature_uncompensated as f32;
321        let partial_data1 = uncompensated - self.par_t1;
322        let partial_data2 = partial_data1 * self.par_t2;
323        let temperature = partial_data2 + (partial_data1 * partial_data1) * self.par_t3;
324        ThermodynamicTemperature::new::<degree_celsius>(temperature)
325    }
326
327    /// Compensate a pressure reading according to calibration coefficients.
328    ///
329    /// # Datasheet
330    /// Apendix A, Section 8.6
331    fn compensate_pressure(
332        &self,
333        temperature: ThermodynamicTemperature,
334        pressure_uncompensated: u32,
335    ) -> Pressure {
336        // This could be done in fewer expressions, but it's broken down for clarity and to match the datasheet
337        let uncompensated = pressure_uncompensated as f32;
338        let temperature = temperature.get::<degree_celsius>();
339        let partial_data1 = self.par_p6 * temperature;
340        let partial_data2 = self.par_p7 * temperature * temperature;
341        let partial_data3 = self.par_p8 * temperature * temperature * temperature;
342        let partial_out1 = self.par_p5 + partial_data1 + partial_data2 + partial_data3;
343
344        let partial_data1 = self.par_p2 * temperature;
345        let partial_data2 = self.par_p3 * temperature * temperature;
346        let partial_data3 = self.par_p4 * temperature * temperature * temperature;
347        let partial_out2 =
348            uncompensated * (self.par_p1 + partial_data1 + partial_data2 + partial_data3);
349
350        let partial_data1 = uncompensated * uncompensated;
351        let partial_data2 = self.par_p9 + self.par_p10 * temperature;
352        let partial_data3 = partial_data1 * partial_data2;
353        let partial_data4 =
354            partial_data3 + uncompensated * uncompensated * uncompensated * self.par_p11;
355
356        let pressure = partial_out1 + partial_out2 + partial_data4;
357        Pressure::new::<pascal>(pressure)
358    }
359
360    /// Gets the bytes to write in a write-read transaction to the BMP390 to read the calibration coefficients. This
361    /// must be combined with a 21-byte read in a combined write-read burst.
362    fn write_read_write_transaction() -> [u8; 1] {
363        [Register::NVM_PAR_T1_0.into()]
364    }
365}
366
367/// Configuration for the BMP390 barometer.
368#[derive(Debug, Clone, Copy, Format)]
369pub struct Configuration {
370    /// Enabling and disabling the pressure and temperature measurements and the power mode.
371    pub power_control: PowerControl,
372
373    /// The oversampling settings for pressure and temperature measurements.
374    pub oversampling: Osr,
375
376    /// The output data rate settings.
377    pub output_data_rate: Odr,
378
379    /// IIR filter coefficient settings.
380    pub iir_filter: Config,
381}
382
383impl Default for Configuration {
384    /// Default configuration for the BMP390 barometer. This configuration enables pressure and temperature measurement
385    /// with normal power mode, x8 oversampling for pressure and x1 oversampling for temperature, an output data rate of
386    /// 50 Hz, and a IIR filter coefficient of 4. This corresponds to a "standard resolution" configuration as
387    /// recommended by the datasheet Section 3.5. Filter selection.
388    fn default() -> Self {
389        Self {
390            power_control: PowerControl {
391                enable_pressure: true,
392                enable_temperature: true,
393                mode: PowerMode::Normal,
394            },
395            oversampling: Osr {
396                pressure: Oversampling::X8,
397                temperature: Oversampling::X1,
398            },
399            output_data_rate: Odr {
400                odr_sel: OdrSel::ODR_50,
401            },
402            iir_filter: Config {
403                iir_filter: IirFilter::coef_15,
404            },
405        }
406    }
407}
408
409impl Configuration {
410    /// Convert the configuration to a byte array that can be written to the BMP390's registers.
411    /// The byte array contains both the register address and the register value.
412    pub fn to_write_bytes(&self) -> [u8; 8] {
413        [
414            Register::PWR_CTRL.into(),
415            self.power_control.into(),
416            Register::OSR.into(),
417            self.oversampling.into(),
418            Register::ODR.into(),
419            self.output_data_rate.into(),
420            Register::CONFIG.into(),
421            self.iir_filter.into(),
422        ]
423    }
424}
425
426/// A driver for the BMP390 pressure sensor over any [`I2c`] implementation.
427///
428/// This driver utilizes [`uom`] to provide automatic, type-safe, and zero-cost units of measurement. Measurements can
429/// be retrieved with [`Bmp390::measure`], which returns a [`Measurement`] struct containing the pressure, temperature,
430/// and altitude. The altitude is calculated based on the current pressure, standard atmospheric pressure at sea level,
431/// and a reference altitude, which can be set with [`Bmp390::set_reference_altitude`]. The reference altitude defaults
432/// to zero, so the default altitude is measured from sea level.
433///
434/// # Example
435/// ```no_run
436/// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
437/// use bmp390::Bmp390;
438/// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
439/// let config = bmp390::Configuration::default();
440/// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
441/// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
442/// let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
443/// let measurement = sensor.measure().await?;
444/// defmt::info!("Measurement: {}", measurement);
445/// # Ok(())
446/// # }
447/// ```
448pub struct Bmp390<I> {
449    /// The I2C bus the barometer is connected to.
450    i2c: I,
451
452    /// The I2C address of the barometer.
453    address: Address,
454
455    /// The calibration coefficients for the barometer to compensate temperature and pressure measurements.
456    coefficients: CalibrationCoefficients,
457
458    /// The reference altitude for altitude calculations.
459    ///
460    /// By default, this is zero. set to the standard atmospheric pressure at sea level, 1013.25 hPa. It can be set to
461    /// a different value using [`Bmp390::set_reference_altitude`] to calculate the altitude relative to a different
462    /// reference point.
463    altitude_reference: Length,
464}
465
466impl<I, E> Bmp390<I>
467where
468    I: I2c<Error = E>,
469{
470    /// Creates a new BMP390 driver. This will initialize the barometer with the provided configuration.
471    /// It will additionally delay for 2 ms to allow the barometer to start up and read the calibration coefficients
472    /// for temperature and pressure measuring.
473    ///
474    /// # Example
475    /// ```no_run
476    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
477    /// use bmp390::Bmp390;
478    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
479    /// let config = bmp390::Configuration::default();
480    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
481    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
482    /// let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
483    /// let measurement = sensor.measure().await?;
484    /// defmt::info!("Measurement: {}", measurement);
485    /// # Ok(())
486    /// # }
487    /// ```
488    pub async fn try_new<D: DelayNs>(
489        mut i2c: I,
490        address: Address,
491        mut delay: D,
492        config: &Configuration,
493    ) -> Result<Self, Error<E>> {
494        // 2 ms time to first communication (Datsheet Section 1, Table 2)
495        delay.delay_ms(2).await;
496
497        let mut data = [0; 2];
498        i2c.write_read(address.into(), &[Register::CHIP_ID.into()], &mut data)
499            .await
500            .map_err(Error::I2c)?;
501
502        let chip_id = data[0];
503        let rev_id = data[1];
504
505        debug!("CHIP_ID = {=u8:#04x}; REV_ID = {=u8:#04x}", chip_id, rev_id);
506        if chip_id != 0x60 {
507            return Err(Error::WrongChip(chip_id));
508        }
509
510        // read Register::EVENT and INT_STATUS in a burst read to clear the event and interrupt status flags
511        let mut data = [0; 2];
512        i2c.write_read(address.into(), &[Register::EVENT.into()], &mut data)
513            .await
514            .map_err(Error::I2c)?;
515
516        // write configuration after clearing interrupt status flags so that they are accurate from here on
517        i2c.write(address.into(), &config.to_write_bytes())
518            .await
519            .map_err(Error::I2c)?;
520
521        // read Register::ERR_REG after writing config to determine if configuration was successful and to clear the error status flags
522        let mut err_reg = [0; 1];
523        i2c.write_read(address.into(), &[Register::ERR_REG.into()], &mut err_reg)
524            .await
525            .map_err(Error::I2c)
526            .and_then(move |_| {
527                let err_reg = ErrReg::from(err_reg[0]);
528                if err_reg.fatal_err {
529                    Err(Error::<E>::Fatal)
530                } else if err_reg.cmd_err {
531                    Err(Error::<E>::Command)
532                } else if err_reg.conf_err {
533                    Err(Error::<E>::Configuration)
534                } else {
535                    Ok(())
536                }
537            })?;
538
539        let coefficients = CalibrationCoefficients::try_from_i2c(address, &mut i2c).await?;
540
541        Ok(Self::new_with_coefficients(i2c, address, coefficients))
542    }
543
544    /// Creates a new BMP390 driver with known calibration coefficients.
545    fn new_with_coefficients(
546        i2c: I,
547        address: Address,
548        coefficients: CalibrationCoefficients,
549    ) -> Self {
550        Self {
551            i2c,
552            address,
553            coefficients,
554            altitude_reference: Length::new::<meter>(0.0),
555        }
556    }
557
558    /// Reads the temperature from the barometer as a [`ThermodynamicTemperature`].
559    ///
560    /// # Example
561    /// ```no_run
562    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
563    /// # use bmp390::Bmp390;
564    /// use uom::si::thermodynamic_temperature::degree_celsius;
565    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
566    /// # let config = bmp390::Configuration::default();
567    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
568    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
569    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
570    /// let temperature = sensor.temperature().await?;
571    /// defmt::info!("Temperature: {} °C", temperature.get::<degree_celsius>());
572    /// # Ok(())
573    /// # }
574    /// ```
575    pub async fn temperature(&mut self) -> Result<ThermodynamicTemperature, Error<E>> {
576        // Burst read: only address DATA_3 (temperature XLSB) and BMP390 auto-increments through DATA_5 (temperature MSB)
577        let write = &[Register::DATA_3.into()];
578        let mut read = [0; 3];
579        self.i2c
580            .write_read(self.address.into(), write, &mut read)
581            .await
582            .map_err(Error::I2c)?;
583
584        // DATA_3 is the LSB, DATA_5 is the MSB
585        let temperature = u32::from(read[0]) | u32::from(read[1]) << 8 | u32::from(read[2]) << 16;
586        let temperature = self.coefficients.compensate_temperature(temperature);
587        Ok(temperature)
588    }
589
590    /// Measures the temperature and pressure from the barometer.
591    ///
592    /// # Example
593    /// ```no_run
594    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
595    /// # use bmp390::Bmp390;
596    /// use uom::si::{pressure::hectopascal, thermodynamic_temperature::degree_celsius};
597    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
598    /// # let config = bmp390::Configuration::default();
599    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
600    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
601    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
602    /// let (temperature, pressure) = sensor.temperature_pressure().await?;
603    /// defmt::info!(
604    ///     "Temperature: {} °C, Pressure: {} hPa",
605    ///     temperature.get::<degree_celsius>(),
606    ///     pressure.get::<hectopascal>()
607    /// );
608    /// # Ok(())
609    /// # }
610    /// ```
611    pub async fn temperature_pressure(
612        &mut self,
613    ) -> Result<(ThermodynamicTemperature, Pressure), Error<E>> {
614        // Burst read: only address DATA_0 (pressure XLSB) and BMP390 auto-increments through DATA_5 (temperature MSB)
615        let write = &[Register::DATA_0.into()];
616        let mut read = [0; 6];
617        self.i2c
618            .write_read(self.address.into(), write, &mut read)
619            .await
620            .map_err(Error::I2c)?;
621
622        trace!("DATA = {=[u8]:#04x}", read);
623
624        // pressure is 0:2 (XLSB, LSB, MSB), temperature is 3:5 (XLSB, LSB, MSB)
625        let temperature = u32::from(read[3]) | u32::from(read[4]) << 8 | u32::from(read[5]) << 16;
626        let temperature = self.coefficients.compensate_temperature(temperature);
627
628        let pressure = u32::from(read[0]) | u32::from(read[1]) << 8 | u32::from(read[2]) << 16;
629        let pressure = self.coefficients.compensate_pressure(temperature, pressure);
630
631        Ok((temperature, pressure))
632    }
633
634    /// Reads the pressure from the barometer as a [`Pressure`].
635    ///
636    /// # Example
637    /// ```no_run
638    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
639    /// # use bmp390::Bmp390;
640    /// use uom::si::pressure::hectopascal;
641    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
642    /// # let config = bmp390::Configuration::default();
643    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
644    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
645    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
646    /// let pressure = sensor.pressure().await?;
647    /// defmt::info!("Pressure: {} hPa", pressure.get::<hectopascal>());
648    /// # Ok(())
649    /// # }
650    /// ```
651    pub async fn pressure(&mut self) -> Result<Pressure, Error<E>> {
652        // pressure requires temperature to compensate, so we have to measure both
653        let (_, pressure) = self.temperature_pressure().await?;
654        Ok(pressure)
655    }
656
657    /// Measures the temperature and pressure from the barometer.
658    /// Altitude is then calculated using the [NOAA formula](https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf).
659    ///
660    /// This altitude calculation can be expensive on devices without floating point hardware. In this case, consider
661    /// calling [`temperature_pressure()`] instead and using an approximation or lookup table.
662    ///
663    /// # Example
664    /// ```no_run
665    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
666    /// # use bmp390::Bmp390;
667    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
668    /// # let config = bmp390::Configuration::default();
669    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
670    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
671    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
672    /// let measurement = sensor.measure().await?;
673    /// defmt::info!("Measurement: {}", measurement);
674    /// # Ok(())
675    /// # }
676    /// ```
677    pub async fn measure(&mut self) -> Result<Measurement, Error<E>> {
678        let (temperature, pressure) = self.temperature_pressure().await?;
679
680        Ok(Measurement {
681            temperature,
682            pressure,
683            altitude: calculate_altitude(pressure, self.altitude_reference),
684        })
685    }
686
687    /// Set the reference altitude for altitude calculations.
688    ///
689    /// Following this, the altitude can be calculated using [`Bmp390::altitude`]. If the current pressure matches
690    /// the pressure when the reference altitude is set, the altitude will be 0.
691    ///
692    /// # Example
693    /// ```no_run
694    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
695    /// # use bmp390::Bmp390;
696    /// # use uom::si::length::meter;
697    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
698    /// # let config = bmp390::Configuration::default();
699    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
700    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
701    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
702    /// let initial_altitude = sensor.altitude().await?;
703    /// sensor.set_reference_altitude(initial_altitude);
704    ///
705    /// // Some time later...
706    /// let altitude = sensor.altitude().await?;
707    /// defmt::info!("Altitude: {} meters", altitude.get::<meter>());
708    /// # Ok(())
709    /// # }
710    /// ```
711    pub fn set_reference_altitude(&mut self, altitude: Length) {
712        self.altitude_reference = altitude;
713    }
714
715    /// Calculates the latest altitude measurement as a [`Length`] after retrieving the latest pressure measurement.
716    ///
717    /// The altitude is calculating following the [NOAA formula](https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf).
718    ///
719    /// # Example
720    /// ```no_run
721    /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
722    /// # use bmp390::Bmp390;
723    /// use uom::si::length::foot;
724    /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
725    /// # let config = bmp390::Configuration::default();
726    /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
727    /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
728    /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
729    /// let altitude = sensor.altitude().await?;
730    /// defmt::info!("Length: {} feet", altitude.get::<foot>());
731    /// # Ok(())
732    /// # }
733    /// ```
734    pub async fn altitude(&mut self) -> Result<Length, Error<E>> {
735        let pressure = self.pressure().await?;
736        Ok(calculate_altitude(pressure, self.altitude_reference))
737    }
738}
739
740/// Calculate the altitude based on the pressure, sea level pressure, and the reference altitude.
741///
742/// The altitude is calculating following the [NOAA formula](https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf).
743fn calculate_altitude(pressure: Pressure, altitude_reference: Length) -> Length {
744    let sea_level = Pressure::new::<hectopascal>(1013.25);
745    let above_sea_level =
746        Length::new::<foot>(145366.45 * (1.0 - powf((pressure / sea_level).value, 0.190284)));
747
748    above_sea_level - altitude_reference
749}
750
751#[cfg(test)]
752mod tests {
753    extern crate std;
754    use embedded_hal_mock::eh1::delay::{CheckedDelay, NoopDelay, Transaction as DelayTransaction};
755    use embedded_hal_mock::eh1::i2c::{Mock, Transaction as I2cTransaction};
756    use std::prelude::rust_2021::*;
757    use std::vec;
758    use uom::ConstZero;
759
760    use super::*;
761
762    /// Bytes for the DATA registers (0x04 .. 0x09) for a pressure and temperature measurement.
763    const PRESSURE_TEMPERATURE_BYTES: [u8; 6] = [0xcb, 0xb3, 0x6b, 0xd1, 0xba, 0x82];
764
765    /// The [`Measurement::pressure`] value for [`PRESSURE_TEMPERATURE_BYTES`] when compensated by [`CalibrationCoefficients::default()`].
766    fn expected_pressure() -> Pressure {
767        Pressure::new::<pascal>(98370.55)
768    }
769
770    /// Bytes for the DATA registers (0x07 .. 0x09) for a temperature measurement.
771    const TEMPERATURE_BYTES: [u8; 3] = [0xd1, 0xba, 0x82];
772
773    /// The [`Measurement::temperature`] value for [`TEMPERATURE_BYTES`] when compensated by [`CalibrationCoefficients::default()`].
774    fn expected_temperature() -> ThermodynamicTemperature {
775        ThermodynamicTemperature::new::<degree_celsius>(25.770_746)
776    }
777
778    /// The [`Measurement::altitude`] value for [`expected_pressure()`] and a reference pressure of 1013.25 hPa.
779    fn expected_altitude() -> Length {
780        Length::new::<meter>(248.78754)
781    }
782
783    impl Default for CalibrationCoefficients {
784        fn default() -> Self {
785            // NVM_PAR registers (0x31 .. 0x45) from a real BMP390, rev 0x01
786            Self::from_registers(&[
787                0x98, 0x6c, 0xa9, 0x4a, 0xf9, 0xe3, 0x1c, 0x61, 0x16, 0x06, 0x01, 0x51, 0x4a, 0xde,
788                0x5d, 0x03, 0xfa, 0xf9, 0x0e, 0x06, 0xf5,
789            ])
790        }
791    }
792
793    fn get_try_new_transactions(
794        addr: Address,
795        configuration: &Configuration,
796        err_reg: &ErrReg,
797        event: &Event,
798        int_status: &IntStatus,
799    ) -> [I2cTransaction; 5] {
800        [
801            // CHIP_ID is read in a 2-byte burst to also read REV_ID
802            I2cTransaction::write_read(
803                addr.into(),
804                vec![Register::CHIP_ID.into()],
805                vec![0x60, 0x01],
806            ),
807            // EVENT and INT_STATUS are read in a 2-byte burst
808            I2cTransaction::write_read(
809                addr.into(),
810                vec![Register::EVENT.into()],
811                vec![u8::from(*event), u8::from(*int_status)],
812            ),
813            I2cTransaction::write(addr.into(), configuration.to_write_bytes().to_vec()),
814            I2cTransaction::write_read(
815                addr.into(),
816                vec![Register::ERR_REG.into()],
817                vec![u8::from(*err_reg)],
818            ),
819            I2cTransaction::write_read(
820                addr.into(),
821                CalibrationCoefficients::write_read_write_transaction().to_vec(),
822                vec![0; 21],
823            ),
824        ]
825    }
826
827    #[tokio::test]
828    async fn test_try_new() {
829        // Several things are implicitly tested here:
830        // 1. The chip ID is read and checked => Ok
831        // 2. The rev ID is read in the same burst as chip ID
832        // 3. The event and int status registers are read in a burst to clear them
833        // 4. The configuration is written
834        // 5. The ERR_REG is read to check for errors
835        // 6. The calibration coefficients are read
836
837        let addr = Address::Up;
838        let config = Configuration::default();
839        let expectations = get_try_new_transactions(addr, &config, &0.into(), &0.into(), &0.into());
840        let mut i2c = Mock::new(&expectations);
841        let mut delay = CheckedDelay::new(&[
842            DelayTransaction::async_delay_ms(2), // time to first communication
843        ]);
844
845        let _bmp390 = Bmp390::try_new(i2c.clone(), addr, delay.clone(), &config)
846            .await
847            .unwrap();
848
849        delay.done();
850        i2c.done();
851    }
852
853    #[tokio::test]
854    async fn test_reads_temperature_and_compensates() {
855        let addr = Address::Up;
856        let expectations = [I2cTransaction::write_read(
857            addr.into(),
858            vec![Register::DATA_3.into()],
859            TEMPERATURE_BYTES.to_vec(),
860        )];
861
862        let mut i2c = Mock::new(&expectations);
863        let mut bmp390 =
864            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
865        let temperature = bmp390.temperature().await.unwrap();
866        assert_eq!(temperature, expected_temperature());
867        i2c.done();
868    }
869
870    #[tokio::test]
871    async fn test_reads_pressure() {
872        let addr = Address::Up;
873
874        // NOTE: a pressure read requires a temperature read, so response is 6 bytes
875        let expectations = [I2cTransaction::write_read(
876            addr.into(),
877            vec![Register::DATA_0.into()],
878            PRESSURE_TEMPERATURE_BYTES.to_vec(),
879        )];
880
881        let mut i2c = Mock::new(&expectations);
882        let mut bmp390 =
883            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
884        let pressure = bmp390.pressure().await.unwrap();
885        assert_eq!(pressure, expected_pressure());
886        i2c.done();
887    }
888
889    #[tokio::test]
890    async fn test_reads_temperature_pressure() {
891        let addr = Address::Up;
892        let expectations = [I2cTransaction::write_read(
893            addr.into(),
894            vec![Register::DATA_0.into()],
895            PRESSURE_TEMPERATURE_BYTES.to_vec(),
896        )];
897
898        let mut i2c = Mock::new(&expectations);
899        let mut bmp390 =
900            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
901        let measurement = bmp390.temperature_pressure().await.unwrap();
902        assert_eq!(measurement.0, expected_temperature());
903        assert_eq!(measurement.1, expected_pressure());
904        i2c.done();
905    }
906
907    #[tokio::test]
908    async fn test_altitude() {
909        let addr = Address::Up;
910
911        // NOTE: a pressure read requires a temperature read, so response is 6 bytes
912        let expectations = [I2cTransaction::write_read(
913            addr.into(),
914            vec![Register::DATA_0.into()],
915            PRESSURE_TEMPERATURE_BYTES.to_vec(),
916        )];
917
918        let mut i2c = Mock::new(&expectations);
919        let mut bmp390 =
920            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
921        let altitude = bmp390.altitude().await.unwrap();
922        assert_eq!(altitude, expected_altitude());
923        i2c.done();
924    }
925
926    #[tokio::test]
927    async fn test_measure_reads_temperature_pressure_altitude() {
928        let addr = Address::Up;
929        let expectations = [I2cTransaction::write_read(
930            addr.into(),
931            vec![Register::DATA_0.into()],
932            PRESSURE_TEMPERATURE_BYTES.to_vec(),
933        )];
934
935        let mut i2c = Mock::new(&expectations);
936        let mut bmp390 =
937            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
938        let measurement = bmp390.measure().await.unwrap();
939        assert_eq!(measurement.temperature, expected_temperature());
940        assert_eq!(measurement.pressure, expected_pressure());
941        assert_eq!(measurement.altitude, expected_altitude());
942        i2c.done();
943    }
944
945    #[tokio::test]
946    async fn test_altitude_custom_reference() {
947        let addr = Address::Up;
948
949        // NOTE: a pressure read requires a temperature read, so response is 6 bytes
950        let expectations = [I2cTransaction::write_read(
951            addr.into(),
952            vec![Register::DATA_0.into()],
953            PRESSURE_TEMPERATURE_BYTES.to_vec(),
954        )];
955
956        let mut i2c = Mock::new(&expectations);
957        let mut bmp390 =
958            Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
959
960        bmp390.set_reference_altitude(expected_altitude());
961        let altitude = bmp390.altitude().await.unwrap();
962        assert_eq!(altitude, Length::ZERO);
963        i2c.done();
964    }
965
966    #[tokio::test]
967    async fn test_chip_id_incorrect() {
968        let addr = Address::Up;
969
970        let mut expectations = get_try_new_transactions(
971            addr,
972            &Configuration::default(),
973            &0.into(),
974            &0.into(),
975            &0.into(),
976        )
977        .into_iter()
978        .take(1)
979        .collect::<Vec<_>>();
980
981        expectations[0] = I2cTransaction::write_read(
982            addr.into(),
983            vec![Register::CHIP_ID.into()],
984            vec![0x42, 0x01],
985        );
986
987        let mut i2c = Mock::new(&expectations);
988        let delay = NoopDelay::new();
989        let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
990        assert!(matches!(result, Err(Error::WrongChip(0x42))));
991        i2c.done();
992    }
993
994    #[tokio::test]
995    async fn test_fatal_error() {
996        let addr = Address::Up;
997
998        let fatal_err = ErrReg {
999            fatal_err: true,
1000            cmd_err: false,
1001            conf_err: false,
1002        };
1003
1004        let expectations = get_try_new_transactions(
1005            addr,
1006            &Configuration::default(),
1007            &fatal_err.into(),
1008            &0.into(),
1009            &0.into(),
1010        )
1011        .into_iter()
1012        .take(4)
1013        .collect::<Vec<_>>();
1014
1015        let mut i2c = Mock::new(&expectations);
1016        let delay = NoopDelay::new();
1017        let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1018        assert!(matches!(result, Err(Error::Fatal)));
1019        // assert_matches!(result, Err(Error::Fatal))); // TODO: use assert_matches once it's stable
1020        i2c.done();
1021    }
1022
1023    #[tokio::test]
1024    async fn test_command_error() {
1025        let addr = Address::Up;
1026
1027        let cmd_err = ErrReg {
1028            fatal_err: false,
1029            cmd_err: true,
1030            conf_err: false,
1031        };
1032
1033        let expectations = get_try_new_transactions(
1034            addr,
1035            &Configuration::default(),
1036            &cmd_err.into(),
1037            &0.into(),
1038            &0.into(),
1039        )
1040        .into_iter()
1041        .take(4)
1042        .collect::<Vec<_>>();
1043
1044        let mut i2c = Mock::new(&expectations);
1045        let delay = NoopDelay::new();
1046        let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1047        assert!(matches!(result, Err(Error::Command)));
1048        i2c.done();
1049    }
1050
1051    #[tokio::test]
1052    async fn test_configuration_error() {
1053        let addr = Address::Up;
1054
1055        let conf_err = ErrReg {
1056            fatal_err: false,
1057            cmd_err: false,
1058            conf_err: true,
1059        };
1060
1061        let expectations = get_try_new_transactions(
1062            addr,
1063            &Configuration::default(),
1064            &conf_err.into(),
1065            &0.into(),
1066            &0.into(),
1067        )
1068        .into_iter()
1069        .take(4)
1070        .collect::<Vec<_>>();
1071
1072        let mut i2c = Mock::new(&expectations);
1073        let delay = NoopDelay::new();
1074        let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1075        assert!(matches!(result, Err(Error::Configuration)));
1076        i2c.done();
1077    }
1078
1079    #[tokio::test]
1080    async fn test_any_other_error() {
1081        // Test that the driver handles unexpected bits in the ERR_REG register gracefully (i.e. doesn't panic or error)
1082        let addr = Address::Up;
1083
1084        for err_reg_bits in 0..=7 {
1085            let err_reg = ErrReg::from(err_reg_bits);
1086            if err_reg.fatal_err || err_reg.cmd_err || err_reg.conf_err {
1087                // skip the error flags we've already tested, we're looking for how the driver handles unexpected bits in this register
1088                continue;
1089            }
1090
1091            let mut expectations = get_try_new_transactions(
1092                addr,
1093                &Configuration::default(),
1094                &0.into(),
1095                &0.into(),
1096                &0.into(),
1097            );
1098
1099            expectations[3] = I2cTransaction::write_read(
1100                addr.into(),
1101                vec![Register::ERR_REG.into()],
1102                vec![err_reg_bits],
1103            );
1104
1105            let mut i2c = Mock::new(&expectations);
1106            let delay = NoopDelay::new();
1107            let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1108            assert!(
1109                result.is_ok(),
1110                "Unexpected error with ERR_REG = {:#010b}",
1111                err_reg_bits
1112            );
1113
1114            i2c.done();
1115        }
1116    }
1117}