From c1a86de6fe1b5089f34f5271012309132044f364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Sun, 8 Dec 2024 06:10:50 -0300 Subject: [PATCH] bcm390: Add first version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- src/bmp390.rs | 327 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/peripherals.rs | 1 + 3 files changed, 329 insertions(+) create mode 100644 src/bmp390.rs diff --git a/src/bmp390.rs b/src/bmp390.rs new file mode 100644 index 000000000..55a4ebb4b --- /dev/null +++ b/src/bmp390.rs @@ -0,0 +1,327 @@ +use std::error::Error; +use std::path::PathBuf; + +use linux_embedded_hal::I2cdev; +use embedded_hal::blocking::i2c::{Write, WriteRead}; + +use crate::peripherals::{ + AnyHardware, BarometerSensor, PeripheralClass, PeripheralInfo, Peripherals, TemperatureSensor, +}; + +const DEFAULT_I2C_ADDRESS: u8 = 0x76; +const DEFAULT_I2C_PATH: &str = "/dev/i2c-1"; + +/// Wrapper type for results +pub type Result = std::result::Result>; + +/// BMP390 Registers +enum Register { + ChipId = 0x00, + ErrReg = 0x02, + Event = 0x10, + /// Calibration data start + NvmParT1_0 = 0x31, + /// data registers + Data0 = 0x04, // pressure XLSB + Data3 = 0x07, // temperature XLSB + + PwrCtrl = 0x1B, +} + +/// Output from the BMP390 consists of ADC outputs. +/// +/// These must be compensated using formulas from the datasheet to obtain the actual temperature and pressure values, +/// using coefficients stored in non-volatile memory (NVM). +/// +/// # Datasheet +/// - Section 3.11 Output compensation. +/// - Appendix A: Computation formulae reference implementation. +#[derive(Debug, Default, Clone, Copy)] +struct CalibrationCoefficients { + par_t1: f32, + par_t2: f32, + par_t3: f32, + par_p1: f32, + par_p2: f32, + par_p3: f32, + par_p4: f32, + par_p5: f32, + par_p6: f32, + par_p7: f32, + par_p8: f32, + par_p9: f32, + par_p10: f32, + par_p11: f32, +} + +pub struct Bmp390 { + i2c_device: I2cdev, + address: u8, + calibration: CalibrationCoefficients, + ground_pressure: f32, +} + +pub struct Bmp390Device { + bmp390: Bmp390, + info: PeripheralInfo, +} + +impl Bmp390Device { + pub fn builder() -> Bmp390Builder { + Bmp390Builder::new() + } + + pub fn get_peripheral_info(&self) -> &PeripheralInfo { + &self.info + } +} + +impl AnyHardware for Bmp390Device { + fn as_temperature_sensor(&mut self) -> Option<&mut dyn TemperatureSensor> { + Some(self) + } + + fn as_barometer_sensor(&mut self) -> Option<&mut dyn BarometerSensor> { + Some(self) + } +} + +pub struct Bmp390Builder { + i2c_address: u8, + i2c_path: PathBuf, + ground_pressure: f32, +} + +impl Bmp390Builder { + pub fn new() -> Self { + Bmp390Builder { + i2c_address: DEFAULT_I2C_ADDRESS, + i2c_path: PathBuf::from(DEFAULT_I2C_PATH), + ground_pressure: 0., + } + } + + pub fn address(&mut self, address: u16) -> &mut Self { + self.i2c_address = address as u8; + self + } + + pub fn path(&mut self, path: impl Into) -> &mut Self { + self.i2c_path = path.into(); + self + } + + pub fn ground_pressure(&mut self, pressure: f32) -> &mut Self { + self.ground_pressure = pressure; + self + } + + pub fn build(&self) -> Result { + let dev = I2cdev::new(&self.i2c_path)?; + let mut sensor = Bmp390 { + i2c_device: dev, + address: self.i2c_address, + calibration: CalibrationCoefficients::default(), + ground_pressure: self.ground_pressure, + }; + + sensor.begin()?; + + if self.ground_pressure != 0. { + sensor.zero()?; + } + + Ok(Bmp390Device { + bmp390: sensor, + info: PeripheralInfo { + peripheral: Peripherals::Bmp390, + class: vec![PeripheralClass::Pressure, PeripheralClass::Temperature], + }, + }) + } +} + +impl Default for Bmp390Builder { + fn default() -> Self { + Self::new() + } +} + +impl Bmp390 { + fn write_reg(&mut self, reg: Register, value: u8) -> Result<()> { + self.i2c_device.write(self.address, &[reg as u8, value])?; + Ok(()) + } + + fn read_reg(&mut self, reg: Register) -> Result { + let mut buf = [0u8; 1]; + self.i2c_device.write_read(self.address, &[reg as u8], &mut buf)?; + Ok(buf[0]) + } + + fn read_burst(&mut self, start: Register, buf: &mut [u8]) -> Result<()> { + self.i2c_device.write_read(self.address, &[start as u8], buf)?; + Ok(()) + } + + fn begin(&mut self) -> Result<()> { + let chip_id = self.read_reg(Register::ChipId)?; + if chip_id != 0x60 { + return Err(format!("BMP390: Invalid chip ID: 0x{:02X}", chip_id).into()); + } + + // reset device not strictly necessary here + // Configure sensor (similar to BMP390 defaults) + // Enable temperature and pressure, normal mode + // PwrCtrl: enable temp + pressure + normal mode: 0x33 (temp + press + normal) + self.write_reg(Register::PwrCtrl, 0x33)?; + + // wait a bit for device to load NVM + std::thread::sleep(std::time::Duration::from_millis(2)); + + self.read_calibration()?; + + Ok(()) + } + + pub fn zero(&mut self) -> Result { + self.ground_pressure = self.pressure_pa()?; + Ok(self.ground_pressure) + } + + fn read_calibration(&mut self) -> Result<()> { + // Reading calibration coefficients from BMP390: + let mut data = [0u8; 21]; + self.read_burst(Register::NvmParT1_0, &mut data)?; + + // Parsing according to BMP390 datasheet (from earlier code): + let nvm_par_t1 = ((data[1] as u16) << 8) | data[0] as u16; + let nvm_par_t2 = ((data[3] as u16) << 8) | data[2] as u16; + let nvm_par_t3 = data[4] as i8; + let nvm_par_p1 = ((data[6] as i16) << 8) | data[5] as i16; + let nvm_par_p2 = ((data[8] as i16) << 8) | data[7] as i16; + let nvm_par_p3 = data[9] as i8; + let nvm_par_p4 = data[10] as i8; + let nvm_par_p5 = ((data[12] as u16) << 8) | data[11] as u16; + let nvm_par_p6 = ((data[14] as u16) << 8) | data[13] as u16; + let nvm_par_p7 = data[15] as i8; + let nvm_par_p8 = data[16] as i8; + let nvm_par_p9 = ((data[18] as i16) << 8) | data[17] as i16; + let nvm_par_p10 = data[19] as i8; + let nvm_par_p11 = data[20] as i8; + + self.calibration = CalibrationCoefficients { + par_t1: (nvm_par_t1 as f32) / 0.00390625, + par_t2: (nvm_par_t2 as f32) / 1073741824.0, + par_t3: (nvm_par_t3 as f32) / 281474976710656.0, + par_p1: ((nvm_par_p1 as f32) - 16384.0) / 1048576.0, + par_p2: ((nvm_par_p2 as f32) - 16384.0) / 536870912.0, + par_p3: (nvm_par_p3 as f32) / 4294967296.0, + par_p4: (nvm_par_p4 as f32) / 137438953472.0, + par_p5: (nvm_par_p5 as f32) / 0.125, + par_p6: (nvm_par_p6 as f32) / 64.0, + par_p7: (nvm_par_p7 as f32) / 256.0, + par_p8: (nvm_par_p8 as f32) / 32768.0, + par_p9: (nvm_par_p9 as f32) / 281474976710656.0, + par_p10: (nvm_par_p10 as f32) / 281474976710656.0, + par_p11: (nvm_par_p11 as f32) / 36893488147419103232.0, + }; + + Ok(()) + } + + fn read_raw_measurements(&mut self) -> Result<(u32, u32)> { + // BMP390 data: pressure in DATA_0..2, temp in DATA_3..5 + let mut buf = [0u8; 6]; + self.read_burst(Register::Data0, &mut buf)?; + + let raw_pressure = (buf[0] as u32) | ((buf[1] as u32) << 8) | ((buf[2] as u32) << 16); + let raw_temperature = (buf[3] as u32) | ((buf[4] as u32) << 8) | ((buf[5] as u32) << 16); + + Ok((raw_temperature, raw_pressure)) + } + + fn temperature_celsius(&mut self) -> Result { + let (raw_temp, _) = self.read_raw_measurements()?; + let t_lin = self.compensate_temperature(raw_temp); + Ok(t_lin) + } + + fn pressure_pa(&mut self) -> Result { + let (raw_temp, raw_press) = self.read_raw_measurements()?; + + let t_lin = self.compensate_temperature(raw_temp); + let pressure = self.compensate_pressure(t_lin, raw_press); + Ok(pressure) + } + + /// Compensate a temperature reading according to calibration coefficients. + /// + /// # Datasheet + /// Apendix A, Section 8.5 + fn compensate_temperature(&self, raw_temp: u32) -> f32 { + let uncomp = raw_temp as f32; + let partial_data1 = uncomp - self.calibration.par_t1; + let partial_data2 = partial_data1 * self.calibration.par_t2; + partial_data2 + (partial_data1 * partial_data1) * self.calibration.par_t3 + } + + /// Compensate a pressure reading according to calibration coefficients. + /// + /// # Datasheet + /// Apendix A, Section 8.6 + fn compensate_pressure(&self, temperature: f32, raw_press: u32) -> f32 { + let uncomp = raw_press as f32; + + let partial_data1 = self.calibration.par_p6 * temperature; + let partial_data2 = self.calibration.par_p7 * temperature * temperature; + let partial_data3 = self.calibration.par_p8 * temperature * temperature * temperature; + let partial_out1 = self.calibration.par_p5 + partial_data1 + partial_data2 + partial_data3; + + let partial_data1 = self.calibration.par_p2 * temperature; + let partial_data2 = self.calibration.par_p3 * temperature * temperature; + let partial_data3 = self.calibration.par_p4 * temperature * temperature * temperature; + let partial_out2 = + uncomp * (self.calibration.par_p1 + partial_data1 + partial_data2 + partial_data3); + + let partial_data1 = uncomp * uncomp; + let partial_data2 = self.calibration.par_p9 + self.calibration.par_p10 * temperature; + let partial_data3 = partial_data1 * partial_data2; + let partial_data4 = partial_data3 + uncomp * uncomp * uncomp * self.calibration.par_p11; + + partial_out1 + partial_out2 + partial_data4 + } +} + +impl TemperatureSensor for Bmp390Device { + fn read_temperature(&mut self) -> Result { + let temperature = self.bmp390.temperature_celsius()?; + Ok(temperature) + } +} + +impl BarometerSensor for Bmp390Device { + fn read_pressure(&mut self) -> Result { + let pressure = self.bmp390.pressure_pa()? / 1000.0; + Ok(pressure) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{thread::sleep, time::Duration}; + + #[ignore] + #[test] + fn test_bmp390_pi_5() { + let mut baro = Bmp390Device::builder() + .build() + .expect("Failed to build BMP390"); + for _ in 0..10 { + println!("BMP390 temperature: {:?}", baro.read_temperature()); + println!("BMP390 pressure: {:?}", baro.read_pressure()); + sleep(Duration::from_millis(100)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a27966a54..a8c56a039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use std::fmt; mod ads1115; mod ak09915; mod bmp280; +mod bmp390; mod icm20689; mod leak; mod led; diff --git a/src/peripherals.rs b/src/peripherals.rs index 856dc4c61..f39d8fbc8 100644 --- a/src/peripherals.rs +++ b/src/peripherals.rs @@ -4,6 +4,7 @@ pub enum Peripherals { Ads1115, // ADC Ak09915, // 3-axis magnetometer Bmp280, // Pressure and Temperature + Bmp390, // Pressure and Temperature Gpio, Icm20689, // Accelerometer and Gyroscope Leak,