[icm20689] Add ICM-20689 6-axis accelerometer/gyroscope (SPI) (#19234)

Co-authored-by: Ludovic BOUÉ <938089+lboue@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Ludovic BOUÉ
2026-10-13 08:07:32 -10:00
committed by GitHub
co-authored by Ludovic BOUÉ Claude Sonnet 5 pre-commit-ci-lite[bot] J. Nick Koston
parent 4a80d785a8
commit 95766ed3ad
10 changed files with 386 additions and 0 deletions
+1
View File
@@ -287,6 +287,7 @@ esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/i2s_clockless_led_strip/* @j9brown
esphome/components/iaqcore/* @yozik04
esphome/components/icm20689/* @lboue
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
+13
View File
@@ -0,0 +1,13 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@lboue"]
DOMAIN = "icm20689"
CONF_ICM20689_ID = "icm20689_id"
# C++ namespace / class
icm20689_ns = cg.esphome_ns.namespace("icm20689")
ICM20689Component = icm20689_ns.class_(
"ICM20689Component", MotionComponent, spi.SPIDevice
)
+126
View File
@@ -0,0 +1,126 @@
#include "icm20689.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cstddef>
#include <cstdint>
namespace esphome::icm20689 {
ESPHOME_LOG_TAG(TAG, "icm20689");
namespace {
// Range enum values 0..3 map to full-scale settings of 2x/4x/8x/16x the base range.
constexpr float accel_scale(uint8_t range) { return (2.0f * (1U << range)) / 32768.0f; }
constexpr float gyro_scale(uint8_t range) { return (250.0f * (1U << range)) / 32768.0f; }
} // namespace
void ICM20689Component::write_register_(uint8_t reg, uint8_t value) {
this->enable();
this->write_byte(reg & 0x7F);
this->write_byte(value);
this->disable();
}
void ICM20689Component::read_registers_(uint8_t reg, uint8_t *data, size_t len) {
this->enable();
this->write_byte(reg | ICM20689_READ_BIT);
this->read_array(data, len);
this->disable();
}
void ICM20689Component::setup() {
MotionComponent::setup();
this->spi_setup();
if (!this->spi_is_ready()) {
ESP_LOGE(TAG, "SPI device is not ready");
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
// Datasheet 4.20: a soft reset must be the first operation after power-up,
// before any register initialization. WHO_AM_I is only valid afterwards.
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_SOFT_RESET);
delay(100); // NOLINT
// The chip comes out of reset in sleep mode: wake it and select the
// auto-selected best clock source (required for full gyro performance).
this->write_register_(ICM20689_REG_PWR_MGMT_1, ICM20689_CLKSEL_AUTO);
// USER_CTRL is not writable in sleep mode, so I2C_IF_DIS is set once awake.
// Datasheet 6.1: disable the I2C interface when using SPI.
this->write_register_(ICM20689_REG_USER_CTRL, ICM20689_I2C_IF_DIS);
uint8_t who_am_i = 0;
this->read_registers_(ICM20689_REG_WHO_AM_I, &who_am_i, 1);
if (who_am_i != ICM20689_WHO_AM_I_VALUE) {
ESP_LOGE(TAG, "Unexpected WHO_AM_I 0x%02X (expected 0x%02X) -- check wiring/CS", who_am_i, ICM20689_WHO_AM_I_VALUE);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
// Allow the sensor to settle after waking from sleep.
delay(50); // NOLINT
// Configure the accelerometer and gyroscope full-scale ranges.
this->write_register_(ICM20689_REG_ACCEL_CONFIG, static_cast<uint8_t>(this->accel_range_) << 3);
this->write_register_(ICM20689_REG_GYRO_CONFIG, static_cast<uint8_t>(this->gyro_range_) << 3);
}
void ICM20689Component::dump_config() {
ESP_LOGCONFIG(TAG, "ICM20689 IMU:");
LOG_PIN(" CS Pin: ", this->cs_);
if (this->is_failed()) {
ESP_LOGE(TAG, " " ESP_LOG_MSG_COMM_FAIL);
return;
}
ESP_LOGCONFIG(TAG, " Accel range: ±%dg", 2 << static_cast<uint8_t>(this->accel_range_));
ESP_LOGCONFIG(TAG, " Gyro range: ±%d°/s", 250 << static_cast<uint8_t>(this->gyro_range_));
MotionComponent::dump_config();
}
bool ICM20689Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
const uint8_t accel_range = static_cast<uint8_t>(this->accel_range_);
const uint8_t gyro_range = static_cast<uint8_t>(this->gyro_range_);
// Read one contiguous sample:
// 0..5 accelerometer X/Y/Z
// 6..7 temperature
// 8..13 gyroscope X/Y/Z
uint8_t raw_data[14] = {};
this->read_registers_(ICM20689_REG_ACCEL_XOUT_H, raw_data, sizeof(raw_data));
const float accel_factor = accel_scale(accel_range);
data.acceleration[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[0], raw_data[1])) * accel_factor;
data.acceleration[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[2], raw_data[3])) * accel_factor;
data.acceleration[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[4], raw_data[5])) * accel_factor;
const float gyro_factor = gyro_scale(gyro_range);
data.angular_rate[motion::X_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[8], raw_data[9])) * gyro_factor;
data.angular_rate[motion::Y_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[10], raw_data[11])) * gyro_factor;
data.angular_rate[motion::Z_AXIS] = static_cast<int16_t>(encode_uint16(raw_data[12], raw_data[13])) * gyro_factor;
if (!this->temperature_callback_.empty()) {
// Datasheet formula: temperature (°C) = raw / 326.8 + 25.
const float temperature = static_cast<int16_t>(encode_uint16(raw_data[6], raw_data[7])) / 326.8f + 25.0f;
this->temperature_callback_.call(temperature);
}
return true;
}
} // namespace esphome::icm20689
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "esphome/components/motion/motion_component.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::icm20689 {
// Register map
static constexpr uint8_t ICM20689_REG_WHO_AM_I = 0x75;
static constexpr uint8_t ICM20689_REG_PWR_MGMT_1 = 0x6B;
static constexpr uint8_t ICM20689_REG_USER_CTRL = 0x6A;
static constexpr uint8_t ICM20689_REG_GYRO_CONFIG = 0x1B;
static constexpr uint8_t ICM20689_REG_ACCEL_CONFIG = 0x1C;
static constexpr uint8_t ICM20689_REG_ACCEL_XOUT_H = 0x3B;
static constexpr uint8_t ICM20689_READ_BIT = 0x80;
static constexpr uint8_t ICM20689_WHO_AM_I_VALUE = 0x98;
static constexpr uint8_t ICM20689_SOFT_RESET = 0x81; // PWR_MGMT_1: DEVICE_RESET | CLKSEL=1 (datasheet 4.20)
static constexpr uint8_t ICM20689_CLKSEL_AUTO = 0x01; // PWR_MGMT_1: SLEEP=0, CLKSEL=1 (auto-select best clock)
static constexpr uint8_t ICM20689_I2C_IF_DIS = 0x10; // USER_CTRL bit 4
// Accelerometer range options: FS_SEL bits [4:3] of ACCEL_CONFIG
enum ICM20689AccelRange : uint8_t {
ICM20689_ACCEL_RANGE_2G = 0x00,
ICM20689_ACCEL_RANGE_4G = 0x01,
ICM20689_ACCEL_RANGE_8G = 0x02,
ICM20689_ACCEL_RANGE_16G = 0x03,
};
// Gyroscope range options: FS_SEL bits [4:3] of GYRO_CONFIG
enum ICM20689GyroRange : uint8_t {
ICM20689_GYRO_RANGE_250 = 0x00,
ICM20689_GYRO_RANGE_500 = 0x01,
ICM20689_GYRO_RANGE_1000 = 0x02,
ICM20689_GYRO_RANGE_2000 = 0x03,
};
class ICM20689Component final : public motion::MotionComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
public:
void setup() override;
void dump_config() override;
void set_accel_range(ICM20689AccelRange range) { this->accel_range_ = range; }
void set_gyro_range(ICM20689GyroRange range) { this->gyro_range_ = range; }
template<typename F> void add_temperature_listener(F &&callback) {
this->temperature_callback_.add(std::forward<F>(callback));
}
protected:
bool update_data(motion::MotionData &data) override;
void write_register_(uint8_t reg, uint8_t value);
// Reads len bytes starting at reg in one CS-low transaction.
void read_registers_(uint8_t reg, uint8_t *data, size_t len);
ICM20689AccelRange accel_range_{ICM20689_ACCEL_RANGE_2G};
ICM20689GyroRange gyro_range_{ICM20689_GYRO_RANGE_250};
LazyCallbackManager<void(float)> temperature_callback_{};
};
} // namespace esphome::icm20689
+56
View File
@@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.const import CONF_ACCELEROMETER_RANGE, CONF_GYROSCOPE_RANGE
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import ICM20689Component, icm20689_ns
DEPENDENCIES = ["spi"]
# Enum proxies (must match the C++ enum values exactly)
ICM20689AccelRange = icm20689_ns.enum("ICM20689AccelRange")
ACCEL_RANGE_OPTIONS = {
"2G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_2G,
"4G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_4G,
"8G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_8G,
"16G": ICM20689AccelRange.ICM20689_ACCEL_RANGE_16G,
}
ICM20689GyroRange = icm20689_ns.enum("ICM20689GyroRange")
GYRO_RANGE_OPTIONS = {
"250DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_250,
"500DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_500,
"1000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_1000,
"2000DPS": ICM20689GyroRange.ICM20689_GYRO_RANGE_2000,
}
# Top-level CONFIG_SCHEMA
CONFIG_SCHEMA = (
motion_schema(ICM20689Component, has_accel=True, has_gyro=True)
.extend(
{
cv.Optional(CONF_ACCELEROMETER_RANGE, default="2G"): cv.enum(
ACCEL_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_RANGE, default="250DPS"): cv.enum(
GYRO_RANGE_OPTIONS, upper=True
),
}
)
.extend(
spi.spi_device_schema(
cs_pin_required=True, default_mode="MODE0", default_data_rate="1MHz"
)
)
)
# Code generation
async def to_code(config: ConfigType) -> None:
var = await new_motion_component(config)
await spi.register_spi_device(var, config)
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
+42
View File
@@ -0,0 +1,42 @@
# YAML config keys
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import CONF_ICM20689_ID, ICM20689Component
AUTO_LOAD = ["icm20689"]
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(
{
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
cv.GenerateID(CONF_ICM20689_ID): cv.use_id(ICM20689Component),
}
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_ICM20689_ID])
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))
+73
View File
@@ -0,0 +1,73 @@
sensor:
- platform: icm20689
type: temperature
name: "ICM20689 Temperature"
- platform: motion
motion_id: icm20689_motion
type: acceleration_x
name: "ICM20689 Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
motion_id: icm20689_motion
type: acceleration_y
name: "ICM20689 Accel Y"
accuracy_decimals: 4
- platform: motion
motion_id: icm20689_motion
type: acceleration_z
name: "ICM20689 Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
motion_id: icm20689_motion
type: gyroscope_x
name: "ICM20689 Gyro X"
- platform: motion
motion_id: icm20689_motion
type: gyroscope_y
name: "ICM20689 Gyro Y"
- platform: motion
motion_id: icm20689_motion
type: gyroscope_z
name: "ICM20689 Gyro Z"
- platform: motion
motion_id: icm20689_motion
type: angular_rate_x
name: "ICM20689 Angular Rate X"
- platform: motion
motion_id: icm20689_motion
type: angular_rate_y
name: "ICM20689 Angular Rate Y"
- platform: motion
motion_id: icm20689_motion
type: angular_rate_z
name: "ICM20689 Angular Rate Z"
- platform: motion
motion_id: icm20689_motion
type: pitch
name: "ICM20689 Pitch"
- platform: motion
motion_id: icm20689_motion
type: roll
name: "ICM20689 Roll"
motion:
- platform: icm20689
id: icm20689_motion
cs_pin: GPIO5
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Gyroscope full-scale range: 250DPS | 500DPS | 1000DPS | 2000DPS
gyroscope_range: 500DPS
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,3 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
icm20689: !include common.yaml
@@ -0,0 +1,3 @@
packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
icm20689: !include common.yaml
@@ -0,0 +1,3 @@
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
icm20689: !include common.yaml