diff --git a/esphome/components/bmi270/bmi270.cpp b/esphome/components/bmi270/bmi270.cpp index a1709c1fa5..37f718fb7e 100644 --- a/esphome/components/bmi270/bmi270.cpp +++ b/esphome/components/bmi270/bmi270.cpp @@ -5,12 +5,16 @@ namespace esphome::bmi270 { +#ifdef USE_BMI270_BMM150 +using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device definitions +#endif + ESPHOME_LOG_TAG(TAG, "bmi270"); #if defined(USE_ARDUINO) && !defined(USE_ESP32) -static const size_t MAX_I2C_BUFFER_SIZE = 32; +static constexpr size_t MAX_I2C_BUFFER_SIZE = 32; #else -static const size_t MAX_I2C_BUFFER_SIZE = 256; +static constexpr size_t MAX_I2C_BUFFER_SIZE = 256; #endif // Configuration blob upload @@ -19,12 +23,12 @@ static const size_t MAX_I2C_BUFFER_SIZE = 256; bool BMI270Component::load_config_file_() { // 1. Disable advanced power-save so the config port is accessible - if (!this->write_byte(BMI270_REG_PWR_CONF, 0x00)) + if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_ADV_POWER_SAVE_OFF)) return false; delay(1); // 2. Prepare config load: write 0x00 to INIT_CTRL to start - if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x00)) + if (!this->write_byte(BMI270_REG_INIT_CTRL, BMI270_INIT_CTRL_LOAD_START)) return false; // 3. Burst-write the config in pages @@ -50,7 +54,7 @@ bool BMI270Component::load_config_file_() { } // 4. Signal end of config load - if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x01)) + if (!this->write_byte(BMI270_REG_INIT_CTRL, BMI270_INIT_CTRL_LOAD_DONE)) return false; delay(20); // spec: wait ≥20 ms for init to complete @@ -58,7 +62,7 @@ bool BMI270Component::load_config_file_() { uint8_t status = 0; if (!this->read_byte(BMI270_REG_INTERNAL_STATUS, &status)) return false; - if ((status & 0x0F) != 0x01) { + if ((status & BMI270_INTERNAL_STATUS_MASK) != BMI270_INTERNAL_STATUS_INIT_OK) { ESP_LOGE(TAG, "Config load failed: INTERNAL_STATUS=0x%02X (expected 0x01)", status); return false; } @@ -84,7 +88,7 @@ void BMI270Component::setup() { ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id); // 2. Soft-reset via CMD register (0x7E = 0xB6) - if (!this->write_byte(0x7E, 0xB6)) { + if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) { this->mark_failed(); return; } @@ -100,7 +104,8 @@ void BMI270Component::setup() { // 5. Configure accelerometer // ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1) - uint8_t acc_conf = (uint8_t) (accel_odr_) | (0x2 << 4) | (1 << 7); + uint8_t acc_conf = + (uint8_t) (accel_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | BMI270_ACC_CONF_PERF_MODE; if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) { this->mark_failed(); return; @@ -112,7 +117,8 @@ void BMI270Component::setup() { // 6. Configure gyroscope // GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1) - uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (0x2 << 4) | (1 << 6) | (1 << 7); + uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | + BMI270_GYR_CONF_NOISE_PERF | BMI270_GYR_CONF_FILTER_PERF; if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) { this->mark_failed(); return; @@ -124,7 +130,7 @@ void BMI270Component::setup() { // 7. Enable accelerometer, gyroscope, and temperature sensor // PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1] - if (!this->write_byte(BMI270_REG_PWR_CTRL, 0x0E)) { + if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)) { this->mark_failed(); return; } @@ -132,14 +138,133 @@ void BMI270Component::setup() { // 8. Re-enable advanced power save (optional; keeps current low between reads) // Disabled here for simplicity – leave in performance mode - if (!this->write_byte(BMI270_REG_PWR_CONF, 0x02)) { // bit1 = fifo_self_wakeup + if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)) { // bit1 = fifo_self_wakeup this->mark_failed(); return; } + // 9. Optionally bring up whatever is wired to the aux interface. + // This is entirely additive: it is off by default, and failure only disables + // magnetometer reporting rather than the whole component. +#ifdef USE_BMI270_BMM150 + if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) { + this->magnetometer_ready_ = this->setup_magnetometer_(); + if (!this->magnetometer_ready_) { + ESP_LOGW(TAG, "BMM150 magnetometer setup failed; continuing without it"); + } + } +#endif + ESP_LOGCONFIG(TAG, "BMI270 initialised successfully"); } +#ifdef USE_BMI270_BMM150 +bool BMI270Component::wait_for_aux_idle_(uint32_t timeout_ms) { + // The STATUS aux-busy bit is set while an aux transaction is in flight. Aux transaction + // duration depends on the attached device's own timing, so poll with a bounded + // timeout instead of guessing a fixed delay. + uint32_t start = millis(); + while (true) { + uint8_t status = 0; + if (!this->read_byte(BMI270_REG_STATUS, &status)) + return false; + if ((status & BMI270_STATUS_AUX_BUSY) == 0) + return true; + if (millis() - start >= timeout_ms) + return false; + delay(1); + } +} + +bool BMI270Component::write_aux_register_(uint8_t reg, uint8_t value) { + if (!this->write_byte(BMI270_REG_AUX_WR_DATA, value)) + return false; + if (!this->write_byte(BMI270_REG_AUX_WR_ADDR, reg)) + return false; + return this->wait_for_aux_idle_(20); +} + +bool BMI270Component::read_aux_block_(uint8_t reg, uint8_t *buf) { + if (!this->write_byte(BMI270_REG_AUX_RD_ADDR, reg)) + return false; + if (!this->wait_for_aux_idle_(20)) + return false; + return this->read_bytes(BMI270_REG_AUX_X_LSB, buf, BMI270_AUX_DATA_LEN); +} + +bool BMI270Component::read_magnetometer_trim_() { + uint8_t regs[BMM150_TRIM_BLOCKS * BMM150_TRIM_BLOCK_LEN]; + for (uint8_t i = 0; i < BMM150_TRIM_BLOCKS; i++) { + if (!this->read_aux_block_(BMM150_REG_TRIM_START + i * BMM150_TRIM_BLOCK_LEN, regs + i * BMM150_TRIM_BLOCK_LEN)) + return false; + } + this->mag_trim_ = bmm150_parse_trim(regs); + return true; +} + +bool BMI270Component::setup_magnetometer_() { + // Sequence adapted from the Bosch BMI270 aux-interface protocol: enable the secondary + // I2C master in manual mode, point it at the BMM150's address, power the BMM150 up + // and soft-reset it, verify its chip ID, read its factory trim values, put it in + // normal/30Hz mode, then switch the aux interface to auto-burst mode so BMI270 keeps + // AUX_X/Y/Z/R refreshed from the BMM150's data registers. + ESP_LOGD(TAG, "Setting up BMM150 magnetometer via BMI270 aux interface..."); + + if (!this->write_byte(BMI270_REG_IF_CONF, BMI270_IF_CONF_AUX_IF_EN)) + return false; + if (!this->write_byte(BMI270_REG_AUX_IF_CONF, BMI270_AUX_IF_CONF_MANUAL_EN | BMI270_AUX_IF_CONF_MAN_RD_BURST_8)) + return false; + if (!this->write_byte(BMI270_REG_AUX_DEV_ID, (uint8_t) (this->aux_device_address_ << 1))) + return false; + + // Suspend -> sleep, then a soft reset so the BMM150 starts from a known state even if it + // stayed powered across a restart. The chip ID is only readable once the power bit is set. + if (!this->write_aux_register_(BMM150_REG_POWER_CONTROL, BMM150_CMD_POWER_ON)) { + ESP_LOGW(TAG, "BMM150 power-on write timed out"); + return false; + } + delay(BMM150_START_UP_TIME_MS); + if (!this->write_aux_register_(BMM150_REG_POWER_CONTROL, BMM150_CMD_SOFT_RESET)) { + ESP_LOGW(TAG, "BMM150 soft-reset write timed out"); + return false; + } + delay(BMM150_SOFT_RESET_TIME_MS); + + uint8_t chip_id[BMI270_AUX_DATA_LEN]; + if (!this->read_aux_block_(BMM150_REG_CHIP_ID, chip_id)) { + ESP_LOGW(TAG, "BMM150 chip-ID read failed"); + return false; + } + if (chip_id[0] != BMM150_CHIP_ID_VALUE) { + ESP_LOGW(TAG, "BMM150 not detected (chip ID 0x%02X, expected 0x%02X)", chip_id[0], BMM150_CHIP_ID_VALUE); + return false; + } + + if (!this->read_magnetometer_trim_()) { + ESP_LOGW(TAG, "BMM150 trim data read failed"); + return false; + } + + if (!this->write_aux_register_(BMM150_REG_OP_MODE, BMM150_CMD_NORMAL_MODE_ODR_30HZ)) { + ESP_LOGW(TAG, "BMM150 mode write timed out"); + return false; + } + + // Switch to auto-burst mode: BMI270 will keep polling 8 bytes from the BMM150's + // data registers (0x42) into its own AUX_X/Y/Z/R registers automatically. + if (!this->write_byte(BMI270_REG_AUX_IF_CONF, BMI270_AUX_IF_CONF_FCU_WRITE_EN | BMI270_AUX_IF_CONF_MAN_RD_BURST_8 | + BMI270_AUX_IF_CONF_RD_BURST_8)) + return false; + if (!this->write_byte(BMI270_REG_AUX_RD_ADDR, BMM150_REG_DATA_X_LSB)) + return false; + if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU | BMI270_PWR_CTRL_AUX_EN)) + return false; + + ESP_LOGD(TAG, "BMM150 magnetometer ready"); + return true; +} +#endif // USE_BMI270_BMM150 + void BMI270Component::dump_config() { ESP_LOGCONFIG(TAG, "BMI270 IMU:"); LOG_I2C_DEVICE(this); @@ -153,6 +278,12 @@ void BMI270Component::dump_config() { ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]); ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]); +#ifdef USE_BMI270_BMM150 + if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) { + ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)", + this->magnetometer_ready_ ? LOG_STR_LITERAL("ready") : LOG_STR_LITERAL("NOT DETECTED")); + } +#endif MotionComponent::dump_config(); } @@ -163,9 +294,22 @@ bool BMI270Component::update_data(motion::MotionData &data) { if (this->is_failed()) return false; - // Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb) - uint8_t raw_data[REG_READ_LEN]; - if (!this->read_bytes(BMI270_REG_DATA_8, raw_data, REG_READ_LEN)) { + // Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb) + // When the magnetometer is active, the 8 aux data bytes (0x04–0x0B) sit directly before + // the accel data, so one burst read from AUX_X_LSB fetches both. +#ifdef USE_BMI270_BMM150 + const bool read_mag = this->magnetometer_ready_ && !this->magnetometer_callback_.empty(); + uint8_t buf[BMI270_AUX_DATA_LEN + REG_READ_LEN]; + const uint8_t *mag_raw = buf; + const uint8_t *raw_data = read_mag ? buf + BMI270_AUX_DATA_LEN : buf; + const bool read_ok = read_mag ? this->read_bytes(BMI270_REG_AUX_X_LSB, buf, sizeof(buf)) + : this->read_bytes(BMI270_REG_DATA_8, buf, REG_READ_LEN); +#else + uint8_t buf[REG_READ_LEN]; + const uint8_t *raw_data = buf; + const bool read_ok = this->read_bytes(BMI270_REG_DATA_8, buf, REG_READ_LEN); +#endif + if (!read_ok) { ESP_LOGW(TAG, "Failed to read IMU data"); return false; } @@ -195,14 +339,21 @@ bool BMI270Component::update_data(motion::MotionData &data) { data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 3] << 8) | raw_data[GYR_OFFS + 2]) * scale; data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 5] << 8) | raw_data[GYR_OFFS + 4]) * scale; - if (this->temperature_callback_.empty()) - return true; - // Temperature: registers 0x22–0x23 - // Formula from datasheet: T[°C] = raw / 512 + 23 - static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8; - int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]); - float temperature = (raw_t / 512.0f) + 23.0f; - this->temperature_callback_.call(temperature); + if (!this->temperature_callback_.empty()) { + // Temperature: registers 0x22–0x23 + // Formula from datasheet: T[°C] = raw / 512 + 23 + static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8; + int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]); + float temperature = (raw_t / 512.0f) + 23.0f; + this->temperature_callback_.call(temperature); + } + +#ifdef USE_BMI270_BMM150 + if (read_mag) { + const BMM150Data mag_data = bmm150_convert(this->mag_trim_, mag_raw); + this->magnetometer_callback_.call(mag_data); + } +#endif return true; } diff --git a/esphome/components/bmi270/bmi270.h b/esphome/components/bmi270/bmi270.h index 56d6a60952..a1fefdffe8 100644 --- a/esphome/components/bmi270/bmi270.h +++ b/esphome/components/bmi270/bmi270.h @@ -4,35 +4,81 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/defines.h" +#ifdef USE_BMI270_BMM150 +#include "bmm150_aux.h" +#endif #include namespace esphome::bmi270 { // Register map -static const uint8_t BMI270_REG_CHIP_ID = 0x00; -static const uint8_t BMI270_REG_ERR_REG = 0x02; -static const uint8_t BMI270_REG_STATUS = 0x03; -static const uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB -static const uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB -static const uint8_t BMI270_REG_TEMP_0 = 0x22; -static const uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish) +static constexpr uint8_t BMI270_REG_CHIP_ID = 0x00; +static constexpr uint8_t BMI270_REG_ERR_REG = 0x02; +static constexpr uint8_t BMI270_REG_STATUS = 0x03; +static constexpr uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB +static constexpr uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB +static constexpr uint8_t BMI270_REG_TEMP_0 = 0x22; +static constexpr uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish) static constexpr uint8_t REG_READ_LEN = BMI270_REG_TEMP_MSB - BMI270_REG_DATA_8 + 1; // 0x23 - 0x0C + 1 = 0x18 bytes total for accel(6) + gyro(6) + temp(2) + padding(4) -static const uint8_t BMI270_REG_PWR_CONF = 0x7C; -static const uint8_t BMI270_REG_PWR_CTRL = 0x7D; -static const uint8_t BMI270_REG_INIT_CTRL = 0x59; -static const uint8_t BMI270_REG_INIT_DATA = 0x5E; -static const uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B; -static const uint8_t BMI270_REG_INTERNAL_STATUS = 0x21; -static const uint8_t BMI270_REG_ACC_CONF = 0x40; -static const uint8_t BMI270_REG_ACC_RANGE = 0x41; -static const uint8_t BMI270_REG_GYR_CONF = 0x42; -static const uint8_t BMI270_REG_GYR_RANGE = 0x43; +static constexpr uint8_t BMI270_REG_PWR_CONF = 0x7C; +static constexpr uint8_t BMI270_REG_PWR_CTRL = 0x7D; +static constexpr uint8_t BMI270_REG_INIT_CTRL = 0x59; +static constexpr uint8_t BMI270_REG_INIT_DATA = 0x5E; +static constexpr uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B; +static constexpr uint8_t BMI270_REG_INTERNAL_STATUS = 0x21; +static constexpr uint8_t BMI270_REG_ACC_CONF = 0x40; +static constexpr uint8_t BMI270_REG_ACC_RANGE = 0x41; +static constexpr uint8_t BMI270_REG_GYR_CONF = 0x42; +static constexpr uint8_t BMI270_REG_GYR_RANGE = 0x43; -static const uint8_t BMI270_CHIP_ID_VALUE = 0x24; +static constexpr uint8_t BMI270_CHIP_ID_VALUE = 0x24; + +// Auxiliary (secondary I2C master) interface registers. +// Used to optionally drive a BMM150 magnetometer wired to the BMI270's AUX pins +// instead of the main I2C bus - the BMM150 has no address of its own on that bus. +static constexpr uint8_t BMI270_REG_AUX_X_LSB = 0x04; // start of aux data: X,Y,Z,R (8 bytes) +static constexpr uint8_t BMI270_REG_AUX_DEV_ID = 0x4B; +static constexpr uint8_t BMI270_REG_AUX_IF_CONF = 0x4C; +static constexpr uint8_t BMI270_REG_AUX_RD_ADDR = 0x4D; +static constexpr uint8_t BMI270_REG_AUX_WR_ADDR = 0x4E; +static constexpr uint8_t BMI270_REG_AUX_WR_DATA = 0x4F; +static constexpr uint8_t BMI270_REG_IF_CONF = 0x6B; +static constexpr uint8_t BMI270_REG_CMD = 0x7E; + +// Register values and bitfields +static constexpr uint8_t BMI270_CMD_SOFT_RESET = 0xB6; +static constexpr uint8_t BMI270_PWR_CONF_ADV_POWER_SAVE_OFF = 0x00; +static constexpr uint8_t BMI270_PWR_CONF_FIFO_SELF_WAKEUP = 0x02; +static constexpr uint8_t BMI270_INIT_CTRL_LOAD_START = 0x00; +static constexpr uint8_t BMI270_INIT_CTRL_LOAD_DONE = 0x01; +static constexpr uint8_t BMI270_INTERNAL_STATUS_MASK = 0x0F; +static constexpr uint8_t BMI270_INTERNAL_STATUS_INIT_OK = 0x01; +static constexpr uint8_t BMI270_CONF_BWP_NORMAL = 0x02; // filter bandwidth, bits 5:4 +static constexpr uint8_t BMI270_CONF_BWP_SHIFT = 4; +static constexpr uint8_t BMI270_ACC_CONF_PERF_MODE = 0x80; +static constexpr uint8_t BMI270_GYR_CONF_NOISE_PERF = 0x40; +static constexpr uint8_t BMI270_GYR_CONF_FILTER_PERF = 0x80; +static constexpr uint8_t BMI270_STATUS_AUX_BUSY = 0x04; +static constexpr uint8_t BMI270_IF_CONF_AUX_IF_EN = 0x20; +static constexpr uint8_t BMI270_PWR_CTRL_AUX_EN = 0x01; +static constexpr uint8_t BMI270_PWR_CTRL_ACC_EN = 0x02; +static constexpr uint8_t BMI270_PWR_CTRL_GYR_EN = 0x04; +static constexpr uint8_t BMI270_PWR_CTRL_TEMP_EN = 0x08; +static constexpr uint8_t BMI270_PWR_CTRL_IMU = + BMI270_PWR_CTRL_ACC_EN | BMI270_PWR_CTRL_GYR_EN | BMI270_PWR_CTRL_TEMP_EN; +static constexpr uint8_t BMI270_AUX_IF_CONF_MANUAL_EN = 0x80; +static constexpr uint8_t BMI270_AUX_IF_CONF_FCU_WRITE_EN = 0x40; +static constexpr uint8_t BMI270_AUX_IF_CONF_MAN_RD_BURST_8 = 0x0C; // bits 3:2 = 3 +static constexpr uint8_t BMI270_AUX_IF_CONF_RD_BURST_8 = 0x03; // bits 1:0 = 3 + +// AUX_X_LSB..AUX_R_MSB (8 bytes) sit directly before the accel data, so one burst can read both. +static constexpr uint8_t BMI270_AUX_DATA_LEN = 8; +static_assert(BMI270_REG_AUX_X_LSB + BMI270_AUX_DATA_LEN == BMI270_REG_DATA_8, "aux data must precede accel data"); // Accelerometer range options enum BMI270AccelRange : uint8_t { @@ -75,7 +121,14 @@ enum BMI270GyroODR : uint8_t { BMI270_GYRO_ODR_3200 = 0x0D, }; -// ---Data class +#ifdef USE_BMI270_BMM150 +// What is wired to this instance's auxiliary (secondary I2C master) interface; another +// instance in the same build may have nothing attached. +enum BMI270AuxDevice : uint8_t { + BMI270_AUX_DEVICE_NONE = 0, + BMI270_AUX_DEVICE_BMM150 = 1, +}; +#endif // Main component class class BMI270Component final : public motion::MotionComponent, public i2c::I2CDevice { @@ -91,18 +144,43 @@ class BMI270Component final : public motion::MotionComponent, public i2c::I2CDev void set_gyro_range(BMI270GyroRange r) { this->gyro_range_ = r; } void set_gyro_odr(BMI270GyroODR o) { this->gyro_odr_ = o; } template void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward(cb)); } +#ifdef USE_BMI270_BMM150 + void set_aux_device(BMI270AuxDevice device) { this->aux_device_ = device; } + void set_aux_device_address(uint8_t address) { this->aux_device_address_ = address; } + template void add_magnetometer_listener(F &&cb) { this->magnetometer_callback_.add(std::forward(cb)); } +#endif protected: bool update_data(motion::MotionData &data) override; bool load_config_file_(); +#ifdef USE_BMI270_BMM150 + // Brings up the BMM150 over the BMI270's aux interface. Optional: on failure this only + // disables magnetometer reporting, it does not fail the whole component (accel/gyro still work). + bool setup_magnetometer_(); + // Blocking poll of the aux-transaction-busy bit; aux transaction time depends on the + // attached device, so this is bounded by retries/timeout rather than a fixed delay. + bool wait_for_aux_idle_(uint32_t timeout_ms); + // Manual-mode aux read of 8 bytes starting at `reg` on the aux device. + bool read_aux_block_(uint8_t reg, uint8_t *buf); + // Manual-mode aux write of one byte to `reg` on the aux device. + bool write_aux_register_(uint8_t reg, uint8_t value); + // Reads the BMM150 factory trim values used to compensate its samples. + bool read_magnetometer_trim_(); +#endif // Config BMI270AccelRange accel_range_{BMI270_ACCEL_RANGE_4G}; BMI270AccelODR accel_odr_{BMI270_ACCEL_ODR_100}; BMI270GyroRange gyro_range_{BMI270_GYRO_RANGE_2000}; BMI270GyroODR gyro_odr_{BMI270_GYRO_ODR_200}; - LazyCallbackManager temperature_callback_{}; +#ifdef USE_BMI270_BMM150 + LazyCallbackManager magnetometer_callback_{}; + bmm150::BMM150Trim mag_trim_{}; + BMI270AuxDevice aux_device_{BMI270_AUX_DEVICE_NONE}; + uint8_t aux_device_address_{bmm150::BMM150_DEFAULT_I2C_ADDRESS}; + bool magnetometer_ready_{false}; +#endif }; } // namespace esphome::bmi270 diff --git a/esphome/components/bmi270/bmm150_aux.h b/esphome/components/bmi270/bmm150_aux.h new file mode 100644 index 0000000000..747a0898fa --- /dev/null +++ b/esphome/components/bmi270/bmm150_aux.h @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include "esphome/core/helpers.h" + +// BMM150 magnetometer definitions - the chip that can optionally be wired to the BMI270's +// auxiliary (secondary I2C master) interface. Lives in its own header/namespace because this +// is BMM150-specific driver knowledge, not BMI270 knowledge - it just can't be a standalone +// ESPHome component, since the BMM150 has no address of its own on the main I2C bus and is only +// reachable by having the BMI270 proxy register reads/writes through its aux interface. +namespace esphome::bmi270::bmm150 { + +static constexpr uint8_t BMM150_DEFAULT_I2C_ADDRESS = 0x10; +static constexpr uint8_t BMM150_REG_CHIP_ID = 0x40; +static constexpr uint8_t BMM150_REG_DATA_X_LSB = 0x42; +static constexpr uint8_t BMM150_REG_POWER_CONTROL = 0x4B; +static constexpr uint8_t BMM150_REG_OP_MODE = 0x4C; +static constexpr uint8_t BMM150_CHIP_ID_VALUE = 0x32; +static constexpr uint8_t BMM150_CMD_POWER_ON = 0x01; // power control bit: suspend -> sleep (3 ms start-up) +static constexpr uint8_t BMM150_CMD_SOFT_RESET = 0x83; // soft reset bits 7 and 1, power control bit kept set +static constexpr uint8_t BMM150_CMD_NORMAL_MODE_ODR_30HZ = 0x38; // data rate 111b (30 Hz), opmode 00b (normal) +static constexpr uint32_t BMM150_START_UP_TIME_MS = 3; +static constexpr uint32_t BMM150_SOFT_RESET_TIME_MS = 1; + +// Factory trim registers 0x5D-0x71, read once at setup and used to compensate every sample. +static constexpr uint8_t BMM150_REG_TRIM_START = 0x5D; +static constexpr uint8_t BMM150_TRIM_BLOCK_LEN = 8; // one aux burst +static constexpr uint8_t BMM150_TRIM_BLOCKS = 3; // 0x5D, 0x65, 0x6D -> covers 0x5D-0x74 +static constexpr uint8_t BMM150_TRIM_X1 = 0x5D; +static constexpr uint8_t BMM150_TRIM_Y1 = 0x5E; +static constexpr uint8_t BMM150_TRIM_Z4 = 0x62; // 16-bit, LSB first +static constexpr uint8_t BMM150_TRIM_X2 = 0x64; +static constexpr uint8_t BMM150_TRIM_Y2 = 0x65; +static constexpr uint8_t BMM150_TRIM_Z2 = 0x68; // 16-bit +static constexpr uint8_t BMM150_TRIM_Z1 = 0x6A; // 16-bit +static constexpr uint8_t BMM150_TRIM_XYZ1 = 0x6C; // 16-bit, 15 valid bits +static constexpr uint8_t BMM150_TRIM_Z3 = 0x6E; // 16-bit +static constexpr uint8_t BMM150_TRIM_XY2 = 0x70; +static constexpr uint8_t BMM150_TRIM_XY1 = 0x71; +static constexpr uint16_t BMM150_TRIM_XYZ1_MASK = 0x7FFF; + +// Raw ADC values that mean "overflow" per the datasheet. +static constexpr int16_t BMM150_XY_OVERFLOW_ADCVAL = -4096; +static constexpr int16_t BMM150_Z_OVERFLOW_ADCVAL = -16384; + +// Result of a BMM150 magnetometer reading, in µT. An axis is NaN when the sensor reported overflow. +struct BMM150Data { + float x; + float y; + float z; +}; + +// Factory trim values (BMM150 datasheet, compensation section). +struct BMM150Trim { + int8_t x1, y1, x2, y2, xy2; + uint8_t xy1; + int16_t z2, z3, z4; + uint16_t z1, xyz1; +}; + +// A little-endian 16-bit trim word, addressed by its register +inline uint16_t bmm150_trim_word(const uint8_t *regs, uint8_t reg) { + const uint8_t offset = reg - BMM150_REG_TRIM_START; + return encode_uint16(regs[offset + 1], regs[offset]); +} + +// Decodes the trim registers; `regs` holds the bytes of registers 0x5D onwards (at least 21). +inline BMM150Trim bmm150_parse_trim(const uint8_t *regs) { + BMM150Trim trim; + trim.x1 = static_cast(regs[BMM150_TRIM_X1 - BMM150_REG_TRIM_START]); + trim.y1 = static_cast(regs[BMM150_TRIM_Y1 - BMM150_REG_TRIM_START]); + trim.z4 = static_cast(bmm150_trim_word(regs, BMM150_TRIM_Z4)); + trim.x2 = static_cast(regs[BMM150_TRIM_X2 - BMM150_REG_TRIM_START]); + trim.y2 = static_cast(regs[BMM150_TRIM_Y2 - BMM150_REG_TRIM_START]); + trim.z2 = static_cast(bmm150_trim_word(regs, BMM150_TRIM_Z2)); + trim.z1 = bmm150_trim_word(regs, BMM150_TRIM_Z1); + trim.xyz1 = bmm150_trim_word(regs, BMM150_TRIM_XYZ1) & BMM150_TRIM_XYZ1_MASK; + trim.z3 = static_cast(bmm150_trim_word(regs, BMM150_TRIM_Z3)); + trim.xy2 = static_cast(regs[BMM150_TRIM_XY2 - BMM150_REG_TRIM_START]); + trim.xy1 = regs[BMM150_TRIM_XY1 - BMM150_REG_TRIM_START]; + return trim; +} + +// Compensates an X or Y sample; returns µT, or NaN on overflow. Port of the Bosch BMM150 API (float variant). +inline float bmm150_compensate_xy(const BMM150Trim &trim, int8_t trim_a, int8_t trim_b, int16_t raw, uint16_t rhall) { + if (raw == BMM150_XY_OVERFLOW_ADCVAL || rhall == 0 || trim.xyz1 == 0) + return NAN; + float retval = (float) trim.xyz1 * 16384.0f / (float) rhall - 16384.0f; + float comp1 = (float) trim.xy2 * (retval * retval / 268435456.0f); + float comp2 = comp1 + retval * (float) trim.xy1 / 16384.0f; + float comp3 = (float) trim_b + 160.0f; + float comp4 = (float) raw * ((comp2 + 256.0f) * comp3); + return ((comp4 / 8192.0f) + (float) trim_a * 8.0f) / 16.0f; +} + +// Compensates a Z sample; returns µT, or NaN on overflow. Port of the Bosch BMM150 API (float variant). +inline float bmm150_compensate_z(const BMM150Trim &trim, int16_t raw, uint16_t rhall) { + if (raw == BMM150_Z_OVERFLOW_ADCVAL || trim.z2 == 0 || trim.z1 == 0 || trim.xyz1 == 0 || rhall == 0) + return NAN; + float comp0 = (float) raw - (float) trim.z4; + float comp1 = (float) rhall - (float) trim.xyz1; + float comp2 = (float) trim.z3 * comp1; + float comp3 = (float) trim.z1 * (float) rhall / 32768.0f; + float comp4 = (float) trim.z2 + comp3; + float comp5 = comp0 * 131072.0f - comp2; + return (comp5 / (comp4 * 4.0f)) / 16.0f; +} + +// Converts 8 raw bytes from the BMM150 data registers (X, Y, Z, RHALL; 0x42-0x49) to compensated µT. +inline BMM150Data bmm150_convert(const BMM150Trim &trim, const uint8_t *raw) { + // X/Y are 13-bit (LSB byte bits 7:3), Z is 15-bit (bits 7:1), RHALL is 14-bit (bits 7:2). + int16_t x = (int16_t) (((int16_t) (int8_t) raw[1] * 32) | (raw[0] >> 3)); + int16_t y = (int16_t) (((int16_t) (int8_t) raw[3] * 32) | (raw[2] >> 3)); + int16_t z = (int16_t) (((int16_t) (int8_t) raw[5] * 128) | (raw[4] >> 1)); + uint16_t rhall = (uint16_t) (((uint16_t) raw[7] << 6) | (raw[6] >> 2)); + return BMM150Data{ + .x = bmm150_compensate_xy(trim, trim.x1, trim.x2, x, rhall), + .y = bmm150_compensate_xy(trim, trim.y1, trim.y2, y, rhall), + .z = bmm150_compensate_z(trim, z, rhall), + }; +} + +} // namespace esphome::bmi270::bmm150 diff --git a/esphome/components/bmi270/motion.py b/esphome/components/bmi270/motion.py index ad36e592d1..97127fd2dd 100644 --- a/esphome/components/bmi270/motion.py +++ b/esphome/components/bmi270/motion.py @@ -14,7 +14,11 @@ from . import BMI270Component, bmi270_ns DEPENDENCIES = ["i2c"] +CONF_AUX_ADDRESS = "aux_address" +CONF_AUX_DEVICE = "aux_device" + # Enum proxies (must match the C++ enum values exactly) +BMI270AuxDevice = bmi270_ns.enum("BMI270AuxDevice") BMI270AccelRange = bmi270_ns.enum("BMI270AccelRange") ACCEL_RANGE_OPTIONS = { "2G": BMI270AccelRange.BMI270_ACCEL_RANGE_2G, @@ -73,6 +77,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_GYROSCOPE_ODR, default="200HZ"): cv.enum( GYRO_ODR_OPTIONS, upper=True ), + # Optional device wired to the BMI270's aux interface (not the main I2C bus). + # Defaults to NONE so existing configs/boards are unaffected. + cv.Optional(CONF_AUX_DEVICE, default="NONE"): cv.one_of( + "NONE", "BMM150", upper=True + ), + # Default matches the BMM150's fixed I2C address; irrelevant when aux_device is NONE. + cv.Optional(CONF_AUX_ADDRESS, default=0x10): cv.i2c_address, } ) .extend(i2c.i2c_device_schema(0x68)) @@ -90,3 +101,9 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR])) cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE])) cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR])) + # The aux interface code only compiles when some instance has a BMM150; the + # instance still records its own device, as another one may have none + if config[CONF_AUX_DEVICE] == "BMM150": + cg.add_define("USE_BMI270_BMM150") + cg.add(var.set_aux_device(BMI270AuxDevice.BMI270_AUX_DEVICE_BMM150)) + cg.add(var.set_aux_device_address(config[CONF_AUX_ADDRESS])) diff --git a/esphome/components/bmi270/sensor.py b/esphome/components/bmi270/sensor.py index 0e1b0604a3..242b86614d 100644 --- a/esphome/components/bmi270/sensor.py +++ b/esphome/components/bmi270/sensor.py @@ -6,37 +6,97 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_TYPE, DEVICE_CLASS_TEMPERATURE, + ICON_MAGNET, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + UNIT_MICROTESLA, ) from esphome.cpp_generator import MockObj +import esphome.final_validate as fv from esphome.types import ConfigType -from . import CONF_BMI270_ID, BMI270Component +from . import CONF_BMI270_ID, BMI270Component, bmi270_ns +from .motion import CONF_AUX_DEVICE AUTO_LOAD = ["bmi270"] -CONFIG_SCHEMA = sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, - accuracy_decimals=2, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_TEMPERATURE, -).extend( +CONF_MAGNETIC_FIELD_X = "magnetic_field_x" +CONF_MAGNETIC_FIELD_Y = "magnetic_field_y" +CONF_MAGNETIC_FIELD_Z = "magnetic_field_z" + +BMM150Data = bmi270_ns.namespace("bmm150").class_("BMM150Data") + +_MAGNETIC_FIELDS = (CONF_MAGNETIC_FIELD_X, CONF_MAGNETIC_FIELD_Y, CONF_MAGNETIC_FIELD_Z) + + +def _parent_schema() -> cv.Schema: + return cv.Schema({cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component)}) + + +def _temperature_sensor_schema() -> cv.Schema: + return sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_TEMPERATURE, + ).extend(_parent_schema()) + + +def _magnetic_field_sensor_schema() -> cv.Schema: + return sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(_parent_schema()) + + +# `type` defaults to temperature so existing configs that omit it keep working unchanged. +CONFIG_SCHEMA = cv.typed_schema( { - cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE), - cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component), - } + CONF_TEMPERATURE: _temperature_sensor_schema(), + **{x: _magnetic_field_sensor_schema() for x in _MAGNETIC_FIELDS}, + }, + default_type=CONF_TEMPERATURE, ) +def _final_validate(config: ConfigType) -> None: + if config[CONF_TYPE] not in _MAGNETIC_FIELDS: + return + full_config = fv.full_config.get() + bmi270_path = full_config.get_path_for_id(config[CONF_BMI270_ID])[:-1] + bmi270_config = full_config.get_config_for_path(bmi270_path) + if bmi270_config.get(CONF_AUX_DEVICE) != "BMM150": + raise cv.Invalid( + "This sensor requires the parent bmi270's 'aux_device' to be set to 'BMM150'", + path=[CONF_TYPE], + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config: ConfigType) -> None: + sensor_type = config[CONF_TYPE] var = await sensor.new_sensor(config) parent = await cg.get_variable(config[CONF_BMI270_ID]) + + if sensor_type == CONF_TEMPERATURE: + data = MockObj("data") + value_lambda = await cg.process_lambda( + var.publish_state(data), + [(cg.float_, str(data))], + ) + cg.add(parent.add_temperature_listener(value_lambda)) + return + + axis = sensor_type[-1:] data = MockObj("data") value_lambda = await cg.process_lambda( - var.publish_state(data), - [(cg.float_, str(data))], + var.publish_state(getattr(data, axis)), + [(BMM150Data.operator("const").operator("ref"), str(data))], ) - cg.add(parent.add_temperature_listener(value_lambda)) + cg.add(parent.add_magnetometer_listener(value_lambda)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a1b32b39f5..c645428c7c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,6 +48,7 @@ #define USE_BINARY_SENSOR_FILTER #define USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER #define USE_BLE_DEVICE_IRK +#define USE_BMI270_BMM150 #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE diff --git a/tests/components/bmi270/common-plain.yaml b/tests/components/bmi270/common-plain.yaml new file mode 100644 index 0000000000..13091521a8 --- /dev/null +++ b/tests/components/bmi270/common-plain.yaml @@ -0,0 +1,11 @@ +motion: + - platform: bmi270 + id: plain_imu + i2c_id: i2c_bus + address: 0x69 + +sensor: + - platform: bmi270 + bmi270_id: plain_imu + type: temperature + name: Plain IMU Temperature diff --git a/tests/components/bmi270/common.yaml b/tests/components/bmi270/common.yaml index 41f30fd5c6..390def04a1 100644 --- a/tests/components/bmi270/common.yaml +++ b/tests/components/bmi270/common.yaml @@ -1,7 +1,21 @@ sensor: - platform: bmi270 + bmi270_id: bmi270_motion name: "BMI270 Temperature" + - platform: bmi270 + bmi270_id: bmi270_motion + type: magnetic_field_x + name: "BMM150 Magnetic Field X" + - platform: bmi270 + bmi270_id: bmi270_motion + type: magnetic_field_y + name: "BMM150 Magnetic Field Y" + - platform: bmi270 + bmi270_id: bmi270_motion + type: magnetic_field_z + name: "BMM150 Magnetic Field Z" + - platform: motion motion_id: bmi270_motion type: acceleration_x @@ -75,6 +89,8 @@ motion: # Gyroscope output data rate: 25HZ | 50HZ | 100HZ | 200HZ | # 400HZ | 800HZ | 1600HZ | 3200HZ gyroscope_odr: 200HZ + aux_device: BMM150 + aux_address: 0x10 axis_map: x: y y: x diff --git a/tests/components/bmi270/test-plain.esp32-idf.yaml b/tests/components/bmi270/test-plain.esp32-idf.yaml new file mode 100644 index 0000000000..aafcb7af3a --- /dev/null +++ b/tests/components/bmi270/test-plain.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + bmi270: !include common-plain.yaml diff --git a/tests/components/bmi270/test.esp32-idf.yaml b/tests/components/bmi270/test.esp32-idf.yaml index b47e39c389..63ea3c5a1a 100644 --- a/tests/components/bmi270/test.esp32-idf.yaml +++ b/tests/components/bmi270/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml - -<<: !include common.yaml + bmi270: !include common.yaml + bmi270_plain: !include common-plain.yaml