[qmi8658] Motion platform for QMI8658 IMU (#16889)

This commit is contained in:
Clyde Stubbs
2026-06-30 09:04:25 +10:00
committed by GitHub
parent 1611345c55
commit 5c7245dfcd
9 changed files with 471 additions and 0 deletions
+1
View File
@@ -405,6 +405,7 @@ esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmi8658/* @clydebarrow
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
+13
View File
@@ -0,0 +1,13 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c", "motion"]
CONF_QMI8658_ID = "qmi8658_id"
# C++ namespace / class
qmi8658_ns = cg.esphome_ns.namespace("qmi8658")
QMI8658Component = qmi8658_ns.class_("QMI8658Component", MotionComponent, i2c.I2CDevice)
CONFIG_SCHEMA = {}
+93
View File
@@ -0,0 +1,93 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import (
CONF_ACCELEROMETER_ODR,
CONF_ACCELEROMETER_RANGE,
CONF_GYROSCOPE_ODR,
CONF_GYROSCOPE_RANGE,
)
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from . import QMI8658Component, qmi8658_ns
# Enum proxies (must match the C++ enum values exactly)
QMI8658AccelRange = qmi8658_ns.enum("QMI8658AccelRange")
ACCEL_RANGE_OPTIONS = {
"2G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_2G,
"4G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_4G,
"8G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_8G,
"16G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_16G,
}
QMI8658GyroRange = qmi8658_ns.enum("QMI8658GyroRange")
GYRO_RANGE_OPTIONS = {
"16DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_16,
"32DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_32,
"64DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_64,
"128DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_128,
"256DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_256,
"512DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_512,
"1024DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_1024,
"2048DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_2048,
}
QMI8658AccelODR = qmi8658_ns.enum("QMI8658AccelODR")
ACCEL_ODR_OPTIONS = {
"31_25HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_31_25,
"62_5HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_62_5,
"125HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_125,
"250HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_250,
"500HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_500,
"1000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_1000,
"2000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_2000,
"4000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_4000,
"8000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_8000,
}
QMI8658GyroODR = qmi8658_ns.enum("QMI8658GyroODR")
GYRO_ODR_OPTIONS = {
"31_25HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_31_25,
"62_5HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_62_5,
"125HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_125,
"250HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_250,
"500HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_500,
"1000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_1000,
"2000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_2000,
"4000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_4000,
"8000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_8000,
}
# Top-level CONFIG_SCHEMA
CONFIG_SCHEMA = (
motion_schema(QMI8658Component, has_accel=True, has_gyro=True)
.extend(
{
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
ACCEL_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_ACCELEROMETER_ODR, default="1000HZ"): cv.enum(
ACCEL_ODR_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_RANGE, default="2048DPS"): cv.enum(
GYRO_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_ODR, default="1000HZ"): cv.enum(
GYRO_ODR_OPTIONS, upper=True
),
}
)
.extend(i2c.i2c_device_schema(0x6B))
)
# Code generation
async def to_code(config):
var = await new_motion_component(config)
await i2c.register_i2c_device(var, config)
# Hardware configuration
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
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]))
+136
View File
@@ -0,0 +1,136 @@
#include "qmi8658.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::qmi8658 {
static const char *const TAG = "qmi8658";
// Acceleration scale (g per LSB), indexed by accel_range_ >> 4.
// Full-scale = range_g, mapped over a signed 16-bit value (2^15 counts).
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
16.0f / 32768.0f,
};
// Angular rate scale (°/s per LSB), indexed by gyro_range_ >> 4.
static constexpr float GYRO_SCALE[] = {
16.0f / 32768.0f, 32.0f / 32768.0f, 64.0f / 32768.0f, 128.0f / 32768.0f,
256.0f / 32768.0f, 512.0f / 32768.0f, 1024.0f / 32768.0f, 2048.0f / 32768.0f,
};
void QMI8658Component::setup() {
MotionComponent::setup();
// 1. Verify chip ID
uint8_t who_am_i = 0;
if (!this->read_byte(QMI8658_REG_WHO_AM_I, &who_am_i)) {
ESP_LOGE(TAG, "Failed to read chip ID - check wiring / address");
this->mark_failed();
return;
}
if (who_am_i != QMI8658_WHO_AM_I_VALUE) {
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", who_am_i, QMI8658_WHO_AM_I_VALUE);
this->mark_failed();
return;
}
// 2. Soft reset
if (!this->write_byte(QMI8658_REG_RESET, QMI8658_RESET_CMD)) {
this->mark_failed();
return;
}
delay(15); // spec: wait for reset to complete
// 3. Serial interface: enable register address auto-increment
if (!this->write_byte(QMI8658_REG_CTRL1, QMI8658_CTRL1_VALUE)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL1"));
return;
}
// 4. Configure accelerometer (CTRL2 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL2, (uint8_t) (this->accel_range_) | (uint8_t) (this->accel_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL2"));
return;
}
// 5. Configure gyroscope (CTRL3 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL3, (uint8_t) (this->gyro_range_) | (uint8_t) (this->gyro_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL3"));
return;
}
// 6. Disable the built-in low-pass filters (leave raw data to the motion pipeline)
if (!this->write_byte(QMI8658_REG_CTRL5, 0x00)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL5"));
this->mark_failed();
return;
}
// 7. Enable accelerometer and gyroscope
if (!this->write_byte(QMI8658_REG_CTRL7, QMI8658_CTRL7_ACC_EN | QMI8658_CTRL7_GYR_EN)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL7"));
return;
}
ESP_LOGCONFIG(TAG, "QMI8658 initialised successfully");
}
void QMI8658Component::dump_config() {
ESP_LOGCONFIG(TAG, "QMI8658 IMU:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, " Communication failed!");
return;
}
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±16°/s", "±32°/s", "±64°/s", "±128°/s",
"±256°/s", "±512°/s", "±1024°/s", "±2048°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[this->accel_range_ >> 4]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[this->gyro_range_ >> 4]);
MotionComponent::dump_config();
}
bool QMI8658Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
// Read temperature + accel + gyro in one contiguous block starting at TEMP_L.
uint8_t raw_data[REG_READ_LEN];
if (!this->read_bytes(QMI8658_REG_TEMP_L, raw_data, REG_READ_LEN)) {
ESP_LOGW(TAG, "Failed to read IMU data");
return false;
}
// Data is little-endian (low byte first).
float scale = ACCEL_SCALE[this->accel_range_ >> 4];
int16_t raw_x = encode_uint16(raw_data[ACC_OFFS + 1], raw_data[ACC_OFFS + 0]);
int16_t raw_y = encode_uint16(raw_data[ACC_OFFS + 3], raw_data[ACC_OFFS + 2]);
int16_t raw_z = encode_uint16(raw_data[ACC_OFFS + 5], raw_data[ACC_OFFS + 4]);
ESP_LOGV(TAG, "Read raw accel data: %d, %d, %d", raw_x, raw_y, raw_z);
data.acceleration[motion::X_AXIS] = raw_x * scale;
data.acceleration[motion::Y_AXIS] = raw_y * scale;
data.acceleration[motion::Z_AXIS] = raw_z * scale;
scale = GYRO_SCALE[this->gyro_range_ >> 4];
raw_x = encode_uint16(raw_data[GYR_OFFS + 1], raw_data[GYR_OFFS + 0]);
raw_y = encode_uint16(raw_data[GYR_OFFS + 3], raw_data[GYR_OFFS + 2]);
raw_z = encode_uint16(raw_data[GYR_OFFS + 5], raw_data[GYR_OFFS + 4]);
ESP_LOGV(TAG, "Read raw gyro data: %d, %d, %d", raw_x, raw_y, raw_z);
data.angular_rate[motion::X_AXIS] = raw_x * scale;
data.angular_rate[motion::Y_AXIS] = raw_y * scale;
data.angular_rate[motion::Z_AXIS] = raw_z * scale;
if (this->temperature_callback_.empty())
return true;
// Temperature: signed 16-bit, °C = raw / 256
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
this->temperature_callback_.call(raw_t / 256.0f);
return true;
}
} // namespace esphome::qmi8658
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include "esphome/components/motion/motion_component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::qmi8658 {
// Register map
static constexpr uint8_t QMI8658_REG_WHO_AM_I = 0x00;
static constexpr uint8_t QMI8658_REG_REVISION = 0x01;
static constexpr uint8_t QMI8658_REG_CTRL1 = 0x02; // serial interface / auto-increment
static constexpr uint8_t QMI8658_REG_CTRL2 = 0x03; // accelerometer ODR / range
static constexpr uint8_t QMI8658_REG_CTRL3 = 0x04; // gyroscope ODR / range
static constexpr uint8_t QMI8658_REG_CTRL5 = 0x06; // low-pass filter
static constexpr uint8_t QMI8658_REG_CTRL7 = 0x08; // sensor enable
static constexpr uint8_t QMI8658_REG_STATUS0 = 0x2E;
static constexpr uint8_t QMI8658_REG_TEMP_BASE = 0x33; // start of the data block
static constexpr uint8_t QMI8658_REG_TEMP_L = 0x33; // Low byte of temperature
static constexpr uint8_t QMI8658_REG_AX_L = 0x35;
static constexpr uint8_t QMI8658_REG_GX_L = 0x3B;
static constexpr uint8_t QMI8658_REG_RESET = 0x60;
// One contiguous read covers temperature (2) + accel (6) + gyro (6) starting at TEMP_L.
static constexpr uint8_t REG_READ_LEN = QMI8658_REG_GX_L + 6 - QMI8658_REG_TEMP_BASE; // 0x41 - 0x33 = 14
static constexpr uint8_t TEMP_OFFS = QMI8658_REG_TEMP_L - QMI8658_REG_TEMP_BASE; // 0
static constexpr uint8_t ACC_OFFS = QMI8658_REG_AX_L - QMI8658_REG_TEMP_BASE; // 2
static constexpr uint8_t GYR_OFFS = QMI8658_REG_GX_L - QMI8658_REG_TEMP_BASE; // 8
static constexpr uint8_t QMI8658_WHO_AM_I_VALUE = 0x05;
static constexpr uint8_t QMI8658_RESET_CMD = 0xB0;
// CTRL1: bit6 ADDR_AI (register address auto-increment); little-endian, 4-wire SPI
static constexpr uint8_t QMI8658_CTRL1_VALUE = 0x40;
// CTRL7: aEN (bit0) | gEN (bit1)
static constexpr uint8_t QMI8658_CTRL7_ACC_EN = 0x01;
static constexpr uint8_t QMI8658_CTRL7_GYR_EN = 0x02;
// Accelerometer range options (CTRL2 bits 6:4)
enum QMI8658AccelRange : uint8_t {
QMI8658_ACCEL_RANGE_2G = 0x00,
QMI8658_ACCEL_RANGE_4G = 0x10,
QMI8658_ACCEL_RANGE_8G = 0x20,
QMI8658_ACCEL_RANGE_16G = 0x30,
};
// Accelerometer ODR options (CTRL2 bits 3:0)
enum QMI8658AccelODR : uint8_t {
QMI8658_ACCEL_ODR_8000 = 0x00,
QMI8658_ACCEL_ODR_4000 = 0x01,
QMI8658_ACCEL_ODR_2000 = 0x02,
QMI8658_ACCEL_ODR_1000 = 0x03,
QMI8658_ACCEL_ODR_500 = 0x04,
QMI8658_ACCEL_ODR_250 = 0x05,
QMI8658_ACCEL_ODR_125 = 0x06,
QMI8658_ACCEL_ODR_62_5 = 0x07,
QMI8658_ACCEL_ODR_31_25 = 0x08,
};
// Gyroscope range options (CTRL3 bits 6:4)
enum QMI8658GyroRange : uint8_t {
QMI8658_GYRO_RANGE_16 = 0x00,
QMI8658_GYRO_RANGE_32 = 0x10,
QMI8658_GYRO_RANGE_64 = 0x20,
QMI8658_GYRO_RANGE_128 = 0x30,
QMI8658_GYRO_RANGE_256 = 0x40,
QMI8658_GYRO_RANGE_512 = 0x50,
QMI8658_GYRO_RANGE_1024 = 0x60,
QMI8658_GYRO_RANGE_2048 = 0x70,
};
// Gyroscope ODR options (CTRL3 bits 3:0)
enum QMI8658GyroODR : uint8_t {
QMI8658_GYRO_ODR_8000 = 0x00,
QMI8658_GYRO_ODR_4000 = 0x01,
QMI8658_GYRO_ODR_2000 = 0x02,
QMI8658_GYRO_ODR_1000 = 0x03,
QMI8658_GYRO_ODR_500 = 0x04,
QMI8658_GYRO_ODR_250 = 0x05,
QMI8658_GYRO_ODR_125 = 0x06,
QMI8658_GYRO_ODR_62_5 = 0x07,
QMI8658_GYRO_ODR_31_25 = 0x08,
};
// Main component class
class QMI8658Component : public motion::MotionComponent, public i2c::I2CDevice {
public:
// Lifecycle
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Configuration setters
void set_accel_range(QMI8658AccelRange r) { this->accel_range_ = r; }
void set_accel_odr(QMI8658AccelODR o) { this->accel_odr_ = o; }
void set_gyro_range(QMI8658GyroRange r) { this->gyro_range_ = r; }
void set_gyro_odr(QMI8658GyroODR o) { this->gyro_odr_ = o; }
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
protected:
bool update_data(motion::MotionData &data) override;
// Config
QMI8658AccelRange accel_range_{QMI8658_ACCEL_RANGE_4G};
QMI8658AccelODR accel_odr_{QMI8658_ACCEL_ODR_1000};
QMI8658GyroRange gyro_range_{QMI8658_GYRO_RANGE_2048};
QMI8658GyroODR gyro_odr_{QMI8658_GYRO_ODR_1000};
LazyCallbackManager<void(float)> temperature_callback_{};
};
} // namespace esphome::qmi8658
+39
View File
@@ -0,0 +1,39 @@
# 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 . import CONF_QMI8658_ID, QMI8658Component
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_QMI8658_ID): cv.use_id(QMI8658Component),
}
)
async def to_code(config):
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_QMI8658_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))
+69
View File
@@ -0,0 +1,69 @@
sensor:
- platform: qmi8658
name: "QMI8658 Temperature"
- platform: motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
type: pitch
name: "Pitch"
- platform: motion
type: roll
name: "Roll"
motion:
- platform: qmi8658
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Accelerometer output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
accelerometer_odr: 1000HZ
# Gyroscope full-scale range: 16DPS | 32DPS | 64DPS | 128DPS |
# 256DPS | 512DPS | 1024DPS | 2048DPS
gyroscope_range: 2048DPS
# Gyroscope output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
gyroscope_odr: 1000HZ
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml