From a8fa4b56f93f8da897948f2832793edc3b1adf3a Mon Sep 17 00:00:00 2001 From: kahrendt Date: Tue, 8 Aug 2023 01:05:08 -0400 Subject: [PATCH] New component: Add support for bmp581 pressure and temperature sensors (#4657) --- CODEOWNERS | 1 + esphome/components/bmp581/__init__.py | 0 esphome/components/bmp581/bmp581.cpp | 596 ++++++++++++++++++++++++++ esphome/components/bmp581/bmp581.h | 222 ++++++++++ esphome/components/bmp581/sensor.py | 163 +++++++ tests/test1.yaml | 8 + 6 files changed, 990 insertions(+) create mode 100644 esphome/components/bmp581/__init__.py create mode 100644 esphome/components/bmp581/bmp581.cpp create mode 100644 esphome/components/bmp581/bmp581.h create mode 100644 esphome/components/bmp581/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 17d1462405..cda8a2f0f5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ esphome/components/ble_client/* @buxtronix esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmp3xx/* @martgras +esphome/components/bmp581/* @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core diff --git a/esphome/components/bmp581/__init__.py b/esphome/components/bmp581/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp581/bmp581.cpp b/esphome/components/bmp581/bmp581.cpp new file mode 100644 index 0000000000..0308da0bcb --- /dev/null +++ b/esphome/components/bmp581/bmp581.cpp @@ -0,0 +1,596 @@ +/* + * Adds support for Bosch's BMP581 high accuracy pressure and temperature sensor + * - Component structure based on ESPHome's BMP3XX component (as of March, 2023) + * - Implementation is easier as the sensor itself automatically compensates pressure for the temperature + * - Temperature and pressure data is converted via simple divison operations in this component + * - IIR filter level can independently be applied to temperature and pressure measurements + * - Bosch's BMP5-Sensor-API was consulted to verify that sensor configuration is done correctly + * - Copyright (c) 2022 Bosch Sensortec Gmbh, SPDX-License-Identifier: BSD-3-Clause + * - This component uses forced power mode only so measurements are synchronized by the host + * - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) + */ + +#include "bmp581.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace bmp581 { + +static const char *const TAG = "bmp581"; + +static const LogString *oversampling_to_str(Oversampling oversampling) { + switch (oversampling) { + case Oversampling::OVERSAMPLING_NONE: + return LOG_STR("None"); + case Oversampling::OVERSAMPLING_X2: + return LOG_STR("2x"); + case Oversampling::OVERSAMPLING_X4: + return LOG_STR("4x"); + case Oversampling::OVERSAMPLING_X8: + return LOG_STR("8x"); + case Oversampling::OVERSAMPLING_X16: + return LOG_STR("16x"); + case Oversampling::OVERSAMPLING_X32: + return LOG_STR("32x"); + case Oversampling::OVERSAMPLING_X64: + return LOG_STR("64x"); + case Oversampling::OVERSAMPLING_X128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +static const LogString *iir_filter_to_str(IIRFilter filter) { + switch (filter) { + case IIRFilter::IIR_FILTER_OFF: + return LOG_STR("OFF"); + case IIRFilter::IIR_FILTER_2: + return LOG_STR("2x"); + case IIRFilter::IIR_FILTER_4: + return LOG_STR("4x"); + case IIRFilter::IIR_FILTER_8: + return LOG_STR("8x"); + case IIRFilter::IIR_FILTER_16: + return LOG_STR("16x"); + case IIRFilter::IIR_FILTER_32: + return LOG_STR("32x"); + case IIRFilter::IIR_FILTER_64: + return LOG_STR("64x"); + case IIRFilter::IIR_FILTER_128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +void BMP581Component::dump_config() { + ESP_LOGCONFIG(TAG, "BMP581:"); + + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, " Communication with BMP581 failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE(TAG, " BMP581 has wrong chip ID - please verify you are using a BMP 581"); + break; + case ERROR_SENSOR_RESET: + ESP_LOGE(TAG, " BMP581 failed to reset"); + break; + case ERROR_SENSOR_STATUS: + ESP_LOGE(TAG, " BMP581 sensor status failed, there were NVM problems"); + break; + case ERROR_PRIME_IIR_FAILED: + ESP_LOGE(TAG, " BMP581's IIR Filter failed to prime with an initial measurement"); + break; + default: + ESP_LOGE(TAG, " BMP581 error code %d", (int) this->error_code_); + break; + } + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_temperature_level_))); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); + } + + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_pressure_level_))); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); + } +} + +void BMP581Component::setup() { + /* + * Setup goes through several stages, which follows the post-power-up procedure (page 18 of datasheet) and then sets + * configured options + * 1) Soft reboot + * 2) Verify ASIC chip ID matches BMP581 + * 3) Verify sensor status (check if NVM is okay) + * 4) Enable data ready interrupt + * 5) Write oversampling settings and set internal configuration values + * 6) Configure and prime IIR Filter(s), if enabled + */ + + this->error_code_ = NONE; + ESP_LOGCONFIG(TAG, "Setting up BMP581..."); + + //////////////////// + // 1) Soft reboot // + //////////////////// + + // Power-On-Reboot bit is asserted if sensor successfully reset + if (!this->reset_()) { + ESP_LOGE(TAG, "BMP581 failed to reset"); + + this->error_code_ = ERROR_SENSOR_RESET; + this->mark_failed(); + + return; + } + + /////////////////////////////////////////// + // 2) Verify ASIC chip ID matches BMP581 // + /////////////////////////////////////////// + + uint8_t chip_id; + + // read chip id from sensor + if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) { + ESP_LOGE(TAG, "Failed to read chip id"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // verify id + if (chip_id != BMP581_ASIC_ID) { + ESP_LOGE(TAG, "Unknown chip ID, is this a BMP581?"); + + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + + return; + } + + //////////////////////////////////////////////////// + // 3) Verify sensor status (check if NVM is okay) // + //////////////////////////////////////////////////// + + if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) { + ESP_LOGE(TAG, "Failed to read status register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // verify status_nvm_rdy bit (it is asserted if boot was successful) + if (!(this->status_.bit.status_nvm_rdy)) { + ESP_LOGE(TAG, "NVM not ready after boot"); + + this->error_code_ = ERROR_SENSOR_STATUS; + this->mark_failed(); + + return; + } + + // verify status_nvm_err bit (it is asserted if an error is detected) + if (this->status_.bit.status_nvm_err) { + ESP_LOGE(TAG, "NVM error detected on boot"); + + this->error_code_ = ERROR_SENSOR_STATUS; + this->mark_failed(); + + return; + } + + //////////////////////////////////// + // 4) Enable data ready interrupt // + //////////////////////////////////// + + // enable the data ready interrupt source + if (!this->write_interrupt_source_settings_(true)) { + ESP_LOGE(TAG, "Failed to write interrupt source register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + ////////////////////////////////////////////////////////////////////////// + // 5) Write oversampling settings and set internal configuration values // + ////////////////////////////////////////////////////////////////////////// + + // configure pressure readings, if sensor is defined + // otherwise, disable pressure oversampling + if (this->pressure_sensor_) { + this->osr_config_.bit.press_en = true; + } else { + this->pressure_oversampling_ = OVERSAMPLING_NONE; + } + + // write oversampling settings + if (!this->write_oversampling_settings_(this->temperature_oversampling_, this->pressure_oversampling_)) { + ESP_LOGE(TAG, "Failed to write oversampling register"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + // set output data rate to 4 Hz=0x19 (page 65 of datasheet) + // - ?shouldn't? matter as this component only uses FORCED_MODE - datasheet is ambiguous + // - If in NORMAL_MODE or NONSTOP_MODE, then this would still allow deep standby to save power + // - will be written to BMP581 at next requested measurement + this->odr_config_.bit.odr = 0x19; + + /////////////////////////////////////////////////////// + /// 6) Configure and prime IIR Filter(s), if enabled // + /////////////////////////////////////////////////////// + + if ((this->iir_temperature_level_ != IIR_FILTER_OFF) || (this->iir_pressure_level_ != IIR_FILTER_OFF)) { + if (!this->write_iir_settings_(this->iir_temperature_level_, this->iir_pressure_level_)) { + ESP_LOGE(TAG, "Failed to write IIR configuration registers"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (!this->prime_iir_filter_()) { + ESP_LOGE(TAG, "Failed to prime the IIR filter with an intiial measurement"); + + this->error_code_ = ERROR_PRIME_IIR_FAILED; + this->mark_failed(); + + return; + } + } +} + +void BMP581Component::update() { + /* + * Each update goes through several stages + * 0) Verify either a temperature or pressure sensor is defined before proceeding + * 1) Request a measurement + * 2) Wait for measurement to finish (based on oversampling rates) + * 3) Read data registers for temperature and pressure, if applicable + * 4) Publish measurements to sensor(s), if applicable + */ + + //////////////////////////////////////////////////////////////////////////////////// + // 0) Verify either a temperature or pressure sensor is defined before proceeding // + //////////////////////////////////////////////////////////////////////////////////// + + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + ////////////////////////////// + // 1) Request a measurement // + ////////////////////////////// + + ESP_LOGVV(TAG, "Requesting a measurement from sensor"); + + if (!this->start_measurement_()) { + ESP_LOGW(TAG, "Failed to request forced measurement of sensor"); + this->status_set_warning(); + + return; + } + + ////////////////////////////////////////////////////////////////////// + // 2) Wait for measurement to finish (based on oversampling rates) // + ////////////////////////////////////////////////////////////////////// + + ESP_LOGVV(TAG, "Measurement is expected to take %d ms to complete", this->conversion_time_); + + this->set_timeout("measurement", this->conversion_time_, [this]() { + float temperature = 0.0; + float pressure = 0.0; + + //////////////////////////////////////////////////////////////////////// + // 3) Read data registers for temperature and pressure, if applicable // + //////////////////////////////////////////////////////////////////////// + + if (this->pressure_sensor_) { + if (!this->read_temperature_and_pressure_(temperature, pressure)) { + ESP_LOGW(TAG, "Failed to read temperature and pressure measurements, skipping update"); + this->status_set_warning(); + + return; + } + } else { + if (!this->read_temperature_(temperature)) { + ESP_LOGW(TAG, "Failed to read temperature measurement, skipping update"); + this->status_set_warning(); + + return; + } + } + + ///////////////////////////////////////////////////////// + // 4) Publish measurements to sensor(s), if applicable // + ///////////////////////////////////////////////////////// + + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } + + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } + + this->status_clear_warning(); + }); +} + +bool BMP581Component::check_data_readiness_() { + // - verifies component is not internally in standby mode + // - reads interrupt status register + // - checks if data ready bit is asserted + // - If true, then internally sets component to standby mode if in forced mode + // - returns data readiness state + + if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) { + ESP_LOGD(TAG, "Data is not ready, sensor is in standby mode"); + return false; + } + + uint8_t status; + + if (!this->read_byte(BMP581_INT_STATUS, &status)) { + ESP_LOGE(TAG, "Failed to read interrupt status register"); + return false; + } + + this->int_status_.reg = status; + + if (this->int_status_.bit.drdy_data_reg) { + // If in forced mode, then set internal record of the power mode to STANDBY_MODE + // - sensor automatically returns to standby mode after completing a forced measurement + if (this->odr_config_.bit.pwr_mode == FORCED_MODE) { + this->odr_config_.bit.pwr_mode = STANDBY_MODE; + } + + return true; + } + + return false; +} + +bool BMP581Component::prime_iir_filter_() { + // - temporarily disables oversampling for a fast initial measurement; avoids slowing down ESPHome's startup process + // - enables IIR filter flushing with forced measurements + // - forces a measurement; flushing the IIR filter and priming it with a current value + // - disables IIR filter flushing with forced measurements + // - reverts to internally configured oversampling rates + // - returns success of all register writes/priming + + // store current internal oversampling settings to revert to after priming + Oversampling current_temperature_oversampling = (Oversampling) this->osr_config_.bit.osr_t; + Oversampling current_pressure_oversampling = (Oversampling) this->osr_config_.bit.osr_p; + + // temporarily disables oversampling for temperature and pressure for a fast priming measurement + if (!this->write_oversampling_settings_(OVERSAMPLING_NONE, OVERSAMPLING_NONE)) { + ESP_LOGE(TAG, "Failed to write oversampling register"); + + return false; + } + + // flush the IIR filter with forced measurements (we will only flush once) + this->dsp_config_.bit.iir_flush_forced_en = true; + if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + ESP_LOGE(TAG, "Failed to write IIR source register"); + + return false; + } + + // forces an intial measurement + // - this measurements flushes the IIR filter reflecting written DSP settings + // - flushing with this initial reading avoids having the internal previous data aquisition being 0, which + // (I)nfinitely affects future values + if (!this->start_measurement_()) { + ESP_LOGE(TAG, "Failed to request a forced measurement"); + + return false; + } + + // wait for priming measurement to complete + // - with oversampling disabled, the conversion time for a single measurement for pressure and temperature is + // ceilf(1.05*(1.0+1.0)) = 3ms + // - see page 12 of datasheet for details + delay(3); + + if (!this->check_data_readiness_()) { + ESP_LOGE(TAG, "IIR priming measurement was not ready"); + + return false; + } + + // disable IIR filter flushings on future forced measurements + this->dsp_config_.bit.iir_flush_forced_en = false; + if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) { + ESP_LOGE(TAG, "Failed to write IIR source register"); + + return false; + } + + // revert oversampling rates to original settings + return this->write_oversampling_settings_(current_temperature_oversampling, current_pressure_oversampling); +} + +bool BMP581Component::read_temperature_(float &temperature) { + // - verifies data is ready to be read + // - reads in 3 bytes of temperature data + // - returns whether successful, where the the variable parameter contains + // - the measured temperature (in degrees Celsius) + + if (!this->check_data_readiness_()) { + ESP_LOGW(TAG, "Data from sensor isn't ready, skipping this update"); + this->status_set_warning(); + + return false; + } + + uint8_t data[3]; + if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) { + ESP_LOGW(TAG, "Failed to read sensor's measurement data"); + this->status_set_warning(); + + return false; + } + + // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] + int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) + + return true; +} + +bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &pressure) { + // - verifies data is ready to be read + // - reads in 6 bytes of temperature data (3 for temeperature, 3 for pressure) + // - returns whether successful, where the variable parameters contain + // - the measured temperature (in degrees Celsius) + // - the measured pressure (in Pa) + + if (!this->check_data_readiness_()) { + ESP_LOGW(TAG, "Data from sensor isn't ready, skipping this update"); + this->status_set_warning(); + + return false; + } + + uint8_t data[6]; + if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) { + ESP_LOGW(TAG, "Failed to read sensor's measurement data"); + this->status_set_warning(); + + return false; + } + + // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] + int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) + + // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] + int32_t raw_press = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; + pressure = (float) (raw_press / 64.0); // Divide by 2^6=64 for Pa (page 22 of datasheet) + + return true; +} + +bool BMP581Component::reset_() { + // - writes reset command to the command register + // - waits for sensor to complete reset + // - returns the Power-On-Reboot interrupt status, which is asserted if successful + + // writes reset command to BMP's command register + if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) { + ESP_LOGE(TAG, "Failed to write reset command"); + + return false; + } + + // t_{soft_res} = 2ms (page 11 of datasheet); time it takes to enter standby mode + // - round up to 3 ms + delay(3); + + // read interrupt status register + if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { + ESP_LOGE(TAG, "Failed to read interrupt status register"); + + return false; + } + + // Power-On-Reboot bit is asserted if sensor successfully reset + return this->int_status_.bit.por; +} + +bool BMP581Component::start_measurement_() { + // - only pushes the sensor into FORCED_MODE for a reading if already in STANDBY_MODE + // - returns whether a measurement is in progress or has been initiated + + if (this->odr_config_.bit.pwr_mode == STANDBY_MODE) { + return this->write_power_mode_(FORCED_MODE); + } else { + return true; + } +} + +bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter pressure_iir) { + // - ensures data registers store filtered values + // - sets IIR filter levels on sensor + // - matches other default settings on sensor + // - writes configuration to the two relevant registers + // - returns success or failure of write to the registers + + // If the temperature/pressure IIR filter is configured, then ensure data registers store the filtered measurement + this->dsp_config_.bit.shdw_sel_iir_t = (temperature_iir != IIR_FILTER_OFF); + this->dsp_config_.bit.shdw_sel_iir_p = (pressure_iir != IIR_FILTER_OFF); + + // set temperature and pressure IIR filter level to configured values + this->iir_config_.bit.set_iir_t = temperature_iir; + this->iir_config_.bit.set_iir_p = pressure_iir; + + // enable pressure and temperature compensation (page 61 of datasheet) + // - ?only relevant if IIR filter is applied?; the datasheet is ambiguous + // - matches BMP's default setting + this->dsp_config_.bit.comp_pt_en = 0x3; + + // BMP581_DSP register and BMP581_DSP_IIR registers are successive + // - allows us to write the IIR configuration with one command to both registers + uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg}; + return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data)); +} + +bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) { + // - updates component's internal setting + // - returns success or failure of write to interrupt source register + + this->int_source_.bit.drdy_data_reg_en = data_ready_enable; + + // write interrupt source register + return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg); +} + +bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling, + Oversampling pressure_oversampling) { + // - updates component's internal setting + // - returns success or failure of write to Over-Sampling Rate register + + this->osr_config_.bit.osr_t = temperature_oversampling; + this->osr_config_.bit.osr_p = pressure_oversampling; + + return this->write_byte(BMP581_OSR, this->osr_config_.reg); +} + +bool BMP581Component::write_power_mode_(OperationMode mode) { + // - updates the component's internal power mode + // - returns success or failure of write to Output Data Rate register + + this->odr_config_.bit.pwr_mode = mode; + + // write odr register + return this->write_byte(BMP581_ODR, this->odr_config_.reg); +} + +} // namespace bmp581 +} // namespace esphome diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h new file mode 100644 index 0000000000..7327be44ae --- /dev/null +++ b/esphome/components/bmp581/bmp581.h @@ -0,0 +1,222 @@ +// All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4) + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bmp581 { + +static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) +static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command + +// BMP581 Register Addresses +enum { + BMP581_CHIP_ID = 0x01, // read chip ID + BMP581_INT_SOURCE = 0x15, // write interrupt sources + BMP581_MEASUREMENT_DATA = + 0x1D, // read measurement registers, 0x1D-0x1F are temperature XLSB to MSB and 0x20-0x22 are pressure XLSB to MSB + BMP581_INT_STATUS = 0x27, // read interrupt statuses + BMP581_STATUS = 0x28, // read sensor status + BMP581_DSP = 0x30, // write sensor configuration + BMP581_DSP_IIR = 0x31, // write IIR filter configuration + BMP581_OSR = 0x36, // write oversampling configuration + BMP581_ODR = 0x37, // write data rate and power mode configuration + BMP581_COMMAND = 0x7E // write sensor command +}; + +// BMP581 Power mode operations +enum OperationMode { + STANDBY_MODE = 0x0, // no active readings + NORMAL_MODE = 0x1, // read continuously at ODR configured rate and standby between + FORCED_MODE = 0x2, // read sensor once (only reading mode used by this component) + NONSTOP_MODE = 0x3 // read continuously with no standby +}; + +// Temperature and pressure sensors can be oversampled to reduce noise +enum Oversampling { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7 +}; + +// Infinite Impulse Response filter reduces noise caused by ambient disturbances +enum IIRFilter { + IIR_FILTER_OFF = 0x0, + IIR_FILTER_2 = 0x1, + IIR_FILTER_4 = 0x2, + IIR_FILTER_8 = 0x3, + IIR_FILTER_16 = 0x4, + IIR_FILTER_32 = 0x5, + IIR_FILTER_64 = 0x6, + IIR_FILTER_128 = 0x7 +}; + +class BMP581Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void setup() override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } + + void set_temperature_iir_filter_config(IIRFilter iir_temperature_level) { + this->iir_temperature_level_ = iir_temperature_level; + } + void set_pressure_iir_filter_config(IIRFilter iir_pressure_level) { this->iir_pressure_level_ = iir_pressure_level; } + + void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; } + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + + Oversampling temperature_oversampling_; + Oversampling pressure_oversampling_; + + IIRFilter iir_temperature_level_; + IIRFilter iir_pressure_level_; + + // Stores the sensors conversion time needed for a measurement based on oversampling settings and datasheet (page 12) + // Computed in Python during codegen + uint8_t conversion_time_; + + // Checks if the BMP581 has measurement data ready by checking the sensor's interrupts + bool check_data_readiness_(); + + // Flushes the IIR filter and primes an initial reading + bool prime_iir_filter_(); + + // Reads temperature data from sensor and converts data to measurement in degrees Celsius + bool read_temperature_(float &temperature); + // Reads temperature and pressure data from sensor and converts data to measurements in degrees Celsius and Pa + bool read_temperature_and_pressure_(float &temperature, float &pressure); + + // Soft resets the BMP581 + bool reset_(); + + // Initiates a measurement on sensor by switching to FORCED_MODE + bool start_measurement_(); + + // Writes the IIR filter configuration to the DSP and DSP_IIR registers + bool write_iir_settings_(IIRFilter temperature_iir, IIRFilter pressure_iir); + + // Writes whether to enable the data ready interrupt to the interrupt source register + bool write_interrupt_source_settings_(bool data_ready_enable); + + // Writes the oversampling settings to the OSR register + bool write_oversampling_settings_(Oversampling temperature_oversampling, Oversampling pressure_oversampling); + + // Sets the power mode on the BMP581 by writing to the ODR register + bool write_power_mode_(OperationMode mode); + + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + ERROR_SENSOR_STATUS, + ERROR_SENSOR_RESET, + ERROR_PRIME_IIR_FAILED + } error_code_{NONE}; + + // BMP581's interrupt source register (address 0x15) to configure which interrupts are enabled (page 54 of datasheet) + union { + struct { + uint8_t drdy_data_reg_en : 1; // Data ready interrupt enable + uint8_t fifo_full_en : 1; // FIFO full interrupt enable + uint8_t fifo_ths_en : 1; // FIFO threshold/watermark interrupt enable + uint8_t oor_p_en : 1; // Pressure data out-of-range interrupt enable + } bit; + uint8_t reg; + } int_source_ = {.reg = 0}; + + // BMP581's interrupt status register (address 0x27) to determine ensor's current state (page 58 of datasheet) + union { + struct { + uint8_t drdy_data_reg : 1; // Data ready + uint8_t fifo_full : 1; // FIFO full + uint8_t fifo_ths : 1; // FIFO fhreshold/watermark + uint8_t oor_p : 1; // Pressure data out-of-range + uint8_t por : 1; // Power-On-Reset complete + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; + + // BMP581's status register (address 0x28) to determine if sensor has setup correctly (page 58 of datasheet) + union { + struct { + uint8_t status_core_rdy : 1; + uint8_t status_nvm_rdy : 1; // asserted if NVM is ready of operations + uint8_t status_nvm_err : 1; // asserted if NVM error + uint8_t status_nvm_cmd_err : 1; // asserted if boot command error + uint8_t status_boot_err_corrected : 1; // asserted if a boot error has been corrected + uint8_t : 2; + uint8_t st_crack_pass : 1; // asserted if crack check has executed without detecting a crack + } bit; + uint8_t reg; + } status_ = {.reg = 0}; + + // BMP581's dsp register (address 0x30) to configure data registers iir selection (page 61 of datasheet) + union { + struct { + uint8_t comp_pt_en : 2; // enable temperature and pressure compensation + uint8_t iir_flush_forced_en : 1; // IIR filter is flushed in forced mode + uint8_t shdw_sel_iir_t : 1; // temperature data register value selected before or after iir + uint8_t fifo_sel_iir_t : 1; // FIFO temperature data register value secected before or after iir + uint8_t shdw_sel_iir_p : 1; // pressure data register value selected before or after iir + uint8_t fifo_sel_iir_p : 1; // FIFO pressure data register value selected before or after iir + uint8_t oor_sel_iir_p : 1; // pressure out-of-range value selected before or after iir + } bit; + uint8_t reg; + } dsp_config_ = {.reg = 0}; + + // BMP581's iir register (address 0x31) to configure iir filtering(page 62 of datasheet) + union { + struct { + uint8_t set_iir_t : 3; // Temperature IIR filter coefficient + uint8_t set_iir_p : 3; // Pressure IIR filter coefficient + } bit; + uint8_t reg; + } iir_config_ = {.reg = 0}; + + // BMP581's OSR register (address 0x36) to configure Over-Sampling Rates (page 64 of datasheet) + union { + struct { + uint8_t osr_t : 3; // Temperature oversampling + uint8_t osr_p : 3; // Pressure oversampling + uint8_t press_en : 1; // Enables pressure measurement + } bit; + uint8_t reg; + } osr_config_ = {.reg = 0}; + + // BMP581's odr register (address 0x37) to configure output data rate and power mode (page 64 of datasheet) + union { + struct { + uint8_t pwr_mode : 2; // power mode of sensor + uint8_t odr : 5; // output data rate + uint8_t deep_dis : 1; // deep standby disabled if asserted + } bit; + uint8_t reg; + } odr_config_ = {.reg = 0}; +}; + +} // namespace bmp581 +} // namespace esphome diff --git a/esphome/components/bmp581/sensor.py b/esphome/components/bmp581/sensor.py new file mode 100644 index 0000000000..1e0350075a --- /dev/null +++ b/esphome/components/bmp581/sensor.py @@ -0,0 +1,163 @@ +import math +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["i2c"] + +bmp581_ns = cg.esphome_ns.namespace("bmp581") + +Oversampling = bmp581_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +IIRFilter = bmp581_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + +BMP581Component = bmp581_ns.class_( + "BMP581Component", cg.PollingComponent, i2c.I2CDevice +) + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # Page 12 of datasheet + PRESSURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.7, + "4X": 2.9, + "8X": 5.4, + "16X": 10.4, + "32X": 20.4, + "64X": 40.4, + "128X": 80.4, + } + + # Page 12 of datasheet + TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 1.0, + "2X": 1.1, + "4X": 1.5, + "8X": 2.1, + "16X": 3.3, + "32X": 5.8, + "64X": 10.8, + "128X": 20.8, + } + + pressure_conversion_time = ( + 0.0 # No conversion time necessary without a pressure sensor + ) + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[ + pressure_config.get(CONF_OVERSAMPLING) + ] + + temperature_conversion_time = ( + 1.0 # BMP581 always samples the temperature even if only reading pressure + ) + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[ + temperature_config.get(CONF_OVERSAMPLING) + ] + + # Datasheet indicates a 5% possible error in each conversion time listed + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP581Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x46)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) diff --git a/tests/test1.yaml b/tests/test1.yaml index 2419634570..254abe2b01 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1355,6 +1355,14 @@ sensor: name: "Distance" update_interval: 60s i2c_id: i2c_bus + - platform: bmp581 + i2c_id: i2c_bus + temperature: + name: "BMP581 Temperature" + iir_filter: 2x + pressure: + name: "BMP581 Pressure" + oversampling: 128x esp32_touch: setup_mode: false