Add MPU6886 driver (tested with M5Stack FIRE) (#56)

Will fix https://github.com/MicroPythonOS/MicroPythonOS/issues/44
This commit is contained in:
Jens Diemer
2026-02-21 07:41:06 +01:00
committed by GitHub
parent 4826063f36
commit ef30c80e49
3 changed files with 248 additions and 15 deletions
@@ -0,0 +1,83 @@
"""
MicroPython driver for MPU6886 3-Axis Accelerometer + 3-Axis Gyroscope.
Tested with M5Stack FIRE
https://docs.m5stack.com/en/unit/imu
https://github.com/m5stack/M5Stack/blob/master/src/utility/MPU6886.h
"""
import time
from machine import I2C
from micropython import const
_I2CADDR_DEFAULT = const(0x68)
# register addresses
_REG_PWR_MGMT_1 = const(0x6B)
_REG_ACCEL_XOUT_H = const(0x3B)
_REG_GYRO_XOUT_H = const(0x43)
_REG_ACCEL_CONFIG = const(0x1C)
_REG_GYRO_CONFIG = const(0x1B)
_REG_TEMPERATURE_OUT_H = const(0x41)
# Scale factors for converting raw sensor data to physical units:
_ACCEL_SCALE_8G = 8.0 / 32768.0 # LSB/g for +-8g range
_GYRO_SCALE_2000DPS = 2000.0 / 32768.0 # LSB/°/s for +-2000dps range
_TEMPERATURE_SCALE = 326.8 # LSB/°C
_TEMPERATURE_OFFSET = const(25) # Offset (25°C at 0 LSB)
def twos_complement(val, bits):
if val & (1 << (bits - 1)):
val -= 1 << bits
return val
class MPU6886:
def __init__(
self,
i2c_bus: I2C,
address: int = _I2CADDR_DEFAULT,
):
self.i2c = i2c_bus
self.address = address
for data in (b"\x00", b"\x80", b"\x01"): # Reset, then wake up
self._write(_REG_PWR_MGMT_1, data)
time.sleep(0.01)
self._write(_REG_ACCEL_CONFIG, b"\x10") # +-8g
time.sleep(0.001)
self._write(_REG_GYRO_CONFIG, b"\x18") # +-2000dps
time.sleep(0.001)
# Helper functions for register operations
def _write(self, reg: int, data: bytes):
self.i2c.writeto_mem(self.address, reg, data)
def _read_xyz(self, reg: int, scale: float) -> tuple[int, int, int]:
data = self.i2c.readfrom_mem(self.address, reg, 6)
x = twos_complement(data[0] << 8 | data[1], 16)
y = twos_complement(data[2] << 8 | data[3], 16)
z = twos_complement(data[4] << 8 | data[5], 16)
return (x * scale, y * scale, z * scale)
@property
def temperature(self) -> float:
buf = self.i2c.readfrom_mem(self.address, _REG_TEMPERATURE_OUT_H, 14)
temp_raw = (buf[6] << 8) | buf[7]
if temp_raw & 0x8000: # If MSB is 1, it's negative
temp_raw -= 0x10000 # Subtract 2^16 to get negative value
return temp_raw / _TEMPERATURE_SCALE + _TEMPERATURE_OFFSET
@property
def acceleration(self) -> tuple[int, int, int]:
"""Get current acceleration reading."""
return self._read_xyz(_REG_ACCEL_XOUT_H, scale=_ACCEL_SCALE_8G)
@property
def gyro(self) -> tuple[int, int, int]:
"""Get current gyroscope reading."""
return self._read_xyz(_REG_GYRO_XOUT_H, scale=_GYRO_SCALE_2000DPS)
@@ -1,7 +1,6 @@
# Hardware initialization for ESP32 M5Stack-Fire board
# Manufacturer's website at https://https://docs.m5stack.com/en/core/fire_v2.7
# Original author: https://github.com/ancebfer
import time
import drivers.display.ili9341 as ili9341
@@ -10,9 +9,9 @@ import lvgl as lv
import machine
import mpos.ui
import mpos.ui.focus_direction
from machine import PWM, Pin
from machine import I2C, PWM, Pin
from micropython import const
from mpos import AudioManager, InputManager
from mpos import AudioManager, InputManager, SensorManager
# Display settings:
SPI_BUS = const(1) # SPI2
@@ -40,6 +39,12 @@ BATTERY_PIN = const(35)
# Buzzer
BUZZER_PIN = const(25)
# MPU6886 Sensor settings:
MPU6886_I2C_ADDR = const(0x68)
MPU6886_I2C_SCL = const(22)
MPU6886_I2C_SDA = const(21)
MPU6886_I2C_FREQ = const(400000)
print("m5stack_fire.py init buzzer")
buzzer = PWM(Pin(BUZZER_PIN, Pin.OUT, value=1), duty=5)
@@ -50,6 +55,15 @@ while AudioManager.is_playing():
time.sleep(0.1)
print("m5stack_fire.py init IMU")
i2c_bus = I2C(0, scl=Pin(MPU6886_I2C_SCL), sda=Pin(MPU6886_I2C_SDA), freq=MPU6886_I2C_FREQ)
SensorManager.init(
i2c_bus=i2c_bus,
address=MPU6886_I2C_ADDR,
mounted_position=SensorManager.FACING_EARTH,
)
print("m5stack_fire.py machine.SPI.Bus() initialization")
try:
spi_bus = machine.SPI.Bus(host=SPI_BUS, mosi=LCD_MOSI, sck=LCD_SCLK)
+148 -12
View File
@@ -159,30 +159,48 @@ class SensorManager:
if not self._initialized or self._imu_driver is not None:
return self._imu_driver is not None
# Try QMI8658 first (Waveshare board)
if self._i2c_bus:
try:
from drivers.imu_sensor.qmi8658 import QMI8658
chip_id = self._i2c_bus.readfrom_mem(self._i2c_address, 0x00, 1)[0] # PARTID register
print("Try QMI8658 first (Waveshare board)")
# PARTID register:
chip_id = self._i2c_bus.readfrom_mem(self._i2c_address, 0x00, 1)[0]
print(f"{chip_id=:#04x}")
if chip_id == 0x05: # QMI8685_PARTID
self._imu_driver = _QMI8658Driver(self._i2c_bus, self._i2c_address)
self._register_qmi8658_sensors()
self._load_calibration()
print("Use QMI8658, ok")
return True
except:
pass
except Exception as e:
print("No QMI8658:", e)
# Try WSEN_ISDS (fri3d_2024) or LSM6DSO (fri3d_2026)
try:
from drivers.imu_sensor.wsen_isds import Wsen_Isds
chip_id = self._i2c_bus.readfrom_mem(self._i2c_address, 0x0F, 1)[0] # WHO_AM_I register - could also use Wsen_Isds.get_chip_id()
if chip_id == 0x6A or chip_id == 0x6C: # WSEN_ISDS WHO_AM_I 0x6A (Fri3d 2024) or 0x6C (Fri3d 2026)
print("Try WSEN_ISDS (fri3d_2024) or LSM6DSO (fri3d_2026)")
# WHO_AM_I register - could also use Wsen_Isds.get_chip_id():
chip_id = self._i2c_bus.readfrom_mem(self._i2c_address, 0x0F, 1)[0]
print(f"{chip_id=:#04x}")
# WSEN_ISDS WHO_AM_I 0x6A (Fri3d 2024) or 0x6C (Fri3d 2026):
if chip_id == 0x6A or chip_id == 0x6C:
self._imu_driver = _WsenISDSDriver(self._i2c_bus, self._i2c_address)
self._register_wsen_isds_sensors()
self._load_calibration()
print("Use WSEN_ISDS/LSM6DSO, ok")
return True
except:
pass
except Exception as e:
print("No WSEN_ISDS or LSM6DSO:", e)
try:
print("Try MPU6886 (M5Stack FIRE)")
chip_id = self._i2c_bus.readfrom_mem(self._i2c_address, 0x75, 1)[0]
print(f"{chip_id=:#04x}")
if chip_id == 0x19:
self._imu_driver = _MPU6886Driver(self._i2c_bus, self._i2c_address)
self._register_mpu6886_sensors()
self._load_calibration()
print("Use MPU6886, ok")
return True
except Exception as e:
print("No MPU6886:", e)
return False
@@ -575,7 +593,39 @@ class SensorManager:
power_ma=0
)
]
def _register_mpu6886_sensors(self):
"""Register MPU6886 sensors in sensor list."""
self._sensor_list = [
Sensor(
name="MPU6886 Accelerometer",
sensor_type=TYPE_ACCELEROMETER,
vendor="InvenSense",
version=1,
max_range="±16g",
resolution="0.0024 m/s²",
power_ma=0.2,
),
Sensor(
name="MPU6886 Gyroscope",
sensor_type=TYPE_GYROSCOPE,
vendor="InvenSense",
version=1,
max_range="±256 deg/s",
resolution="0.002 deg/s",
power_ma=0.7,
),
Sensor(
name="MPU6886 Temperature",
sensor_type=TYPE_IMU_TEMPERATURE,
vendor="InvenSense",
version=1,
max_range="-40°C to +85°C",
resolution="0.05°C",
power_ma=0,
),
]
def _register_wsen_isds_sensors(self):
"""Register WSEN_ISDS sensors in sensor list."""
self._sensor_list = [
@@ -813,6 +863,92 @@ class _QMI8658Driver(_IMUDriver):
self.gyro_offset = list(gyro_offsets)
class _MPU6886Driver(_IMUDriver):
"""Wrapper for MPU6886 IMU (Waveshare board)."""
def __init__(self, i2c_bus, address):
from drivers.imu_sensor.mpu6886 import MPU6886
self.sensor = MPU6886(i2c_bus, address=address)
# Software calibration offsets (MPU6886 has no built-in calibration)
self.accel_offset = [0.0, 0.0, 0.0]
self.gyro_offset = [0.0, 0.0, 0.0]
def read_temperature(self):
"""Read temperature in °C."""
return self.sensor.temperature
def read_acceleration(self):
"""Read acceleration in m/s² (converts from G)."""
ax, ay, az = self.sensor.acceleration
# Convert G to m/s² and apply calibration
return (
(ax * _GRAVITY) - self.accel_offset[0],
(ay * _GRAVITY) - self.accel_offset[1],
(az * _GRAVITY) - self.accel_offset[2],
)
def read_gyroscope(self):
"""Read gyroscope in deg/s (already in correct units)."""
gx, gy, gz = self.sensor.gyro
# Apply calibration
return (
gx - self.gyro_offset[0],
gy - self.gyro_offset[1],
gz - self.gyro_offset[2],
)
def calibrate_accelerometer(self, samples):
"""Calibrate accelerometer (device must be stationary)."""
sum_x, sum_y, sum_z = 0.0, 0.0, 0.0
for _ in range(samples):
ax, ay, az = self.sensor.acceleration
sum_x += ax * _GRAVITY
sum_y += ay * _GRAVITY
sum_z += az * _GRAVITY
time.sleep_ms(10)
if FACING_EARTH == FACING_EARTH:
sum_z *= -1
# Average offsets (assuming Z-axis should read +9.8 m/s²)
self.accel_offset[0] = sum_x / samples
self.accel_offset[1] = sum_y / samples
self.accel_offset[2] = (sum_z / samples) - _GRAVITY
return tuple(self.accel_offset)
def calibrate_gyroscope(self, samples):
"""Calibrate gyroscope (device must be stationary)."""
sum_x, sum_y, sum_z = 0.0, 0.0, 0.0
for _ in range(samples):
gx, gy, gz = self.sensor.gyro
sum_x += gx
sum_y += gy
sum_z += gz
time.sleep_ms(10)
# Average offsets (should be 0 when stationary)
self.gyro_offset[0] = sum_x / samples
self.gyro_offset[1] = sum_y / samples
self.gyro_offset[2] = sum_z / samples
return tuple(self.gyro_offset)
def get_calibration(self):
"""Get current calibration."""
return {"accel_offsets": self.accel_offset, "gyro_offsets": self.gyro_offset}
def set_calibration(self, accel_offsets, gyro_offsets):
"""Set calibration from saved values."""
if accel_offsets:
self.accel_offset = list(accel_offsets)
if gyro_offsets:
self.gyro_offset = list(gyro_offsets)
class _WsenISDSDriver(_IMUDriver):
"""Wrapper for WSEN_ISDS IMU (Fri3d badge)."""