[bmi270] Add optional BMM150 magnetometer support via aux interface (#18436)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Dennis Gaida
2026-10-12 13:08:41 -10:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent 54fdeb7b1b
commit 81c275777d
10 changed files with 518 additions and 58 deletions
+173 -22
View File
@@ -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;
}
+98 -20
View File
@@ -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 <functional>
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<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(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<typename F> void add_magnetometer_listener(F &&cb) { this->magnetometer_callback_.add(std::forward<F>(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<void(float)> temperature_callback_{};
#ifdef USE_BMI270_BMM150
LazyCallbackManager<void(const bmm150::BMM150Data &)> 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
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include <cmath>
#include <cstdint>
#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<int8_t>(regs[BMM150_TRIM_X1 - BMM150_REG_TRIM_START]);
trim.y1 = static_cast<int8_t>(regs[BMM150_TRIM_Y1 - BMM150_REG_TRIM_START]);
trim.z4 = static_cast<int16_t>(bmm150_trim_word(regs, BMM150_TRIM_Z4));
trim.x2 = static_cast<int8_t>(regs[BMM150_TRIM_X2 - BMM150_REG_TRIM_START]);
trim.y2 = static_cast<int8_t>(regs[BMM150_TRIM_Y2 - BMM150_REG_TRIM_START]);
trim.z2 = static_cast<int16_t>(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<int16_t>(bmm150_trim_word(regs, BMM150_TRIM_Z3));
trim.xy2 = static_cast<int8_t>(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
+17
View File
@@ -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]))
+74 -14
View File
@@ -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))
+1
View File
@@ -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
+11
View File
@@ -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
+16
View File
@@ -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
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
bmi270: !include common-plain.yaml
+2 -2
View File
@@ -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