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_auto_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
200/// The BMP390 barometer's I2C addresses, either `0x76` or `0x77`.
201///
202/// The BMP390 can be configured to use two different addresses by either pulling the `SDO` pin down to `GND`
203/// (`0x76` via [`Address::Down`]) or up to `V_DDIO` (`0x77` via [`Address::Up`]).
204#[derive(Debug, Clone, Copy, Format)]
205pub enum Address {
206 /// `0x76`: The BMP390's address when `SDO` is pulled up to `GND`.
207 Down = 0x76,
208
209 /// `0x77`: The BMP390's address when `SDO` is pulled down to `V_DDIO`
210 Up = 0x77,
211}
212
213impl From<Address> for u8 {
214 /// Convert the address to a [`u8`] for I2C communication.
215 fn from(address: Address) -> u8 {
216 address as u8
217 }
218}
219
220/// Output from the BMP390 consists of ADC outputs.
221///
222/// These must be compensated using formulas from the datasheet to obtain the actual temperature and pressure values,
223/// using coefficients stored in non-volatile memory (NVM).
224///
225/// # Datasheet
226/// - Section 3.11 Output compensation.
227/// - Appendix A: Computation formulae reference implementation.
228#[derive(Debug, Clone, Copy, Format)]
229struct CalibrationCoefficients {
230 par_t1: f32,
231 par_t2: f32,
232 par_t3: f32,
233 par_p1: f32,
234 par_p2: f32,
235 par_p3: f32,
236 par_p4: f32,
237 par_p5: f32,
238 par_p6: f32,
239 par_p7: f32,
240 par_p8: f32,
241 par_p9: f32,
242 par_p10: f32,
243 par_p11: f32,
244}
245
246impl CalibrationCoefficients {
247 /// Read the calibration coefficients from the BMP390's NVM registers and convert them to into a set of
248 /// floating-point calibration coefficients for the formulas implemented in the compensation functions.
249 async fn try_from_i2c<I: I2c>(address: Address, i2c: &mut I) -> Result<Self, Error<I::Error>> {
250 let mut calibration_coefficient_regs = [0; 21];
251 i2c.write_read(
252 address.into(),
253 &Self::write_read_write_transaction(),
254 &mut calibration_coefficient_regs,
255 )
256 .await
257 .map_err(Error::I2c)?;
258
259 Ok(Self::from_registers(&calibration_coefficient_regs))
260 }
261
262 /// Calculate the calibration coefficients from the raw register data in registers [`Register::NVM_PAR_T1_0`] to
263 /// [`Register::NVM_PAR_P11`].
264 ///
265 /// # Datasheet
266 /// Apendix A, Section 8.4
267 fn from_registers(data: &[u8; 21]) -> Self {
268 trace!("NVM_PAR: {=[u8]:#04x}", *data);
269 let nvm_par_t1: u16 = (data[1] as u16) << 8 | data[0] as u16;
270 let nvm_par_t2: u16 = (data[3] as u16) << 8 | data[2] as u16;
271 let nvm_par_t3: i8 = data[4] as i8;
272 let nvm_par_p1: i16 = (data[6] as i16) << 8 | data[5] as i16;
273 let nvm_par_p2: i16 = (data[8] as i16) << 8 | data[7] as i16;
274 let nvm_par_p3: i8 = data[9] as i8;
275 let nvm_par_p4: i8 = data[10] as i8;
276 let nvm_par_p5: u16 = (data[12] as u16) << 8 | data[11] as u16;
277 let nvm_par_p6: u16 = (data[14] as u16) << 8 | data[13] as u16;
278 let nvm_par_p7: i8 = data[15] as i8;
279 let nvm_par_p8: i8 = data[16] as i8;
280 let nvm_par_p9: i16 = (data[18] as i16) << 8 | data[17] as i16;
281 let nvm_par_p10: i8 = data[19] as i8;
282 let nvm_par_p11: i8 = data[20] as i8;
283
284 Self {
285 par_t1: (nvm_par_t1 as f32) / 0.003_906_25, // 2^-8
286 par_t2: (nvm_par_t2 as f32) / 1_073_741_824.0, // 2^30
287 par_t3: (nvm_par_t3 as f32) / 281_474_976_710_656.0, // 2^48
288 par_p1: ((nvm_par_p1 as f32) - 16_384.0) / 1_048_576.0, // 2^14 / 2^20
289 par_p2: ((nvm_par_p2 as f32) - 16_384.0) / 536_870_912.0, // 2^14 / 2^29
290 par_p3: (nvm_par_p3 as f32) / 4_294_967_296.0, // 2^32
291 par_p4: (nvm_par_p4 as f32) / 137_438_953_472.0, // 2^37
292 par_p5: (nvm_par_p5 as f32) / 0.125, // 2^-3
293 par_p6: (nvm_par_p6 as f32) / 64.0, // 2^6
294 par_p7: (nvm_par_p7 as f32) / 256.0, // 2^8
295 par_p8: (nvm_par_p8 as f32) / 32768.0, // 2^15
296 par_p9: (nvm_par_p9 as f32) / 281_474_976_710_656.0, //2^48
297 par_p10: (nvm_par_p10 as f32) / 281_474_976_710_656.0, // 2^48
298 par_p11: (nvm_par_p11 as f32) / 36_893_488_147_419_103_232.0, // 2^65
299 }
300 }
301
302 /// Compensate a temperature reading according to calibration coefficients.
303 ///
304 /// # Datasheet
305 /// Apendix A, Section 8.5
306 fn compensate_temperature(&self, temperature_uncompensated: u32) -> ThermodynamicTemperature {
307 // This could be done in fewer expressions, but it's broken down for clarity and to match the datasheet
308 let uncompensated = temperature_uncompensated as f32;
309 let partial_data1 = uncompensated - self.par_t1;
310 let partial_data2 = partial_data1 * self.par_t2;
311 let temperature = partial_data2 + (partial_data1 * partial_data1) * self.par_t3;
312 ThermodynamicTemperature::new::<degree_celsius>(temperature)
313 }
314
315 /// Compensate a pressure reading according to calibration coefficients.
316 ///
317 /// # Datasheet
318 /// Apendix A, Section 8.6
319 fn compensate_pressure(
320 &self,
321 temperature: ThermodynamicTemperature,
322 pressure_uncompensated: u32,
323 ) -> Pressure {
324 // This could be done in fewer expressions, but it's broken down for clarity and to match the datasheet
325 let uncompensated = pressure_uncompensated as f32;
326 let temperature = temperature.get::<degree_celsius>();
327 let partial_data1 = self.par_p6 * temperature;
328 let partial_data2 = self.par_p7 * temperature * temperature;
329 let partial_data3 = self.par_p8 * temperature * temperature * temperature;
330 let partial_out1 = self.par_p5 + partial_data1 + partial_data2 + partial_data3;
331
332 let partial_data1 = self.par_p2 * temperature;
333 let partial_data2 = self.par_p3 * temperature * temperature;
334 let partial_data3 = self.par_p4 * temperature * temperature * temperature;
335 let partial_out2 =
336 uncompensated * (self.par_p1 + partial_data1 + partial_data2 + partial_data3);
337
338 let partial_data1 = uncompensated * uncompensated;
339 let partial_data2 = self.par_p9 + self.par_p10 * temperature;
340 let partial_data3 = partial_data1 * partial_data2;
341 let partial_data4 =
342 partial_data3 + uncompensated * uncompensated * uncompensated * self.par_p11;
343
344 let pressure = partial_out1 + partial_out2 + partial_data4;
345 Pressure::new::<pascal>(pressure)
346 }
347
348 /// Gets the bytes to write in a write-read transaction to the BMP390 to read the calibration coefficients. This
349 /// must be combined with a 21-byte read in a combined write-read burst.
350 fn write_read_write_transaction() -> [u8; 1] {
351 [Register::NVM_PAR_T1_0.into()]
352 }
353}
354
355/// Configuration for the BMP390 barometer.
356#[derive(Debug, Clone, Copy, Format)]
357pub struct Configuration {
358 /// Enabling and disabling the pressure and temperature measurements and the power mode.
359 pub power_control: PowerControl,
360
361 /// The oversampling settings for pressure and temperature measurements.
362 pub oversampling: Osr,
363
364 /// The output data rate settings.
365 pub output_data_rate: Odr,
366
367 /// IIR filter coefficient settings.
368 pub iir_filter: Config,
369}
370
371impl Default for Configuration {
372 /// Default configuration for the BMP390 barometer. This configuration enables pressure and temperature measurement
373 /// with normal power mode, x8 oversampling for pressure and x1 oversampling for temperature, an output data rate of
374 /// 50 Hz, and a IIR filter coefficient of 4. This corresponds to a "standard resolution" configuration as
375 /// recommended by the datasheet Section 3.5. Filter selection.
376 fn default() -> Self {
377 Self {
378 power_control: PowerControl {
379 enable_pressure: true,
380 enable_temperature: true,
381 mode: PowerMode::Normal,
382 },
383 oversampling: Osr {
384 pressure: Oversampling::X8,
385 temperature: Oversampling::X1,
386 },
387 output_data_rate: Odr {
388 odr_sel: OdrSel::ODR_50,
389 },
390 iir_filter: Config {
391 iir_filter: IirFilter::coef_15,
392 },
393 }
394 }
395}
396
397impl Configuration {
398 /// Convert the configuration to a byte array that can be written to the BMP390's registers.
399 /// The byte array contains both the register address and the register value.
400 pub fn to_write_bytes(&self) -> [u8; 8] {
401 [
402 Register::PWR_CTRL.into(),
403 self.power_control.into(),
404 Register::OSR.into(),
405 self.oversampling.into(),
406 Register::ODR.into(),
407 self.output_data_rate.into(),
408 Register::CONFIG.into(),
409 self.iir_filter.into(),
410 ]
411 }
412}
413
414/// A driver for the BMP390 pressure sensor over any [`I2c`] implementation.
415///
416/// This driver utilizes [`uom`] to provide automatic, type-safe, and zero-cost units of measurement. Measurements can
417/// be retrieved with [`Bmp390::measure`], which returns a [`Measurement`] struct containing the pressure, temperature,
418/// and altitude. The altitude is calculated based on the current pressure, standard atmospheric pressure at sea level,
419/// and a reference altitude, which can be set with [`Bmp390::set_reference_altitude`]. The reference altitude defaults
420/// to zero, so the default altitude is measured from sea level.
421///
422/// # Example
423/// ```no_run
424/// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
425/// use bmp390::Bmp390;
426/// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
427/// let config = bmp390::Configuration::default();
428/// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
429/// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
430/// let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
431/// let measurement = sensor.measure().await?;
432/// defmt::info!("Measurement: {}", measurement);
433/// # Ok(())
434/// # }
435/// ```
436pub struct Bmp390<I> {
437 /// The I2C bus the barometer is connected to.
438 i2c: I,
439
440 /// The I2C address of the barometer.
441 address: Address,
442
443 /// The calibration coefficients for the barometer to compensate temperature and pressure measurements.
444 coefficients: CalibrationCoefficients,
445
446 /// The reference altitude for altitude calculations.
447 ///
448 /// By default, this is zero. set to the standard atmospheric pressure at sea level, 1013.25 hPa. It can be set to
449 /// a different value using [`Bmp390::set_reference_altitude`] to calculate the altitude relative to a different
450 /// reference point.
451 altitude_reference: Length,
452}
453
454impl<I, E> Bmp390<I>
455where
456 I: I2c<Error = E>,
457{
458 /// Creates a new BMP390 driver. This will initialize the barometer with the provided configuration.
459 /// It will additionally delay for 2 ms to allow the barometer to start up and read the calibration coefficients
460 /// for temperature and pressure measuring.
461 ///
462 /// # Example
463 /// ```no_run
464 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
465 /// use bmp390::Bmp390;
466 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
467 /// let config = bmp390::Configuration::default();
468 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
469 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
470 /// let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
471 /// let measurement = sensor.measure().await?;
472 /// defmt::info!("Measurement: {}", measurement);
473 /// # Ok(())
474 /// # }
475 /// ```
476 pub async fn try_new<D: DelayNs>(
477 mut i2c: I,
478 address: Address,
479 mut delay: D,
480 config: &Configuration,
481 ) -> Result<Self, Error<E>> {
482 // 2 ms time to first communication (Datsheet Section 1, Table 2)
483 delay.delay_ms(2).await;
484
485 let mut data = [0; 2];
486 i2c.write_read(address.into(), &[Register::CHIP_ID.into()], &mut data)
487 .await
488 .map_err(Error::I2c)?;
489
490 let chip_id = data[0];
491 let rev_id = data[1];
492
493 debug!("CHIP_ID = {=u8:#04x}; REV_ID = {=u8:#04x}", chip_id, rev_id);
494 if chip_id != 0x60 {
495 return Err(Error::WrongChip(chip_id));
496 }
497
498 // read Register::EVENT and INT_STATUS in a burst read to clear the event and interrupt status flags
499 let mut data = [0; 2];
500 i2c.write_read(address.into(), &[Register::EVENT.into()], &mut data)
501 .await
502 .map_err(Error::I2c)?;
503
504 // write configuration after clearing interrupt status flags so that they are accurate from here on
505 i2c.write(address.into(), &config.to_write_bytes())
506 .await
507 .map_err(Error::I2c)?;
508
509 // read Register::ERR_REG after writing config to determine if configuration was successful and to clear the error status flags
510 let mut err_reg = [0; 1];
511 i2c.write_read(address.into(), &[Register::ERR_REG.into()], &mut err_reg)
512 .await
513 .map_err(Error::I2c)
514 .and_then(move |_| {
515 let err_reg = ErrReg::from(err_reg[0]);
516 if err_reg.fatal_err {
517 Err(Error::<E>::Fatal)
518 } else if err_reg.cmd_err {
519 Err(Error::<E>::Command)
520 } else if err_reg.conf_err {
521 Err(Error::<E>::Configuration)
522 } else {
523 Ok(())
524 }
525 })?;
526
527 let coefficients = CalibrationCoefficients::try_from_i2c(address, &mut i2c).await?;
528
529 Ok(Self::new_with_coefficients(i2c, address, coefficients))
530 }
531
532 /// Creates a new BMP390 driver with known calibration coefficients.
533 fn new_with_coefficients(
534 i2c: I,
535 address: Address,
536 coefficients: CalibrationCoefficients,
537 ) -> Self {
538 Self {
539 i2c,
540 address,
541 coefficients,
542 altitude_reference: Length::new::<meter>(0.0),
543 }
544 }
545
546 /// Reads the temperature from the barometer as a [`ThermodynamicTemperature`].
547 ///
548 /// # Example
549 /// ```no_run
550 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
551 /// # use bmp390::Bmp390;
552 /// use uom::si::thermodynamic_temperature::degree_celsius;
553 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
554 /// # let config = bmp390::Configuration::default();
555 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
556 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
557 /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
558 /// let temperature = sensor.temperature().await?;
559 /// defmt::info!("Temperature: {} °C", temperature.get::<degree_celsius>());
560 /// # Ok(())
561 /// # }
562 /// ```
563 pub async fn temperature(&mut self) -> Result<ThermodynamicTemperature, Error<E>> {
564 // Burst read: only address DATA_3 (temperature XLSB) and BMP390 auto-increments through DATA_5 (temperature MSB)
565 let write = &[Register::DATA_3.into()];
566 let mut read = [0; 3];
567 self.i2c
568 .write_read(self.address.into(), write, &mut read)
569 .await
570 .map_err(Error::I2c)?;
571
572 // DATA_3 is the LSB, DATA_5 is the MSB
573 let temperature = u32::from(read[0]) | u32::from(read[1]) << 8 | u32::from(read[2]) << 16;
574 let temperature = self.coefficients.compensate_temperature(temperature);
575 Ok(temperature)
576 }
577
578 /// Reads the pressure from the barometer as a [`Pressure`].
579 ///
580 /// # Example
581 /// ```no_run
582 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
583 /// # use bmp390::Bmp390;
584 /// use uom::si::pressure::hectopascal;
585 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
586 /// # let config = bmp390::Configuration::default();
587 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
588 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
589 /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
590 /// let pressure = sensor.pressure().await?;
591 /// defmt::info!("Pressure: {} hPa", pressure.get::<hectopascal>());
592 /// # Ok(())
593 /// # }
594 /// ```
595 pub async fn pressure(&mut self) -> Result<Pressure, Error<E>> {
596 // pressure requires temperature to compensate, so just measure both
597 let measurement = self.measure().await?;
598 Ok(measurement.pressure)
599 }
600
601 /// Measures the pressure and temperature from the barometer.
602 ///
603 /// # Example
604 /// ```no_run
605 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
606 /// # use bmp390::Bmp390;
607 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
608 /// # let config = bmp390::Configuration::default();
609 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
610 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
611 /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
612 /// let measurement = sensor.measure().await?;
613 /// defmt::info!("Measurement: {}", measurement);
614 /// # Ok(())
615 /// # }
616 /// ```
617 pub async fn measure(&mut self) -> Result<Measurement, Error<E>> {
618 // Burst read: only address DATA_0 (pressure XLSB) and BMP390 auto-increments through DATA_5 (temperature MSB)
619 let write = &[Register::DATA_0.into()];
620 let mut read = [0; 6];
621 self.i2c
622 .write_read(self.address.into(), write, &mut read)
623 .await
624 .map_err(Error::I2c)?;
625
626 trace!("DATA = {=[u8]:#04x}", read);
627
628 // pressure is 0:2 (XLSB, LSB, MSB), temperature is 3:5 (XLSB, LSB, MSB)
629 let temperature = u32::from(read[3]) | u32::from(read[4]) << 8 | u32::from(read[5]) << 16;
630 let temperature = self.coefficients.compensate_temperature(temperature);
631
632 let pressure = u32::from(read[0]) | u32::from(read[1]) << 8 | u32::from(read[2]) << 16;
633 let pressure = self.coefficients.compensate_pressure(temperature, pressure);
634
635 Ok(Measurement {
636 temperature,
637 pressure,
638 altitude: calculate_altitude(pressure, self.altitude_reference),
639 })
640 }
641
642 /// Set the reference altitude for altitude calculations.
643 ///
644 /// Following this, the altitude can be calculated using [`Bmp390::altitude`]. If the current pressure matches
645 /// the pressure when the reference altitude is set, the altitude will be 0.
646 ///
647 /// # Example
648 /// ```no_run
649 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
650 /// # use bmp390::Bmp390;
651 /// # use uom::si::length::meter;
652 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
653 /// # let config = bmp390::Configuration::default();
654 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
655 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
656 /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
657 /// let initial_altitude = sensor.altitude().await?;
658 /// sensor.set_reference_altitude(initial_altitude);
659 ///
660 /// // Some time later...
661 /// let altitude = sensor.altitude().await?;
662 /// defmt::info!("Altitude: {} meters", altitude.get::<meter>());
663 /// # Ok(())
664 /// # }
665 /// ```
666 pub fn set_reference_altitude(&mut self, altitude: Length) {
667 self.altitude_reference = altitude;
668 }
669
670 /// Calculates the latest altitude measurement as a [`Length`] after retrieving the latest pressure measurement.
671 ///
672 /// The altitude is calculating following the [NOAA formula](https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf).
673 ///
674 /// # Example
675 /// ```no_run
676 /// # use embedded_hal_mock::eh1::{delay::NoopDelay, i2c::Mock};
677 /// # use bmp390::Bmp390;
678 /// use uom::si::length::foot;
679 /// # async fn run() -> Result<(), bmp390::Error<embedded_hal_async::i2c::ErrorKind>> {
680 /// # let config = bmp390::Configuration::default();
681 /// # let i2c = embedded_hal_mock::eh1::i2c::Mock::new(&[]);
682 /// # let delay = embedded_hal_mock::eh1::delay::NoopDelay::new();
683 /// # let mut sensor = Bmp390::try_new(i2c, bmp390::Address::Up, delay, &config).await?;
684 /// let altitude = sensor.altitude().await?;
685 /// defmt::info!("Length: {} feet", altitude.get::<foot>());
686 /// # Ok(())
687 /// # }
688 /// ```
689 pub async fn altitude(&mut self) -> Result<Length, Error<E>> {
690 let pressure = self.pressure().await?;
691 Ok(calculate_altitude(pressure, self.altitude_reference))
692 }
693}
694
695/// Calculate the altitude based on the pressure, sea level pressure, and the reference altitude.
696///
697/// The altitude is calculating following the [NOAA formula](https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf).
698fn calculate_altitude(pressure: Pressure, altitude_reference: Length) -> Length {
699 let sea_level = Pressure::new::<hectopascal>(1013.25);
700 let above_sea_level =
701 Length::new::<foot>(145366.45 * (1.0 - powf((pressure / sea_level).value, 0.190284)));
702
703 above_sea_level - altitude_reference
704}
705
706#[cfg(test)]
707mod tests {
708 extern crate std;
709 use embedded_hal_mock::eh1::delay::{CheckedDelay, NoopDelay, Transaction as DelayTransaction};
710 use embedded_hal_mock::eh1::i2c::{Mock, Transaction as I2cTransaction};
711 use std::prelude::rust_2021::*;
712 use std::vec;
713 use uom::ConstZero;
714
715 use super::*;
716
717 /// Bytes for the DATA registers (0x04 .. 0x09) for a pressure and temperature measurement.
718 const PRESSURE_TEMPERATURE_BYTES: [u8; 6] = [0xcb, 0xb3, 0x6b, 0xd1, 0xba, 0x82];
719
720 /// The [`Measurement::pressure`] value for [`PRESSURE_TEMPERATURE_BYTES`] when compensated by [`CalibrationCoefficients::default()`].
721 fn expected_pressure() -> Pressure {
722 Pressure::new::<pascal>(98370.55)
723 }
724
725 /// Bytes for the DATA registers (0x07 .. 0x09) for a temperature measurement.
726 const TEMPERATURE_BYTES: [u8; 3] = [0xd1, 0xba, 0x82];
727
728 /// The [`Measurement::temperature`] value for [`TEMPERATURE_BYTES`] when compensated by [`CalibrationCoefficients::default()`].
729 fn expected_temperature() -> ThermodynamicTemperature {
730 ThermodynamicTemperature::new::<degree_celsius>(25.770_746)
731 }
732
733 /// The [`Measurement::altitude`] value for [`expected_pressure()`] and a reference pressure of 1013.25 hPa.
734 fn expected_altitude() -> Length {
735 Length::new::<meter>(248.78754)
736 }
737
738 impl Default for CalibrationCoefficients {
739 fn default() -> Self {
740 // NVM_PAR registers (0x31 .. 0x45) from a real BMP390, rev 0x01
741 Self::from_registers(&[
742 0x98, 0x6c, 0xa9, 0x4a, 0xf9, 0xe3, 0x1c, 0x61, 0x16, 0x06, 0x01, 0x51, 0x4a, 0xde,
743 0x5d, 0x03, 0xfa, 0xf9, 0x0e, 0x06, 0xf5,
744 ])
745 }
746 }
747
748 fn get_try_new_transactions(
749 addr: Address,
750 configuration: &Configuration,
751 err_reg: &ErrReg,
752 event: &Event,
753 int_status: &IntStatus,
754 ) -> [I2cTransaction; 5] {
755 [
756 // CHIP_ID is read in a 2-byte burst to also read REV_ID
757 I2cTransaction::write_read(
758 addr.into(),
759 vec![Register::CHIP_ID.into()],
760 vec![0x60, 0x01],
761 ),
762 // EVENT and INT_STATUS are read in a 2-byte burst
763 I2cTransaction::write_read(
764 addr.into(),
765 vec![Register::EVENT.into()],
766 vec![u8::from(*event), u8::from(*int_status)],
767 ),
768 I2cTransaction::write(addr.into(), configuration.to_write_bytes().to_vec()),
769 I2cTransaction::write_read(
770 addr.into(),
771 vec![Register::ERR_REG.into()],
772 vec![u8::from(*err_reg)],
773 ),
774 I2cTransaction::write_read(
775 addr.into(),
776 CalibrationCoefficients::write_read_write_transaction().to_vec(),
777 vec![0; 21],
778 ),
779 ]
780 }
781
782 #[tokio::test]
783 async fn test_try_new() {
784 // Several things are implicitly tested here:
785 // 1. The chip ID is read and checked => Ok
786 // 2. The rev ID is read in the same burst as chip ID
787 // 3. The event and int status registers are read in a burst to clear them
788 // 4. The configuration is written
789 // 5. The ERR_REG is read to check for errors
790 // 6. The calibration coefficients are read
791
792 let addr = Address::Up;
793 let config = Configuration::default();
794 let expectations = get_try_new_transactions(addr, &config, &0.into(), &0.into(), &0.into());
795 let mut i2c = Mock::new(&expectations);
796 let mut delay = CheckedDelay::new(&[
797 DelayTransaction::async_delay_ms(2), // time to first communication
798 ]);
799
800 let _bmp390 = Bmp390::try_new(i2c.clone(), addr, delay.clone(), &config)
801 .await
802 .unwrap();
803
804 delay.done();
805 i2c.done();
806 }
807
808 #[tokio::test]
809 async fn test_reads_temperature_and_compensates() {
810 let addr = Address::Up;
811 let expectations = [I2cTransaction::write_read(
812 addr.into(),
813 vec![Register::DATA_3.into()],
814 TEMPERATURE_BYTES.to_vec(),
815 )];
816
817 let mut i2c = Mock::new(&expectations);
818 let mut bmp390 =
819 Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
820 let temperature = bmp390.temperature().await.unwrap();
821 assert_eq!(temperature, expected_temperature());
822 i2c.done();
823 }
824
825 #[tokio::test]
826 async fn test_reads_pressure() {
827 let addr = Address::Up;
828
829 // NOTE: a pressure read requires a temperature read, so response is 6 bytes
830 let expectations = [I2cTransaction::write_read(
831 addr.into(),
832 vec![Register::DATA_0.into()],
833 PRESSURE_TEMPERATURE_BYTES.to_vec(),
834 )];
835
836 let mut i2c = Mock::new(&expectations);
837 let mut bmp390 =
838 Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
839 let pressure = bmp390.pressure().await.unwrap();
840 assert_eq!(pressure, expected_pressure());
841 i2c.done();
842 }
843
844 #[tokio::test]
845 async fn test_measure_reads_temperature_and_pressure() {
846 let addr = Address::Up;
847 let expectations = [I2cTransaction::write_read(
848 addr.into(),
849 vec![Register::DATA_0.into()],
850 PRESSURE_TEMPERATURE_BYTES.to_vec(),
851 )];
852
853 let mut i2c = Mock::new(&expectations);
854 let mut bmp390 =
855 Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
856 let measurement = bmp390.measure().await.unwrap();
857 assert_eq!(measurement.temperature, expected_temperature());
858 assert_eq!(measurement.pressure, expected_pressure());
859 i2c.done();
860 }
861
862 #[tokio::test]
863 async fn test_altitude() {
864 let addr = Address::Up;
865
866 // NOTE: a pressure read requires a temperature read, so response is 6 bytes
867 let expectations = [I2cTransaction::write_read(
868 addr.into(),
869 vec![Register::DATA_0.into()],
870 PRESSURE_TEMPERATURE_BYTES.to_vec(),
871 )];
872
873 let mut i2c = Mock::new(&expectations);
874 let mut bmp390 =
875 Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
876 let altitude = bmp390.altitude().await.unwrap();
877 assert_eq!(altitude, expected_altitude());
878 i2c.done();
879 }
880
881 #[tokio::test]
882 async fn test_altitude_custom_reference() {
883 let addr = Address::Up;
884
885 // NOTE: a pressure read requires a temperature read, so response is 6 bytes
886 let expectations = [I2cTransaction::write_read(
887 addr.into(),
888 vec![Register::DATA_0.into()],
889 PRESSURE_TEMPERATURE_BYTES.to_vec(),
890 )];
891
892 let mut i2c = Mock::new(&expectations);
893 let mut bmp390 =
894 Bmp390::new_with_coefficients(i2c.clone(), addr, CalibrationCoefficients::default());
895
896 bmp390.set_reference_altitude(expected_altitude());
897 let altitude = bmp390.altitude().await.unwrap();
898 assert_eq!(altitude, Length::ZERO);
899 i2c.done();
900 }
901
902 #[tokio::test]
903 async fn test_chip_id_incorrect() {
904 let addr = Address::Up;
905
906 let mut expectations = get_try_new_transactions(
907 addr,
908 &Configuration::default(),
909 &0.into(),
910 &0.into(),
911 &0.into(),
912 )
913 .into_iter()
914 .take(1)
915 .collect::<Vec<_>>();
916
917 expectations[0] = I2cTransaction::write_read(
918 addr.into(),
919 vec![Register::CHIP_ID.into()],
920 vec![0x42, 0x01],
921 );
922
923 let mut i2c = Mock::new(&expectations);
924 let delay = NoopDelay::new();
925 let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
926 assert!(matches!(result, Err(Error::WrongChip(0x42))));
927 i2c.done();
928 }
929
930 #[tokio::test]
931 async fn test_fatal_error() {
932 let addr = Address::Up;
933
934 let fatal_err = ErrReg {
935 fatal_err: true,
936 cmd_err: false,
937 conf_err: false,
938 };
939
940 let expectations = get_try_new_transactions(
941 addr,
942 &Configuration::default(),
943 &fatal_err.into(),
944 &0.into(),
945 &0.into(),
946 )
947 .into_iter()
948 .take(4)
949 .collect::<Vec<_>>();
950
951 let mut i2c = Mock::new(&expectations);
952 let delay = NoopDelay::new();
953 let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
954 assert!(matches!(result, Err(Error::Fatal)));
955 // assert_matches!(result, Err(Error::Fatal))); // TODO: use assert_matches once it's stable
956 i2c.done();
957 }
958
959 #[tokio::test]
960 async fn test_command_error() {
961 let addr = Address::Up;
962
963 let cmd_err = ErrReg {
964 fatal_err: false,
965 cmd_err: true,
966 conf_err: false,
967 };
968
969 let expectations = get_try_new_transactions(
970 addr,
971 &Configuration::default(),
972 &cmd_err.into(),
973 &0.into(),
974 &0.into(),
975 )
976 .into_iter()
977 .take(4)
978 .collect::<Vec<_>>();
979
980 let mut i2c = Mock::new(&expectations);
981 let delay = NoopDelay::new();
982 let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
983 assert!(matches!(result, Err(Error::Command)));
984 i2c.done();
985 }
986
987 #[tokio::test]
988 async fn test_configuration_error() {
989 let addr = Address::Up;
990
991 let conf_err = ErrReg {
992 fatal_err: false,
993 cmd_err: false,
994 conf_err: true,
995 };
996
997 let expectations = get_try_new_transactions(
998 addr,
999 &Configuration::default(),
1000 &conf_err.into(),
1001 &0.into(),
1002 &0.into(),
1003 )
1004 .into_iter()
1005 .take(4)
1006 .collect::<Vec<_>>();
1007
1008 let mut i2c = Mock::new(&expectations);
1009 let delay = NoopDelay::new();
1010 let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1011 assert!(matches!(result, Err(Error::Configuration)));
1012 i2c.done();
1013 }
1014
1015 #[tokio::test]
1016 async fn test_any_other_error() {
1017 // Test that the driver handles unexpected bits in the ERR_REG register gracefully (i.e. doesn't panic or error)
1018 let addr = Address::Up;
1019
1020 for err_reg_bits in 0..=7 {
1021 let err_reg = ErrReg::from(err_reg_bits);
1022 if err_reg.fatal_err || err_reg.cmd_err || err_reg.conf_err {
1023 // skip the error flags we've already tested, we're looking for how the driver handles unexpected bits in this register
1024 continue;
1025 }
1026
1027 let mut expectations = get_try_new_transactions(
1028 addr,
1029 &Configuration::default(),
1030 &0.into(),
1031 &0.into(),
1032 &0.into(),
1033 );
1034
1035 expectations[3] = I2cTransaction::write_read(
1036 addr.into(),
1037 vec![Register::ERR_REG.into()],
1038 vec![err_reg_bits],
1039 );
1040
1041 let mut i2c = Mock::new(&expectations);
1042 let delay = NoopDelay::new();
1043 let result = Bmp390::try_new(i2c.clone(), addr, delay, &Configuration::default()).await;
1044 assert!(
1045 result.is_ok(),
1046 "Unexpected error with ERR_REG = {:#010b}",
1047 err_reg_bits
1048 );
1049
1050 i2c.done();
1051 }
1052 }
1053}