From f71d0ca0a76e3e559b630ba0268ff61f7b5666d9 Mon Sep 17 00:00:00 2001 From: Charly-sketch Date: Thu, 12 Mar 2026 14:36:37 +0100 Subject: [PATCH 01/11] ism330dl: Add ism330dl driver. --- lib/ism330dl/README.md | 0 lib/ism330dl/examples/basic_read.py | 28 ++ lib/ism330dl/examples/test.py | 387 ++++++++++++++++++++++++++++ lib/ism330dl/ism330dl/__init__.py | 15 ++ lib/ism330dl/ism330dl/const.py | 134 ++++++++++ lib/ism330dl/ism330dl/device.py | 193 ++++++++++++++ lib/ism330dl/ism330dl/exceptions.py | 14 + lib/ism330dl/manifest.py | 6 + 8 files changed, 777 insertions(+) create mode 100644 lib/ism330dl/README.md create mode 100644 lib/ism330dl/examples/basic_read.py create mode 100644 lib/ism330dl/examples/test.py create mode 100644 lib/ism330dl/ism330dl/__init__.py create mode 100644 lib/ism330dl/ism330dl/const.py create mode 100644 lib/ism330dl/ism330dl/device.py create mode 100644 lib/ism330dl/ism330dl/exceptions.py create mode 100644 lib/ism330dl/manifest.py diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md new file mode 100644 index 00000000..e69de29b diff --git a/lib/ism330dl/examples/basic_read.py b/lib/ism330dl/examples/basic_read.py new file mode 100644 index 00000000..2ebc9ac9 --- /dev/null +++ b/lib/ism330dl/examples/basic_read.py @@ -0,0 +1,28 @@ +from machine import I2C +from time import sleep +from ism330dl import ISM330DL + +i2c = I2C(1) + +imu = ISM330DL(i2c, address=0x6B) + +print("ISM330DL example: basic read") +print() + +while True: + + ax, ay, az = imu.acceleration_g() + gx, gy, gz = imu.gyroscope_dps() + temp = imu.temperature_c() + + print( + "A[g]=({:+.2f},{:+.2f},{:+.2f}) " + "G[dps]=({:+.1f},{:+.1f},{:+.1f}) " + "T={:.1f}°C".format( + ax, ay, az, + gx, gy, gz, + temp + ) + ) + + sleep(0.5) diff --git a/lib/ism330dl/examples/test.py b/lib/ism330dl/examples/test.py new file mode 100644 index 00000000..bf8e9675 --- /dev/null +++ b/lib/ism330dl/examples/test.py @@ -0,0 +1,387 @@ +from machine import I2C, Pin +from time import sleep +from ism330dl import ISM330DL +from ism330dl.const import * + + +# --------------------------------------------------------------------- +# Update these pins and bus number to match your board +# --------------------------------------------------------------------- +TEMP_MIN = -40.0 +TEMP_MAX = 85.0 +ACCEL_MIN_G = 0.5 +ACCEL_MAX_G = 1.5 + + +def print_header(title): + print() + print("=" * 60) + print(title) + print("=" * 60) + + +def print_pass(name): + print("[PASS] {}".format(name)) + + +def print_fail(name, err=None): + if err is None: + print("[FAIL] {}".format(name)) + else: + print("[FAIL] {} -> {}".format(name, err)) + + +def dump_registers(sensor): + who_am_i = sensor._read_u8(REG_WHO_AM_I) + ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) + ctrl2_g = sensor._read_u8(REG_CTRL2_G) + ctrl3_c = sensor._read_u8(REG_CTRL3_C) + status = sensor._read_u8(REG_STATUS_REG) + + print("WHO_AM_I = 0x{:02X}".format(who_am_i)) + print("CTRL1_XL = 0x{:02X}".format(ctrl1_xl)) + print("CTRL2_G = 0x{:02X}".format(ctrl2_g)) + print("CTRL3_C = 0x{:02X}".format(ctrl3_c)) + print("STATUS_REG = 0x{:02X}".format(status)) + + +def test_i2c_scan(i2c): + print_header("1) I2C scan") + devices = i2c.scan() + print("I2C devices found:", [hex(x) for x in devices]) + + if ISM330DL_I2C_ADDR_LOW in devices or ISM330DL_I2C_ADDR_HIGH in devices: + print_pass("ISM330DL address found") + return True + else: + print_fail("ISM330DL address not found") + return False + + +def test_device_id(sensor): + print_header("2) Device ID") + dev_id = sensor.who_am_i() + print("WHO_AM_I:", hex(dev_id)) + + if dev_id == ISM330DL_WHO_AM_I_VALUE: + print_pass("WHO_AM_I matches 0x{:02X}".format(ISM330DL_WHO_AM_I_VALUE)) + return True + else: + print_fail("WHO_AM_I matches expected value", hex(dev_id)) + return False + + +def test_default_registers(sensor): + print_header("3) Default driver configuration") + dump_registers(sensor) + + ctrl3 = sensor._read_u8(REG_CTRL3_C) + + bdu_ok = bool(ctrl3 & CTRL3_C_BDU) + if_inc_ok = bool(ctrl3 & CTRL3_C_IF_INC) + + if bdu_ok: + print_pass("BDU enabled") + else: + print_fail("BDU enabled") + + if if_inc_ok: + print_pass("IF_INC enabled") + else: + print_fail("IF_INC enabled") + + return bdu_ok and if_inc_ok + + +def test_soft_reset(sensor): + print_header("4) Soft reset") + try: + sensor.reset() + sleep(0.05) + dump_registers(sensor) + + if sensor.who_am_i() == ISM330DL_WHO_AM_I_VALUE: + print_pass("Soft reset") + return True + else: + print_fail("Soft reset", "WHO_AM_I mismatch after reset") + return False + except Exception as err: + print_fail("Soft reset", err) + return False + + +def test_raw_readings(sensor): + print_header("5) Raw readings") + + try: + raw_accel = sensor.acceleration_raw() + raw_gyro = sensor.gyroscope_raw() + raw_temp = sensor.temperature_raw() + + print("Raw accel :", raw_accel) + print("Raw gyro :", raw_gyro) + print("Raw temp :", raw_temp) + + accel_ok = raw_accel != (0, 0, 0) + gyro_ok = raw_gyro != (0, 0, 0) + + if accel_ok: + print_pass("Accelerometer raw data is not all zero") + else: + print_fail("Accelerometer raw data is not all zero") + + if gyro_ok: + print_pass("Gyroscope raw data is not all zero") + else: + print_fail("Gyroscope raw data is not all zero") + + # temp can be close to zero raw around 25°C, so just reading it is enough + print_pass("Temperature raw read") + + return accel_ok and gyro_ok + + except Exception as err: + print_fail("Raw readings", err) + return False + + +def test_converted_readings(sensor): + print_header("6) Converted readings") + + try: + accel_g = sensor.acceleration_g() + accel_ms2 = sensor.acceleration_ms2() + gyro_dps = sensor.gyroscope_dps() + gyro_rads = sensor.gyroscope_rads() + temp_c = sensor.temperature_c() + + print("Acceleration [g] :", accel_g) + print("Acceleration [m/s²] :", accel_ms2) + print("Gyroscope [dps] :", gyro_dps) + print("Gyroscope [rad/s] :", gyro_rads) + print("Temperature [°C] : {:.2f}".format(temp_c)) + + temp_ok = TEMP_MIN <= temp_c <= TEMP_MAX + + # Basic accel sanity check: at rest one axis often sees ~1 g total norm nearby + ax, ay, az = accel_g + total_abs = abs(ax) + abs(ay) + abs(az) + accel_ok = ACCEL_MIN_G <= total_abs <= ACCEL_MAX_G + + if accel_ok: + print_pass("Acceleration is in a plausible range") + else: + print_fail("Acceleration is in a plausible range", total_abs) + + if temp_ok: + print_pass("Temperature is in a valid range") + else: + print_fail("Temperature is in a valid range", temp_c) + + return accel_ok and temp_ok + + except Exception as err: + print_fail("Converted readings", err) + return False + + +def test_configuration_change(sensor): + print_header("7) Configuration change") + + try: + sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_4G) + sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_500DPS) + sleep(0.1) + + ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) + ctrl2_g = sensor._read_u8(REG_CTRL2_G) + + print("CTRL1_XL = 0x{:02X}".format(ctrl1_xl)) + print("CTRL2_G = 0x{:02X}".format(ctrl2_g)) + print("Accel scale =", sensor._accel_scale, "g") + print("Gyro scale =", sensor._gyro_scale, "dps") + + accel_ok = sensor._accel_scale == ACCEL_FS_4G + gyro_ok = sensor._gyro_scale == GYRO_FS_500DPS + + if accel_ok: + print_pass("Accelerometer configuration updated") + else: + print_fail("Accelerometer configuration updated") + + if gyro_ok: + print_pass("Gyroscope configuration updated") + else: + print_fail("Gyroscope configuration updated") + + # Restore default config + sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) + sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) + + return accel_ok and gyro_ok + + except Exception as err: + print_fail("Configuration change", err) + return False + + +def test_status_flags(sensor): + print_header("8) STATUS helpers") + + try: + sleep(0.1) + + status = sensor.status() + + print("STATUS =", status) + + accel_ready = status["accel_ready"] + gyro_ready = status["gyro_ready"] + temp_ready = status["temp_ready"] + + print("accel_ready =", accel_ready) + print("gyro_ready =", gyro_ready) + print("temp_ready =", temp_ready) + + if accel_ready or gyro_ready or temp_ready: + print_pass("STATUS helper methods") + return True + else: + print_fail("STATUS helper methods") + return False + + except Exception as err: + print_fail("STATUS helper methods", err) + return False + + +def test_live_readings(sensor, samples=5): + print_header("9) Live readings") + + try: + previous = None + changed = False + + for i in range(samples): + accel = sensor.acceleration_g() + gyro = sensor.gyroscope_dps() + temp = sensor.temperature_c() + + print( + "#{:d} A=({:+.3f}, {:+.3f}, {:+.3f}) g " + "G=({:+.3f}, {:+.3f}, {:+.3f}) dps " + "T={:.2f} °C".format( + i + 1, + accel[0], accel[1], accel[2], + gyro[0], gyro[1], gyro[2], + temp, + ) + ) + + current = (accel, gyro, temp) + + if previous is not None and current != previous: + changed = True + + previous = current + sleep(0.5) + + if changed: + print_pass("Live readings change over time") + return True + else: + print_fail("Live readings change over time", "values stayed identical") + return False + + except Exception as err: + print_fail("Live readings", err) + return False + + +def test_power_down(sensor): + print_header("10) Power down") + + try: + sensor.power_down() + sleep(0.05) + + ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) + ctrl2_g = sensor._read_u8(REG_CTRL2_G) + + print("CTRL1_XL after power_down = 0x{:02X}".format(ctrl1_xl)) + print("CTRL2_G after power_down = 0x{:02X}".format(ctrl2_g)) + + accel_ok = ctrl1_xl == 0x00 + gyro_ok = ctrl2_g == 0x00 + + if accel_ok: + print_pass("Accelerometer powered down") + else: + print_fail("Accelerometer powered down", hex(ctrl1_xl)) + + if gyro_ok: + print_pass("Gyroscope powered down") + else: + print_fail("Gyroscope powered down", hex(ctrl2_g)) + + return accel_ok and gyro_ok + + except Exception as err: + print_fail("Power down", err) + return False + + +def main(): + print_header("ISM330DL full driver test") + + i2c = I2C(1) # Update bus number if needed + + if not test_i2c_scan(i2c): + print() + print("Stop: sensor not found on I2C bus.") + return + + try: + sensor = ISM330DL(i2c) + print_pass("Driver init") + except Exception as err: + print_fail("Driver init", err) + return + + results = [] + + results.append(test_device_id(sensor)) + results.append(test_default_registers(sensor)) + results.append(test_soft_reset(sensor)) + + # Reconfigure after reset for the following tests + try: + sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) + sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) + sleep(0.1) + except Exception as err: + print_fail("Post-reset reconfiguration", err) + return + + results.append(test_raw_readings(sensor)) + results.append(test_converted_readings(sensor)) + results.append(test_configuration_change(sensor)) + results.append(test_status_flags(sensor)) + results.append(test_live_readings(sensor, samples=5)) + results.append(test_power_down(sensor)) + + print_header("Final result") + + passed = sum(1 for x in results if x) + total = len(results) + + print("Passed: {}/{}".format(passed, total)) + + if passed == total: + print("All tests passed.") + else: + print("Some tests failed.") + + +main() diff --git a/lib/ism330dl/ism330dl/__init__.py b/lib/ism330dl/ism330dl/__init__.py new file mode 100644 index 00000000..19358d98 --- /dev/null +++ b/lib/ism330dl/ism330dl/__init__.py @@ -0,0 +1,15 @@ +from ism330dl.device import ISM330DL +from ism330dl.exceptions import ( + ISM330DLError, + ISM330DLNotFound, + ISM330DLConfigError, + ISM330DLIOError, +) + +__all__ = [ + "ISM330DL", + "ISM330DLConfigError", + "ISM330DLError", + "ISM330DLIOError", + "ISM330DLNotFound", +] diff --git a/lib/ism330dl/ism330dl/const.py b/lib/ism330dl/ism330dl/const.py new file mode 100644 index 00000000..c12538ab --- /dev/null +++ b/lib/ism330dl/ism330dl/const.py @@ -0,0 +1,134 @@ +# ISM330DL constants + + +# --------------------------------------------------------------------- +# I2C addresses / identity +# --------------------------------------------------------------------- +ISM330DL_I2C_ADDR_LOW = 0x6A +ISM330DL_I2C_ADDR_HIGH = 0x6B +ISM330DL_WHO_AM_I_VALUE = 0x6A + + +# --------------------------------------------------------------------- +# Registers +# --------------------------------------------------------------------- +REG_WHO_AM_I = 0x0F +REG_CTRL1_XL = 0x10 +REG_CTRL2_G = 0x11 +REG_CTRL3_C = 0x12 +REG_STATUS_REG = 0x1E + +REG_OUT_TEMP_L = 0x20 +REG_OUT_TEMP_H = 0x21 + +REG_OUTX_L_G = 0x22 +REG_OUTX_H_G = 0x23 +REG_OUTY_L_G = 0x24 +REG_OUTY_H_G = 0x25 +REG_OUTZ_L_G = 0x26 +REG_OUTZ_H_G = 0x27 + +REG_OUTX_L_XL = 0x28 +REG_OUTX_H_XL = 0x29 +REG_OUTY_L_XL = 0x2A +REG_OUTY_H_XL = 0x2B +REG_OUTZ_L_XL = 0x2C +REG_OUTZ_H_XL = 0x2D + + +# --------------------------------------------------------------------- +# CTRL3_C bits +# --------------------------------------------------------------------- +CTRL3_C_BDU = 1 << 6 +CTRL3_C_IF_INC = 1 << 2 +CTRL3_C_SW_RESET = 1 << 0 + + +# --------------------------------------------------------------------- +# STATUS bits +# --------------------------------------------------------------------- +STATUS_TDA = 1 << 2 +STATUS_GDA = 1 << 1 +STATUS_XLDA = 1 << 0 + + +# --------------------------------------------------------------------- +# Accelerometer ODR +# --------------------------------------------------------------------- +ACCEL_ODR_POWER_DOWN = 0x00 +ACCEL_ODR_12_5HZ = 0x01 +ACCEL_ODR_26HZ = 0x02 +ACCEL_ODR_52HZ = 0x03 +ACCEL_ODR_104HZ = 0x04 +ACCEL_ODR_208HZ = 0x05 +ACCEL_ODR_416HZ = 0x06 +ACCEL_ODR_833HZ = 0x07 +ACCEL_ODR_1660HZ = 0x08 + + +# --------------------------------------------------------------------- +# Accelerometer full scale +# --------------------------------------------------------------------- +ACCEL_FS_2G = 2 +ACCEL_FS_4G = 4 +ACCEL_FS_8G = 8 +ACCEL_FS_16G = 16 + +ACCEL_FS_BITS = { + ACCEL_FS_2G: 0x00, + ACCEL_FS_16G: 0x01, + ACCEL_FS_4G: 0x02, + ACCEL_FS_8G: 0x03, +} + +ACCEL_SENSITIVITY_MG = { + ACCEL_FS_2G: 0.061, + ACCEL_FS_4G: 0.122, + ACCEL_FS_8G: 0.244, + ACCEL_FS_16G: 0.488, +} + + +# --------------------------------------------------------------------- +# Gyroscope ODR +# --------------------------------------------------------------------- +GYRO_ODR_POWER_DOWN = 0x00 +GYRO_ODR_12_5HZ = 0x01 +GYRO_ODR_26HZ = 0x02 +GYRO_ODR_52HZ = 0x03 +GYRO_ODR_104HZ = 0x04 +GYRO_ODR_208HZ = 0x05 +GYRO_ODR_416HZ = 0x06 +GYRO_ODR_833HZ = 0x07 +GYRO_ODR_1660HZ = 0x08 + + +# --------------------------------------------------------------------- +# Gyroscope full scale +# --------------------------------------------------------------------- +GYRO_FS_125DPS = 125 +GYRO_FS_250DPS = 250 +GYRO_FS_500DPS = 500 +GYRO_FS_1000DPS = 1000 +GYRO_FS_2000DPS = 2000 + +GYRO_FS_BITS = { + GYRO_FS_250DPS: 0x00, + GYRO_FS_500DPS: 0x01, + GYRO_FS_1000DPS: 0x02, + GYRO_FS_2000DPS: 0x03, +} + +GYRO_SENSITIVITY_MDPS = { + GYRO_FS_125DPS: 4.375, + GYRO_FS_250DPS: 8.75, + GYRO_FS_500DPS: 17.50, + GYRO_FS_1000DPS: 35.0, + GYRO_FS_2000DPS: 70.0, +} + + +TEMP_SENSITIVITY = 256.0 +TEMP_OFFSET = 25.0 + +STANDARD_GRAVITY = 9.80665 diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py new file mode 100644 index 00000000..8f5fbe59 --- /dev/null +++ b/lib/ism330dl/ism330dl/device.py @@ -0,0 +1,193 @@ +from math import pi +from time import sleep_ms + +from ism330dl.const import * +from ism330dl.exceptions import * + + +class ISM330DL: + """MicroPython driver for the ISM330DL 6-axis IMU.""" + + def __init__(self, i2c, address=None): + self.i2c = i2c + + if address is None: # Auto-detect address. Je ne sais pas si c'est nécessaire pour la STeaMi, dans le manuel du capteur il y a 2 adresses possibles, 0x6A et 0x6B. + for addr in (ISM330DL_I2C_ADDR_LOW, ISM330DL_I2C_ADDR_HIGH): + try: + if i2c.readfrom_mem(addr, REG_WHO_AM_I, 1)[0] == ISM330DL_WHO_AM_I_VALUE: + address = addr + break + except OSError: + pass + + if address is None: + raise ISM330DLNotFound("ISM330DL not detected on I2C bus") + + self.address = address + + self._accel_scale = ACCEL_FS_2G + self._gyro_scale = GYRO_FS_250DPS + + self.reset() + self.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) + self.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) + + # -------------------------------------------------- + # Low level I2C + # -------------------------------------------------- + + def _read_u8(self, reg): + return self.i2c.readfrom_mem(self.address, reg, 1)[0] + + def _write_u8(self, reg, value): + self.i2c.writeto_mem(self.address, reg, bytes([value])) + + def _read_bytes(self, reg, n): + return self.i2c.readfrom_mem(self.address, reg, n) + + def _read_i16(self, reg): + data = self._read_bytes(reg, 2) + value = data[0] | (data[1] << 8) + if value & 0x8000: + value -= 0x10000 + return value + + def _read_vector(self, reg): + data = self._read_bytes(reg, 6) + + x = data[0] | (data[1] << 8) + y = data[2] | (data[3] << 8) + z = data[4] | (data[5] << 8) + + if x & 0x8000: + x -= 0x10000 + if y & 0x8000: + y -= 0x10000 + if z & 0x8000: + z -= 0x10000 + + return (x, y, z) + + # -------------------------------------------------- + # Device check + # -------------------------------------------------- + + def who_am_i(self): + return self._read_u8(REG_WHO_AM_I) + + def check_device(self): + if self.who_am_i() != ISM330DL_WHO_AM_I_VALUE: + raise ISM330DLNotFound("ISM330DL not detected") + + # -------------------------------------------------- + # Reset + # -------------------------------------------------- + + def reset(self): + self._write_u8(REG_CTRL3_C, CTRL3_C_SW_RESET) + sleep_ms(50) + + self._write_u8(REG_CTRL3_C, CTRL3_C_BDU | CTRL3_C_IF_INC) + + # -------------------------------------------------- + # Configuration + # -------------------------------------------------- + + def configure_accel(self, odr, scale): + + if scale not in ACCEL_FS_BITS: + raise ISM330DLConfigError("Invalid accel scale") + + self._accel_scale = scale + + value = (odr << 4) | (ACCEL_FS_BITS[scale] << 2) + + self._write_u8(REG_CTRL1_XL, value) + + def configure_gyro(self, odr, scale): + + if scale == GYRO_FS_125DPS: + value = (odr << 4) | 0x02 + else: + if scale not in GYRO_FS_BITS: + raise ISM330DLConfigError("Invalid gyro scale") + + value = (odr << 4) | (GYRO_FS_BITS[scale] << 2) + + self._gyro_scale = scale + + self._write_u8(REG_CTRL2_G, value) + + # -------------------------------------------------- + # Raw readings + # -------------------------------------------------- + + def acceleration_raw(self): + return self._read_vector(REG_OUTX_L_XL) + + def gyroscope_raw(self): + return self._read_vector(REG_OUTX_L_G) + + def temperature_raw(self): + return self._read_i16(REG_OUT_TEMP_L) + + # -------------------------------------------------- + # Converted readings + # -------------------------------------------------- + + def acceleration_g(self): + + sens = ACCEL_SENSITIVITY_MG[self._accel_scale] + + raw = self.acceleration_raw() + + return tuple((v * sens) / 1000.0 for v in raw) + + def acceleration_ms2(self): + + g = self.acceleration_g() + + return tuple(v * STANDARD_GRAVITY for v in g) + + def gyroscope_dps(self): + + sens = GYRO_SENSITIVITY_MDPS[self._gyro_scale] + + raw = self.gyroscope_raw() + + return tuple((v * sens) / 1000.0 for v in raw) + + def gyroscope_rads(self): + + dps = self.gyroscope_dps() + + return tuple(v * pi / 180.0 for v in dps) + + def temperature_c(self): + + raw = self.temperature_raw() + + return TEMP_OFFSET + raw / TEMP_SENSITIVITY + + # -------------------------------------------------- + # Status + # -------------------------------------------------- + + def status(self): + + s = self._read_u8(REG_STATUS_REG) + + return { + "temp_ready": bool(s & STATUS_TDA), + "gyro_ready": bool(s & STATUS_GDA), + "accel_ready": bool(s & STATUS_XLDA), + } + + # -------------------------------------------------- + # Power + # -------------------------------------------------- + + def power_down(self): + + self._write_u8(REG_CTRL1_XL, 0) + self._write_u8(REG_CTRL2_G, 0) diff --git a/lib/ism330dl/ism330dl/exceptions.py b/lib/ism330dl/ism330dl/exceptions.py new file mode 100644 index 00000000..5681b0f6 --- /dev/null +++ b/lib/ism330dl/ism330dl/exceptions.py @@ -0,0 +1,14 @@ +class ISM330DLError(Exception): + """Base exception for ISM330DL driver.""" + + +class ISM330DLNotFound(ISM330DLError): + """Raised when the sensor is not detected.""" + + +class ISM330DLConfigError(ISM330DLError): + """Raised when configuration is invalid.""" + + +class ISM330DLIOError(ISM330DLError): + """Raised when an I2C communication error occurs.""" diff --git a/lib/ism330dl/manifest.py b/lib/ism330dl/manifest.py new file mode 100644 index 00000000..c1eb34a9 --- /dev/null +++ b/lib/ism330dl/manifest.py @@ -0,0 +1,6 @@ +metadata( + description="Driver of ISM330DL gyroscope and accelerometer sensor.", + version="0.0.1", +) + +package("ism330dl") From f17b7534a47d4914e1a33e73a48e16f5af5c5bd9 Mon Sep 17 00:00:00 2001 From: Charly-sketch Date: Thu, 12 Mar 2026 15:14:12 +0100 Subject: [PATCH 02/11] ism330dl: Add examples. --- lib/ism330dl/README.md | 258 ++++++++++++++++++++ lib/ism330dl/examples/motion_orientation.py | 17 ++ lib/ism330dl/examples/static_orientation.py | 22 ++ lib/ism330dl/ism330dl/device.py | 42 ++++ 4 files changed, 339 insertions(+) create mode 100644 lib/ism330dl/examples/motion_orientation.py create mode 100644 lib/ism330dl/examples/static_orientation.py diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md index e69de29b..c4625734 100644 --- a/lib/ism330dl/README.md +++ b/lib/ism330dl/README.md @@ -0,0 +1,258 @@ +Voici un **README propre et complet** pour ton driver **ISM330DL**, dans le style des drivers MicroPython (similaire à ce que tu as fait pour `wsen-pads` et `lis2mdl`). + +--- + +# ISM330DL MicroPython Driver + +MicroPython driver for the **STMicroelectronics ISM330DL** 6-axis IMU. + +The ISM330DL integrates: + +* a **3-axis accelerometer** +* a **3-axis gyroscope** + +This driver provides a simple API to configure the sensor and read motion data using **I²C**. + +--- + +# Features + +* I²C communication +* automatic device detection (`WHO_AM_I`) +* accelerometer configuration +* gyroscope configuration +* raw sensor readings +* converted physical units +* board orientation reading +* board rotation reading +* temperature reading +* data-ready status helpers +* power-down mode + +--- + +# Sensor overview + +| Sensor | Range | Unit | +| ------------- | ---------------------------------- | --------- | +| Accelerometer | ±2g / ±4g / ±8g / ±16g | g | +| Gyroscope | ±125 / ±250 / ±500 / ±1000 / ±2000 | degrees/s | +| Temperature | internal sensor | °C | + +--- + +# I²C Address + +The sensor can use two I²C addresses depending on the **SA0 pin**: + +| SA0 | Address | +| --- | ------- | +| 0 | `0x6A` | +| 1 | `0x6B` | + +Most breakout boards use **0x6B**. + +--- + +# Basic Usage + +```python +from machine import I2C, Pin +from ism330dl import ISM330DL + +i2c = I2C(1) + +imu = ISM330DL(i2c, address=0x6B) + +ax, ay, az = imu.acceleration_g() +gx, gy, gz = imu.gyroscope_dps() +temp = imu.temperature_c() + +print("Accel:", ax, ay, az) +print("Gyro :", gx, gy, gz) +print("Temp :", temp) +``` + +--- + +# API + +## Initialization + +```python +imu = ISM330DL(i2c) +``` + +--- + +## Accelerometer + +### Raw data + +```python +imu.acceleration_raw() +``` + +Returns: + +``` +(x, y, z) +``` + +16-bit raw values. + +--- + +### Acceleration in g + +```python +imu.acceleration_g() +``` + +Example: + +``` +(0.01, -0.02, 0.99) +``` + +--- + +### Acceleration in m/s² + +```python +imu.acceleration_ms2() +``` + +### Board orientation + +```python +imu.orientation() +``` + +--- + +## Gyroscope + +### Raw data + +```python +imu.gyroscope_raw() +``` + +--- + +### Angular velocity in degrees per second + +```python +imu.gyroscope_dps() +``` + +Example: + +``` +(0.5, -0.3, 1.2) +``` + +--- + +### Angular velocity in radians per second + +```python +imu.gyroscope_rads() +``` + +### motion orientation + +```python +imu.gyro_motion() +``` + +--- + +## Temperature + +```python +imu.temperature_c() +``` + +Example: + +``` +26.3 +``` + +--- + +## Configuration + +### Accelerometer + +```python +imu.configure_accel(odr, scale) +``` + +Example: + +```python +from ism330dl.const import ACCEL_ODR_104HZ, ACCEL_FS_4G + +imu.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_4G) +``` + +--- + +### Gyroscope + +```python +imu.configure_gyro(odr, scale) +``` + +Example: + +```python +from ism330dl.const import GYRO_ODR_104HZ, GYRO_FS_500DPS + +imu.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_500DPS) +``` + +--- + +## Status + +```python +imu.status() +``` + +Returns: + +``` +{ + "temp_ready": True, + "gyro_ready": True, + "accel_ready": True +} +``` + +--- + +## Power Down + +```python +imu.power_down() +``` + +Stops accelerometer and gyroscope. + +--- + +# Examples + +The repository provides several example scripts: + +| Example | Description | +| ----------------------- | ------------------------------------------------- | +| `basic_reader.py` | Simple sensor readout | +| `static_orientation.py` | Detect device orientation using the accelerometer | +| `motion_orientation.py` | Detect rotation using the gyroscope | + +--- \ No newline at end of file diff --git a/lib/ism330dl/examples/motion_orientation.py b/lib/ism330dl/examples/motion_orientation.py new file mode 100644 index 00000000..0a476c1f --- /dev/null +++ b/lib/ism330dl/examples/motion_orientation.py @@ -0,0 +1,17 @@ +from machine import I2C +from time import sleep +from ism330dl import ISM330DL + +i2c = I2C(1) + +imu = ISM330DL(i2c, address=0x6B) + +print("ISM330DL gyroscope direction demo") +print() + +while True: + motion, speed = imu.motion() + + print("Motion: {} Speed: {:.1f} dps".format(motion, speed)) + + sleep(0.2) diff --git a/lib/ism330dl/examples/static_orientation.py b/lib/ism330dl/examples/static_orientation.py new file mode 100644 index 00000000..00fc99bf --- /dev/null +++ b/lib/ism330dl/examples/static_orientation.py @@ -0,0 +1,22 @@ +from machine import I2C, Pin +from time import sleep +from ism330dl import ISM330DL + +i2c = I2C(1) + +imu = ISM330DL(i2c, address=0x6B) + +print("ISM330DL orientation demo") +print() + + +# ------------------------------------------------- +# Main loop +# ------------------------------------------------- + +while True: + dir = imu.orientation() + + print("Orientation: {}".format(dir)) + + sleep(0.5) diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py index 8f5fbe59..502310a3 100644 --- a/lib/ism330dl/ism330dl/device.py +++ b/lib/ism330dl/ism330dl/device.py @@ -169,6 +169,48 @@ def temperature_c(self): return TEMP_OFFSET + raw / TEMP_SENSITIVITY + def orientation(self): + ax, ay, az = self.acceleration_g() + threshold = 0.75 # ~1 g when aligned with gravity + + if az > threshold: + return "SCREEN_DOWN" + if az < -threshold: + return "SCREEN_UP" + if ax > threshold: + return "TOP_EDGE_DOWN" + if ax < -threshold: + return "BOTTOM_EDGE_DOWN" + if ay > threshold: + return "RIGHT_EDGE_DOWN" + if ay < -threshold: + return "LEFT_EDGE_DOWN" + return "MOVING" + + def motion(self): + gx, gy, gz = self.gyroscope_dps() + threshold = 10 # minimum rotation speed in dps to be considered as motion + + if abs(gz) > abs(gx) and abs(gz) > abs(gy): # Z rotation (board turning left/right) + if gz > threshold: + return "TURNING RIGHT", gz + if gz < -threshold: + return "TURNING LEFT", abs(gz) + + if abs(gx) > abs(gy): # X rotation (console tilting left/right) + if gx > threshold: + return "TILTING LEFT", gx + if gx < -threshold: + return "TILTING RIGHT", abs(gx) + + else: # Y rotation (console tilting up/down) + if gy > threshold: + return "TILTING DOWN", gy + if gy < -threshold: + return "TILTING UP", abs(gy) + + return "STABLE", 0 # No significant motion detected + # -------------------------------------------------- # Status # -------------------------------------------------- From a2aaadd9bb6ea35330fb9630845be5a699f8297e Mon Sep 17 00:00:00 2001 From: Charly-sketch Date: Fri, 13 Mar 2026 10:50:26 +0100 Subject: [PATCH 03/11] ism330dl: Add test scenario. --- tests/scenarios/ism330dl.yaml | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/scenarios/ism330dl.yaml diff --git a/tests/scenarios/ism330dl.yaml b/tests/scenarios/ism330dl.yaml new file mode 100644 index 00000000..6543be03 --- /dev/null +++ b/tests/scenarios/ism330dl.yaml @@ -0,0 +1,90 @@ +driver: ism330dl +driver_class: ISM330DL +i2c_address: 0x6B + +i2c: + id: 1 + +mock_registers: + 0x0F: 0x6A + 0x1E: 0x07 + 0x20: [0x00, 0x00] + 0x22: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + 0x28: [0x00, 0x00, 0x00, 0x00, 0x00, 0x40] + +tests: + - name: "Verify WHO_AM_I register" + action: read_register + register: 0x0F + expect: 0x6A + mode: [mock, hardware] + + - name: "Read WHO_AM_I via method" + action: call + method: who_am_i + expect: 0x6A + mode: [mock, hardware] + + - name: "Status returns dictionary" + action: call + method: status + expect_not_none: true + mode: [mock, hardware] + + - name: "Acceleration returns tuple" + action: call + method: acceleration_g + expect_not_none: true + mode: [mock] + + - name: "Gyroscope returns tuple" + action: call + method: gyroscope_dps + expect_not_none: true + mode: [mock] + + - name: "Temperature returns plausible value" + action: call + method: temperature_c + expect_range: [24.0, 26.0] + mode: [mock] + + - name: "Orientation detection works" + action: call + method: orientation + expect: "SCREEN_DOWN" + mode: [mock] + + - name: "Acceleration magnitude plausible" + action: call + method: acceleration_g + expect_not_none: true + mode: [hardware] + + - name: "Gyroscope magnitude plausible" + action: call + method: gyroscope_dps + expect_not_none: true + mode: [hardware] + + - name: "Temperature in plausible range" + action: call + method: temperature_c + expect_range: [-20.0, 80.0] + mode: [hardware] + + - name: "Orientation feels correct" + action: manual + display: + - method: acceleration_g + label: "Acceleration (g)" + unit: "" + - method: gyroscope_dps + label: "Gyroscope (dps)" + unit: "" + - method: temperature_c + label: "Temperature" + unit: "°C" + prompt: "Les valeurs d'accélération, rotation et température sont-elles cohérentes ?" + expect_true: true + mode: [hardware] \ No newline at end of file From f335c038213eb7d354f869060c22216c13dd8262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:20:37 +0100 Subject: [PATCH 04/11] ism330dl: Apply project coding conventions to driver. --- lib/ism330dl/ism330dl/const.py | 113 ++++++++++++++++---------------- lib/ism330dl/ism330dl/device.py | 48 ++++---------- 2 files changed, 69 insertions(+), 92 deletions(-) diff --git a/lib/ism330dl/ism330dl/const.py b/lib/ism330dl/ism330dl/const.py index c12538ab..904f11b9 100644 --- a/lib/ism330dl/ism330dl/const.py +++ b/lib/ism330dl/ism330dl/const.py @@ -1,78 +1,77 @@ -# ISM330DL constants - +from micropython import const # --------------------------------------------------------------------- # I2C addresses / identity # --------------------------------------------------------------------- -ISM330DL_I2C_ADDR_LOW = 0x6A -ISM330DL_I2C_ADDR_HIGH = 0x6B -ISM330DL_WHO_AM_I_VALUE = 0x6A +ISM330DL_I2C_ADDR_LOW = const(0x6A) +ISM330DL_I2C_ADDR_HIGH = const(0x6B) +ISM330DL_WHO_AM_I_VALUE = const(0x6A) # --------------------------------------------------------------------- # Registers # --------------------------------------------------------------------- -REG_WHO_AM_I = 0x0F -REG_CTRL1_XL = 0x10 -REG_CTRL2_G = 0x11 -REG_CTRL3_C = 0x12 -REG_STATUS_REG = 0x1E +REG_WHO_AM_I = const(0x0F) +REG_CTRL1_XL = const(0x10) +REG_CTRL2_G = const(0x11) +REG_CTRL3_C = const(0x12) +REG_STATUS_REG = const(0x1E) -REG_OUT_TEMP_L = 0x20 -REG_OUT_TEMP_H = 0x21 +REG_OUT_TEMP_L = const(0x20) +REG_OUT_TEMP_H = const(0x21) -REG_OUTX_L_G = 0x22 -REG_OUTX_H_G = 0x23 -REG_OUTY_L_G = 0x24 -REG_OUTY_H_G = 0x25 -REG_OUTZ_L_G = 0x26 -REG_OUTZ_H_G = 0x27 +REG_OUTX_L_G = const(0x22) +REG_OUTX_H_G = const(0x23) +REG_OUTY_L_G = const(0x24) +REG_OUTY_H_G = const(0x25) +REG_OUTZ_L_G = const(0x26) +REG_OUTZ_H_G = const(0x27) -REG_OUTX_L_XL = 0x28 -REG_OUTX_H_XL = 0x29 -REG_OUTY_L_XL = 0x2A -REG_OUTY_H_XL = 0x2B -REG_OUTZ_L_XL = 0x2C -REG_OUTZ_H_XL = 0x2D +REG_OUTX_L_XL = const(0x28) +REG_OUTX_H_XL = const(0x29) +REG_OUTY_L_XL = const(0x2A) +REG_OUTY_H_XL = const(0x2B) +REG_OUTZ_L_XL = const(0x2C) +REG_OUTZ_H_XL = const(0x2D) # --------------------------------------------------------------------- # CTRL3_C bits # --------------------------------------------------------------------- -CTRL3_C_BDU = 1 << 6 -CTRL3_C_IF_INC = 1 << 2 -CTRL3_C_SW_RESET = 1 << 0 +CTRL3_C_BDU = const(1 << 6) +CTRL3_C_IF_INC = const(1 << 2) +CTRL3_C_SW_RESET = const(1 << 0) # --------------------------------------------------------------------- # STATUS bits # --------------------------------------------------------------------- -STATUS_TDA = 1 << 2 -STATUS_GDA = 1 << 1 -STATUS_XLDA = 1 << 0 +STATUS_TDA = const(1 << 2) +STATUS_GDA = const(1 << 1) +STATUS_XLDA = const(1 << 0) # --------------------------------------------------------------------- # Accelerometer ODR # --------------------------------------------------------------------- -ACCEL_ODR_POWER_DOWN = 0x00 -ACCEL_ODR_12_5HZ = 0x01 -ACCEL_ODR_26HZ = 0x02 -ACCEL_ODR_52HZ = 0x03 -ACCEL_ODR_104HZ = 0x04 -ACCEL_ODR_208HZ = 0x05 -ACCEL_ODR_416HZ = 0x06 -ACCEL_ODR_833HZ = 0x07 -ACCEL_ODR_1660HZ = 0x08 +ACCEL_ODR_POWER_DOWN = const(0x00) +ACCEL_ODR_12_5HZ = const(0x01) +ACCEL_ODR_26HZ = const(0x02) +ACCEL_ODR_52HZ = const(0x03) +ACCEL_ODR_104HZ = const(0x04) +ACCEL_ODR_208HZ = const(0x05) +ACCEL_ODR_416HZ = const(0x06) +ACCEL_ODR_833HZ = const(0x07) +ACCEL_ODR_1660HZ = const(0x08) # --------------------------------------------------------------------- # Accelerometer full scale # --------------------------------------------------------------------- -ACCEL_FS_2G = 2 -ACCEL_FS_4G = 4 -ACCEL_FS_8G = 8 -ACCEL_FS_16G = 16 +ACCEL_FS_2G = const(2) +ACCEL_FS_4G = const(4) +ACCEL_FS_8G = const(8) +ACCEL_FS_16G = const(16) ACCEL_FS_BITS = { ACCEL_FS_2G: 0x00, @@ -92,25 +91,25 @@ # --------------------------------------------------------------------- # Gyroscope ODR # --------------------------------------------------------------------- -GYRO_ODR_POWER_DOWN = 0x00 -GYRO_ODR_12_5HZ = 0x01 -GYRO_ODR_26HZ = 0x02 -GYRO_ODR_52HZ = 0x03 -GYRO_ODR_104HZ = 0x04 -GYRO_ODR_208HZ = 0x05 -GYRO_ODR_416HZ = 0x06 -GYRO_ODR_833HZ = 0x07 -GYRO_ODR_1660HZ = 0x08 +GYRO_ODR_POWER_DOWN = const(0x00) +GYRO_ODR_12_5HZ = const(0x01) +GYRO_ODR_26HZ = const(0x02) +GYRO_ODR_52HZ = const(0x03) +GYRO_ODR_104HZ = const(0x04) +GYRO_ODR_208HZ = const(0x05) +GYRO_ODR_416HZ = const(0x06) +GYRO_ODR_833HZ = const(0x07) +GYRO_ODR_1660HZ = const(0x08) # --------------------------------------------------------------------- # Gyroscope full scale # --------------------------------------------------------------------- -GYRO_FS_125DPS = 125 -GYRO_FS_250DPS = 250 -GYRO_FS_500DPS = 500 -GYRO_FS_1000DPS = 1000 -GYRO_FS_2000DPS = 2000 +GYRO_FS_125DPS = const(125) +GYRO_FS_250DPS = const(250) +GYRO_FS_500DPS = const(500) +GYRO_FS_1000DPS = const(1000) +GYRO_FS_2000DPS = const(2000) GYRO_FS_BITS = { GYRO_FS_250DPS: 0x00, diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py index 502310a3..29f993df 100644 --- a/lib/ism330dl/ism330dl/device.py +++ b/lib/ism330dl/ism330dl/device.py @@ -5,13 +5,14 @@ from ism330dl.exceptions import * -class ISM330DL: +class ISM330DL(object): """MicroPython driver for the ISM330DL 6-axis IMU.""" def __init__(self, i2c, address=None): self.i2c = i2c + self._buffer_1 = bytearray(1) - if address is None: # Auto-detect address. Je ne sais pas si c'est nécessaire pour la STeaMi, dans le manuel du capteur il y a 2 adresses possibles, 0x6A et 0x6B. + if address is None: for addr in (ISM330DL_I2C_ADDR_LOW, ISM330DL_I2C_ADDR_HIGH): try: if i2c.readfrom_mem(addr, REG_WHO_AM_I, 1)[0] == ISM330DL_WHO_AM_I_VALUE: @@ -37,10 +38,12 @@ def __init__(self, i2c, address=None): # -------------------------------------------------- def _read_u8(self, reg): - return self.i2c.readfrom_mem(self.address, reg, 1)[0] + self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1) + return self._buffer_1[0] def _write_u8(self, reg, value): - self.i2c.writeto_mem(self.address, reg, bytes([value])) + self._buffer_1[0] = value & 0xFF + self.i2c.writeto_mem(self.address, reg, self._buffer_1) def _read_bytes(self, reg, n): return self.i2c.readfrom_mem(self.address, reg, n) @@ -86,7 +89,6 @@ def check_device(self): def reset(self): self._write_u8(REG_CTRL3_C, CTRL3_C_SW_RESET) sleep_ms(50) - self._write_u8(REG_CTRL3_C, CTRL3_C_BDU | CTRL3_C_IF_INC) # -------------------------------------------------- @@ -94,28 +96,20 @@ def reset(self): # -------------------------------------------------- def configure_accel(self, odr, scale): - if scale not in ACCEL_FS_BITS: raise ISM330DLConfigError("Invalid accel scale") - self._accel_scale = scale - value = (odr << 4) | (ACCEL_FS_BITS[scale] << 2) - self._write_u8(REG_CTRL1_XL, value) def configure_gyro(self, odr, scale): - if scale == GYRO_FS_125DPS: value = (odr << 4) | 0x02 else: if scale not in GYRO_FS_BITS: raise ISM330DLConfigError("Invalid gyro scale") - value = (odr << 4) | (GYRO_FS_BITS[scale] << 2) - self._gyro_scale = scale - self._write_u8(REG_CTRL2_G, value) # -------------------------------------------------- @@ -136,42 +130,30 @@ def temperature_raw(self): # -------------------------------------------------- def acceleration_g(self): - sens = ACCEL_SENSITIVITY_MG[self._accel_scale] - raw = self.acceleration_raw() - return tuple((v * sens) / 1000.0 for v in raw) def acceleration_ms2(self): - g = self.acceleration_g() - return tuple(v * STANDARD_GRAVITY for v in g) def gyroscope_dps(self): - sens = GYRO_SENSITIVITY_MDPS[self._gyro_scale] - raw = self.gyroscope_raw() - return tuple((v * sens) / 1000.0 for v in raw) def gyroscope_rads(self): - dps = self.gyroscope_dps() - return tuple(v * pi / 180.0 for v in dps) def temperature_c(self): - raw = self.temperature_raw() - return TEMP_OFFSET + raw / TEMP_SENSITIVITY def orientation(self): ax, ay, az = self.acceleration_g() - threshold = 0.75 # ~1 g when aligned with gravity + threshold = 0.75 if az > threshold: return "SCREEN_DOWN" @@ -189,36 +171,33 @@ def orientation(self): def motion(self): gx, gy, gz = self.gyroscope_dps() - threshold = 10 # minimum rotation speed in dps to be considered as motion + threshold = 10 - if abs(gz) > abs(gx) and abs(gz) > abs(gy): # Z rotation (board turning left/right) + if abs(gz) > abs(gx) and abs(gz) > abs(gy): if gz > threshold: return "TURNING RIGHT", gz if gz < -threshold: return "TURNING LEFT", abs(gz) - if abs(gx) > abs(gy): # X rotation (console tilting left/right) + if abs(gx) > abs(gy): if gx > threshold: return "TILTING LEFT", gx if gx < -threshold: return "TILTING RIGHT", abs(gx) - - else: # Y rotation (console tilting up/down) + else: if gy > threshold: return "TILTING DOWN", gy if gy < -threshold: return "TILTING UP", abs(gy) - return "STABLE", 0 # No significant motion detected + return "STABLE", 0 # -------------------------------------------------- # Status # -------------------------------------------------- def status(self): - s = self._read_u8(REG_STATUS_REG) - return { "temp_ready": bool(s & STATUS_TDA), "gyro_ready": bool(s & STATUS_GDA), @@ -230,6 +209,5 @@ def status(self): # -------------------------------------------------- def power_down(self): - self._write_u8(REG_CTRL1_XL, 0) self._write_u8(REG_CTRL2_G, 0) From 180a4912ba4cedcbf691371586bb7e1633056012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:20:42 +0100 Subject: [PATCH 05/11] ism330dl: Fix examples and remove test script. --- lib/ism330dl/examples/basic_read.py | 4 +- lib/ism330dl/examples/motion_orientation.py | 4 +- lib/ism330dl/examples/static_orientation.py | 6 +- lib/ism330dl/examples/test.py | 387 -------------------- 4 files changed, 7 insertions(+), 394 deletions(-) delete mode 100644 lib/ism330dl/examples/test.py diff --git a/lib/ism330dl/examples/basic_read.py b/lib/ism330dl/examples/basic_read.py index 2ebc9ac9..74ebe87b 100644 --- a/lib/ism330dl/examples/basic_read.py +++ b/lib/ism330dl/examples/basic_read.py @@ -1,5 +1,5 @@ from machine import I2C -from time import sleep +from time import sleep_ms from ism330dl import ISM330DL i2c = I2C(1) @@ -25,4 +25,4 @@ ) ) - sleep(0.5) + sleep_ms(500) diff --git a/lib/ism330dl/examples/motion_orientation.py b/lib/ism330dl/examples/motion_orientation.py index 0a476c1f..e3cd6a86 100644 --- a/lib/ism330dl/examples/motion_orientation.py +++ b/lib/ism330dl/examples/motion_orientation.py @@ -1,5 +1,5 @@ from machine import I2C -from time import sleep +from time import sleep_ms from ism330dl import ISM330DL i2c = I2C(1) @@ -14,4 +14,4 @@ print("Motion: {} Speed: {:.1f} dps".format(motion, speed)) - sleep(0.2) + sleep_ms(200) diff --git a/lib/ism330dl/examples/static_orientation.py b/lib/ism330dl/examples/static_orientation.py index 00fc99bf..834bc028 100644 --- a/lib/ism330dl/examples/static_orientation.py +++ b/lib/ism330dl/examples/static_orientation.py @@ -1,5 +1,5 @@ -from machine import I2C, Pin -from time import sleep +from machine import I2C +from time import sleep_ms from ism330dl import ISM330DL i2c = I2C(1) @@ -19,4 +19,4 @@ print("Orientation: {}".format(dir)) - sleep(0.5) + sleep_ms(500) diff --git a/lib/ism330dl/examples/test.py b/lib/ism330dl/examples/test.py deleted file mode 100644 index bf8e9675..00000000 --- a/lib/ism330dl/examples/test.py +++ /dev/null @@ -1,387 +0,0 @@ -from machine import I2C, Pin -from time import sleep -from ism330dl import ISM330DL -from ism330dl.const import * - - -# --------------------------------------------------------------------- -# Update these pins and bus number to match your board -# --------------------------------------------------------------------- -TEMP_MIN = -40.0 -TEMP_MAX = 85.0 -ACCEL_MIN_G = 0.5 -ACCEL_MAX_G = 1.5 - - -def print_header(title): - print() - print("=" * 60) - print(title) - print("=" * 60) - - -def print_pass(name): - print("[PASS] {}".format(name)) - - -def print_fail(name, err=None): - if err is None: - print("[FAIL] {}".format(name)) - else: - print("[FAIL] {} -> {}".format(name, err)) - - -def dump_registers(sensor): - who_am_i = sensor._read_u8(REG_WHO_AM_I) - ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) - ctrl2_g = sensor._read_u8(REG_CTRL2_G) - ctrl3_c = sensor._read_u8(REG_CTRL3_C) - status = sensor._read_u8(REG_STATUS_REG) - - print("WHO_AM_I = 0x{:02X}".format(who_am_i)) - print("CTRL1_XL = 0x{:02X}".format(ctrl1_xl)) - print("CTRL2_G = 0x{:02X}".format(ctrl2_g)) - print("CTRL3_C = 0x{:02X}".format(ctrl3_c)) - print("STATUS_REG = 0x{:02X}".format(status)) - - -def test_i2c_scan(i2c): - print_header("1) I2C scan") - devices = i2c.scan() - print("I2C devices found:", [hex(x) for x in devices]) - - if ISM330DL_I2C_ADDR_LOW in devices or ISM330DL_I2C_ADDR_HIGH in devices: - print_pass("ISM330DL address found") - return True - else: - print_fail("ISM330DL address not found") - return False - - -def test_device_id(sensor): - print_header("2) Device ID") - dev_id = sensor.who_am_i() - print("WHO_AM_I:", hex(dev_id)) - - if dev_id == ISM330DL_WHO_AM_I_VALUE: - print_pass("WHO_AM_I matches 0x{:02X}".format(ISM330DL_WHO_AM_I_VALUE)) - return True - else: - print_fail("WHO_AM_I matches expected value", hex(dev_id)) - return False - - -def test_default_registers(sensor): - print_header("3) Default driver configuration") - dump_registers(sensor) - - ctrl3 = sensor._read_u8(REG_CTRL3_C) - - bdu_ok = bool(ctrl3 & CTRL3_C_BDU) - if_inc_ok = bool(ctrl3 & CTRL3_C_IF_INC) - - if bdu_ok: - print_pass("BDU enabled") - else: - print_fail("BDU enabled") - - if if_inc_ok: - print_pass("IF_INC enabled") - else: - print_fail("IF_INC enabled") - - return bdu_ok and if_inc_ok - - -def test_soft_reset(sensor): - print_header("4) Soft reset") - try: - sensor.reset() - sleep(0.05) - dump_registers(sensor) - - if sensor.who_am_i() == ISM330DL_WHO_AM_I_VALUE: - print_pass("Soft reset") - return True - else: - print_fail("Soft reset", "WHO_AM_I mismatch after reset") - return False - except Exception as err: - print_fail("Soft reset", err) - return False - - -def test_raw_readings(sensor): - print_header("5) Raw readings") - - try: - raw_accel = sensor.acceleration_raw() - raw_gyro = sensor.gyroscope_raw() - raw_temp = sensor.temperature_raw() - - print("Raw accel :", raw_accel) - print("Raw gyro :", raw_gyro) - print("Raw temp :", raw_temp) - - accel_ok = raw_accel != (0, 0, 0) - gyro_ok = raw_gyro != (0, 0, 0) - - if accel_ok: - print_pass("Accelerometer raw data is not all zero") - else: - print_fail("Accelerometer raw data is not all zero") - - if gyro_ok: - print_pass("Gyroscope raw data is not all zero") - else: - print_fail("Gyroscope raw data is not all zero") - - # temp can be close to zero raw around 25°C, so just reading it is enough - print_pass("Temperature raw read") - - return accel_ok and gyro_ok - - except Exception as err: - print_fail("Raw readings", err) - return False - - -def test_converted_readings(sensor): - print_header("6) Converted readings") - - try: - accel_g = sensor.acceleration_g() - accel_ms2 = sensor.acceleration_ms2() - gyro_dps = sensor.gyroscope_dps() - gyro_rads = sensor.gyroscope_rads() - temp_c = sensor.temperature_c() - - print("Acceleration [g] :", accel_g) - print("Acceleration [m/s²] :", accel_ms2) - print("Gyroscope [dps] :", gyro_dps) - print("Gyroscope [rad/s] :", gyro_rads) - print("Temperature [°C] : {:.2f}".format(temp_c)) - - temp_ok = TEMP_MIN <= temp_c <= TEMP_MAX - - # Basic accel sanity check: at rest one axis often sees ~1 g total norm nearby - ax, ay, az = accel_g - total_abs = abs(ax) + abs(ay) + abs(az) - accel_ok = ACCEL_MIN_G <= total_abs <= ACCEL_MAX_G - - if accel_ok: - print_pass("Acceleration is in a plausible range") - else: - print_fail("Acceleration is in a plausible range", total_abs) - - if temp_ok: - print_pass("Temperature is in a valid range") - else: - print_fail("Temperature is in a valid range", temp_c) - - return accel_ok and temp_ok - - except Exception as err: - print_fail("Converted readings", err) - return False - - -def test_configuration_change(sensor): - print_header("7) Configuration change") - - try: - sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_4G) - sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_500DPS) - sleep(0.1) - - ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) - ctrl2_g = sensor._read_u8(REG_CTRL2_G) - - print("CTRL1_XL = 0x{:02X}".format(ctrl1_xl)) - print("CTRL2_G = 0x{:02X}".format(ctrl2_g)) - print("Accel scale =", sensor._accel_scale, "g") - print("Gyro scale =", sensor._gyro_scale, "dps") - - accel_ok = sensor._accel_scale == ACCEL_FS_4G - gyro_ok = sensor._gyro_scale == GYRO_FS_500DPS - - if accel_ok: - print_pass("Accelerometer configuration updated") - else: - print_fail("Accelerometer configuration updated") - - if gyro_ok: - print_pass("Gyroscope configuration updated") - else: - print_fail("Gyroscope configuration updated") - - # Restore default config - sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) - sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) - - return accel_ok and gyro_ok - - except Exception as err: - print_fail("Configuration change", err) - return False - - -def test_status_flags(sensor): - print_header("8) STATUS helpers") - - try: - sleep(0.1) - - status = sensor.status() - - print("STATUS =", status) - - accel_ready = status["accel_ready"] - gyro_ready = status["gyro_ready"] - temp_ready = status["temp_ready"] - - print("accel_ready =", accel_ready) - print("gyro_ready =", gyro_ready) - print("temp_ready =", temp_ready) - - if accel_ready or gyro_ready or temp_ready: - print_pass("STATUS helper methods") - return True - else: - print_fail("STATUS helper methods") - return False - - except Exception as err: - print_fail("STATUS helper methods", err) - return False - - -def test_live_readings(sensor, samples=5): - print_header("9) Live readings") - - try: - previous = None - changed = False - - for i in range(samples): - accel = sensor.acceleration_g() - gyro = sensor.gyroscope_dps() - temp = sensor.temperature_c() - - print( - "#{:d} A=({:+.3f}, {:+.3f}, {:+.3f}) g " - "G=({:+.3f}, {:+.3f}, {:+.3f}) dps " - "T={:.2f} °C".format( - i + 1, - accel[0], accel[1], accel[2], - gyro[0], gyro[1], gyro[2], - temp, - ) - ) - - current = (accel, gyro, temp) - - if previous is not None and current != previous: - changed = True - - previous = current - sleep(0.5) - - if changed: - print_pass("Live readings change over time") - return True - else: - print_fail("Live readings change over time", "values stayed identical") - return False - - except Exception as err: - print_fail("Live readings", err) - return False - - -def test_power_down(sensor): - print_header("10) Power down") - - try: - sensor.power_down() - sleep(0.05) - - ctrl1_xl = sensor._read_u8(REG_CTRL1_XL) - ctrl2_g = sensor._read_u8(REG_CTRL2_G) - - print("CTRL1_XL after power_down = 0x{:02X}".format(ctrl1_xl)) - print("CTRL2_G after power_down = 0x{:02X}".format(ctrl2_g)) - - accel_ok = ctrl1_xl == 0x00 - gyro_ok = ctrl2_g == 0x00 - - if accel_ok: - print_pass("Accelerometer powered down") - else: - print_fail("Accelerometer powered down", hex(ctrl1_xl)) - - if gyro_ok: - print_pass("Gyroscope powered down") - else: - print_fail("Gyroscope powered down", hex(ctrl2_g)) - - return accel_ok and gyro_ok - - except Exception as err: - print_fail("Power down", err) - return False - - -def main(): - print_header("ISM330DL full driver test") - - i2c = I2C(1) # Update bus number if needed - - if not test_i2c_scan(i2c): - print() - print("Stop: sensor not found on I2C bus.") - return - - try: - sensor = ISM330DL(i2c) - print_pass("Driver init") - except Exception as err: - print_fail("Driver init", err) - return - - results = [] - - results.append(test_device_id(sensor)) - results.append(test_default_registers(sensor)) - results.append(test_soft_reset(sensor)) - - # Reconfigure after reset for the following tests - try: - sensor.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) - sensor.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) - sleep(0.1) - except Exception as err: - print_fail("Post-reset reconfiguration", err) - return - - results.append(test_raw_readings(sensor)) - results.append(test_converted_readings(sensor)) - results.append(test_configuration_change(sensor)) - results.append(test_status_flags(sensor)) - results.append(test_live_readings(sensor, samples=5)) - results.append(test_power_down(sensor)) - - print_header("Final result") - - passed = sum(1 for x in results if x) - total = len(results) - - print("Passed: {}/{}".format(passed, total)) - - if passed == total: - print("All tests passed.") - else: - print("Some tests failed.") - - -main() From bcbd63badac59b42dca3801613b8a3a12aae0008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:20:48 +0100 Subject: [PATCH 06/11] ism330dl: Fix README LLM artifact and incorrect method name. --- lib/ism330dl/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md index c4625734..0a4fb9e0 100644 --- a/lib/ism330dl/README.md +++ b/lib/ism330dl/README.md @@ -1,7 +1,3 @@ -Voici un **README propre et complet** pour ton driver **ISM330DL**, dans le style des drivers MicroPython (similaire à ce que tu as fait pour `wsen-pads` et `lis2mdl`). - ---- - # ISM330DL MicroPython Driver MicroPython driver for the **STMicroelectronics ISM330DL** 6-axis IMU. @@ -164,7 +160,7 @@ imu.gyroscope_rads() ### motion orientation ```python -imu.gyro_motion() +imu.motion() ``` --- From 80e6da61781536ed93945f9a14b812b2e2431226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:24:42 +0100 Subject: [PATCH 07/11] ism330dl: Improve test coverage for all driver features. --- tests/scenarios/ism330dl.yaml | 228 +++++++++++++++++++++++++++++----- 1 file changed, 199 insertions(+), 29 deletions(-) diff --git a/tests/scenarios/ism330dl.yaml b/tests/scenarios/ism330dl.yaml index 6543be03..819f7f46 100644 --- a/tests/scenarios/ism330dl.yaml +++ b/tests/scenarios/ism330dl.yaml @@ -5,14 +5,29 @@ i2c_address: 0x6B i2c: id: 1 +# Register values for mock tests +# Simulate an ISM330DL at rest, screen facing down mock_registers: + # WHO_AM_I 0x0F: 0x6A + # CTRL1_XL (written by configure_accel during init) + 0x10: 0x00 + # CTRL2_G (written by configure_gyro during init) + 0x11: 0x00 + # CTRL3_C (written by reset during init) + 0x12: 0x00 + # STATUS_REG (all data ready: TDA | GDA | XLDA) 0x1E: 0x07 + # OUT_TEMP_L/H (raw=0 => 25.0°C) 0x20: [0x00, 0x00] + # OUTX_L_G..OUTZ_H_G (gyro all zero => stable) 0x22: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + # OUTX_L_XL..OUTZ_H_XL (z=0x4000=16384 => ~1g downward) 0x28: [0x00, 0x00, 0x00, 0x00, 0x00, 0x40] tests: + # ----- Identity ----- + - name: "Verify WHO_AM_I register" action: read_register register: 0x0F @@ -25,55 +40,207 @@ tests: expect: 0x6A mode: [mock, hardware] + # ----- Status ----- + + - name: "Status returns all ready flags" + action: script + script: | + s = dev.status() + result = s["temp_ready"] and s["gyro_ready"] and s["accel_ready"] + expect_true: true + mode: [mock] + - name: "Status returns dictionary" action: call method: status expect_not_none: true - mode: [mock, hardware] + mode: [hardware] - - name: "Acceleration returns tuple" - action: call - method: acceleration_g - expect_not_none: true + # ----- Accelerometer ----- + + - name: "Acceleration raw returns tuple of 3 ints" + action: script + script: | + raw = dev.acceleration_raw() + result = len(raw) == 3 and all(isinstance(v, int) for v in raw) + expect_true: true mode: [mock] - - name: "Gyroscope returns tuple" - action: call - method: gyroscope_dps - expect_not_none: true + - name: "Acceleration g returns expected values" + action: script + script: | + ax, ay, az = dev.acceleration_g() + result = abs(ax) < 0.01 and abs(ay) < 0.01 and abs(az - 1.0) < 0.01 + expect_true: true mode: [mock] - - name: "Temperature returns plausible value" - action: call - method: temperature_c - expect_range: [24.0, 26.0] + - name: "Acceleration ms2 matches g times 9.80665" + action: script + script: | + g = dev.acceleration_g() + ms2 = dev.acceleration_ms2() + ok = all(abs(ms2[i] - g[i] * 9.80665) < 0.01 for i in range(3)) + result = ok + expect_true: true mode: [mock] - - name: "Orientation detection works" - action: call - method: orientation - expect: "SCREEN_DOWN" + - name: "Acceleration magnitude near 1g" + action: script + script: | + ax, ay, az = dev.acceleration_g() + mag = (ax**2 + ay**2 + az**2) ** 0.5 + result = 0.8 < mag < 1.2 + expect_true: true + mode: [hardware] + + # ----- Gyroscope ----- + + - name: "Gyroscope raw returns tuple of 3 ints" + action: script + script: | + raw = dev.gyroscope_raw() + result = len(raw) == 3 and all(isinstance(v, int) for v in raw) + expect_true: true mode: [mock] - - name: "Acceleration magnitude plausible" - action: call - method: acceleration_g - expect_not_none: true + - name: "Gyroscope dps returns zeros when static" + action: script + script: | + gx, gy, gz = dev.gyroscope_dps() + result = gx == 0.0 and gy == 0.0 and gz == 0.0 + expect_true: true + mode: [mock] + + - name: "Gyroscope rads consistent with dps" + action: script + script: | + from math import pi + dps = dev.gyroscope_dps() + rads = dev.gyroscope_rads() + ok = all(abs(rads[i] - dps[i] * pi / 180.0) < 0.001 for i in range(3)) + result = ok + expect_true: true + mode: [mock] + + - name: "Gyroscope values plausible at rest" + action: script + script: | + gx, gy, gz = dev.gyroscope_dps() + result = abs(gx) < 20 and abs(gy) < 20 and abs(gz) < 20 + expect_true: true mode: [hardware] - - name: "Gyroscope magnitude plausible" + # ----- Temperature ----- + + - name: "Temperature returns 25.0 from mock data" action: call - method: gyroscope_dps - expect_not_none: true - mode: [hardware] + method: temperature_c + expect_range: [24.9, 25.1] + mode: [mock] + + - name: "Temperature raw returns int" + action: script + script: | + result = isinstance(dev.temperature_raw(), int) + expect_true: true + mode: [mock] - name: "Temperature in plausible range" action: call method: temperature_c - expect_range: [-20.0, 80.0] + expect_range: [10.0, 50.0] mode: [hardware] - - name: "Orientation feels correct" + # ----- Orientation ----- + + - name: "Orientation detects SCREEN_DOWN" + action: call + method: orientation + expect: "SCREEN_DOWN" + mode: [mock] + + # ----- Motion ----- + + - name: "Motion returns STABLE when static" + action: script + script: | + direction, speed = dev.motion() + result = direction == "STABLE" and speed == 0 + expect_true: true + mode: [mock] + + # ----- Configuration ----- + + - name: "Configure accel changes scale" + action: script + script: | + from ism330dl.const import ACCEL_ODR_104HZ, ACCEL_FS_4G + dev.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_4G) + result = dev._accel_scale == ACCEL_FS_4G + expect_true: true + mode: [mock] + + - name: "Configure gyro changes scale" + action: script + script: | + from ism330dl.const import GYRO_ODR_104HZ, GYRO_FS_500DPS + dev.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_500DPS) + result = dev._gyro_scale == GYRO_FS_500DPS + expect_true: true + mode: [mock] + + - name: "Configure accel rejects invalid scale" + action: script + script: | + from ism330dl.const import ACCEL_ODR_104HZ + try: + dev.configure_accel(ACCEL_ODR_104HZ, 99) + result = False + except Exception: + result = True + expect_true: true + mode: [mock] + + - name: "Configure gyro rejects invalid scale" + action: script + script: | + from ism330dl.const import GYRO_ODR_104HZ + try: + dev.configure_gyro(GYRO_ODR_104HZ, 99) + result = False + except Exception: + result = True + expect_true: true + mode: [mock] + + # ----- Power ----- + + - name: "Power down writes zero to CTRL registers" + action: script + script: | + dev.power_down() + ctrl1 = dev._read_u8(0x10) + ctrl2 = dev._read_u8(0x11) + result = ctrl1 == 0 and ctrl2 == 0 + expect_true: true + mode: [mock] + + # ----- Reset ----- + + - name: "Reset sets BDU and IF_INC in CTRL3_C" + action: script + script: | + dev.reset() + ctrl3 = dev._read_u8(0x12) + bdu = bool(ctrl3 & (1 << 6)) + if_inc = bool(ctrl3 & (1 << 2)) + result = bdu and if_inc + expect_true: true + mode: [mock] + + # ----- Hardware manual ----- + + - name: "Sensor readings feel correct" action: manual display: - method: acceleration_g @@ -85,6 +252,9 @@ tests: - method: temperature_c label: "Temperature" unit: "°C" - prompt: "Les valeurs d'accélération, rotation et température sont-elles cohérentes ?" + - method: orientation + label: "Orientation" + unit: "" + prompt: "Les valeurs d'accélération, rotation, température et orientation sont-elles cohérentes ?" expect_true: true - mode: [hardware] \ No newline at end of file + mode: [hardware] From 61c736fcb9bf5f25a2891d4c59ca7bf6ed099154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:41:19 +0100 Subject: [PATCH 08/11] ism330dl: Use default I2C address 0x6A for STeaMi board. --- lib/ism330dl/README.md | 8 ++++---- lib/ism330dl/examples/basic_read.py | 2 +- lib/ism330dl/examples/motion_orientation.py | 2 +- lib/ism330dl/examples/static_orientation.py | 2 +- lib/ism330dl/ism330dl/const.py | 1 + lib/ism330dl/ism330dl/device.py | 17 ++--------------- tests/scenarios/ism330dl.yaml | 2 +- 7 files changed, 11 insertions(+), 23 deletions(-) diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md index 0a4fb9e0..5f60ff9d 100644 --- a/lib/ism330dl/README.md +++ b/lib/ism330dl/README.md @@ -14,7 +14,7 @@ This driver provides a simple API to configure the sensor and read motion data u # Features * I²C communication -* automatic device detection (`WHO_AM_I`) +* device identification (`WHO_AM_I`) * accelerometer configuration * gyroscope configuration * raw sensor readings @@ -46,19 +46,19 @@ The sensor can use two I²C addresses depending on the **SA0 pin**: | 0 | `0x6A` | | 1 | `0x6B` | -Most breakout boards use **0x6B**. +The STeaMi board uses **0x6A** (default). --- # Basic Usage ```python -from machine import I2C, Pin +from machine import I2C from ism330dl import ISM330DL i2c = I2C(1) -imu = ISM330DL(i2c, address=0x6B) +imu = ISM330DL(i2c) ax, ay, az = imu.acceleration_g() gx, gy, gz = imu.gyroscope_dps() diff --git a/lib/ism330dl/examples/basic_read.py b/lib/ism330dl/examples/basic_read.py index 74ebe87b..0e6a459a 100644 --- a/lib/ism330dl/examples/basic_read.py +++ b/lib/ism330dl/examples/basic_read.py @@ -4,7 +4,7 @@ i2c = I2C(1) -imu = ISM330DL(i2c, address=0x6B) +imu = ISM330DL(i2c) print("ISM330DL example: basic read") print() diff --git a/lib/ism330dl/examples/motion_orientation.py b/lib/ism330dl/examples/motion_orientation.py index e3cd6a86..d98a404c 100644 --- a/lib/ism330dl/examples/motion_orientation.py +++ b/lib/ism330dl/examples/motion_orientation.py @@ -4,7 +4,7 @@ i2c = I2C(1) -imu = ISM330DL(i2c, address=0x6B) +imu = ISM330DL(i2c) print("ISM330DL gyroscope direction demo") print() diff --git a/lib/ism330dl/examples/static_orientation.py b/lib/ism330dl/examples/static_orientation.py index 834bc028..2af04420 100644 --- a/lib/ism330dl/examples/static_orientation.py +++ b/lib/ism330dl/examples/static_orientation.py @@ -4,7 +4,7 @@ i2c = I2C(1) -imu = ISM330DL(i2c, address=0x6B) +imu = ISM330DL(i2c) print("ISM330DL orientation demo") print() diff --git a/lib/ism330dl/ism330dl/const.py b/lib/ism330dl/ism330dl/const.py index 904f11b9..545c34cf 100644 --- a/lib/ism330dl/ism330dl/const.py +++ b/lib/ism330dl/ism330dl/const.py @@ -5,6 +5,7 @@ # --------------------------------------------------------------------- ISM330DL_I2C_ADDR_LOW = const(0x6A) ISM330DL_I2C_ADDR_HIGH = const(0x6B) +ISM330DL_I2C_DEFAULT_ADDR = ISM330DL_I2C_ADDR_LOW ISM330DL_WHO_AM_I_VALUE = const(0x6A) diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py index 29f993df..e0bf8a6d 100644 --- a/lib/ism330dl/ism330dl/device.py +++ b/lib/ism330dl/ism330dl/device.py @@ -8,23 +8,10 @@ class ISM330DL(object): """MicroPython driver for the ISM330DL 6-axis IMU.""" - def __init__(self, i2c, address=None): + def __init__(self, i2c, address=ISM330DL_I2C_DEFAULT_ADDR): self.i2c = i2c - self._buffer_1 = bytearray(1) - - if address is None: - for addr in (ISM330DL_I2C_ADDR_LOW, ISM330DL_I2C_ADDR_HIGH): - try: - if i2c.readfrom_mem(addr, REG_WHO_AM_I, 1)[0] == ISM330DL_WHO_AM_I_VALUE: - address = addr - break - except OSError: - pass - - if address is None: - raise ISM330DLNotFound("ISM330DL not detected on I2C bus") - self.address = address + self._buffer_1 = bytearray(1) self._accel_scale = ACCEL_FS_2G self._gyro_scale = GYRO_FS_250DPS diff --git a/tests/scenarios/ism330dl.yaml b/tests/scenarios/ism330dl.yaml index 819f7f46..ee0a9aa6 100644 --- a/tests/scenarios/ism330dl.yaml +++ b/tests/scenarios/ism330dl.yaml @@ -1,6 +1,6 @@ driver: ism330dl driver_class: ISM330DL -i2c_address: 0x6B +i2c_address: 0x6A i2c: id: 1 From 5779f61b4271b72e63128285d9584fff4f56abe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:45:21 +0100 Subject: [PATCH 09/11] ism330dl: Add device check on init and wrap I2C errors. --- lib/ism330dl/README.md | 4 ++-- lib/ism330dl/ism330dl/device.py | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md index 5f60ff9d..232abbda 100644 --- a/lib/ism330dl/README.md +++ b/lib/ism330dl/README.md @@ -157,7 +157,7 @@ Example: imu.gyroscope_rads() ``` -### motion orientation +### Motion detection ```python imu.motion() @@ -247,7 +247,7 @@ The repository provides several example scripts: | Example | Description | | ----------------------- | ------------------------------------------------- | -| `basic_reader.py` | Simple sensor readout | +| `basic_read.py` | Simple sensor readout | | `static_orientation.py` | Detect device orientation using the accelerometer | | `motion_orientation.py` | Detect rotation using the gyroscope | diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py index e0bf8a6d..57f71e42 100644 --- a/lib/ism330dl/ism330dl/device.py +++ b/lib/ism330dl/ism330dl/device.py @@ -16,6 +16,7 @@ def __init__(self, i2c, address=ISM330DL_I2C_DEFAULT_ADDR): self._accel_scale = ACCEL_FS_2G self._gyro_scale = GYRO_FS_250DPS + self.check_device() self.reset() self.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_2G) self.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_250DPS) @@ -25,15 +26,24 @@ def __init__(self, i2c, address=ISM330DL_I2C_DEFAULT_ADDR): # -------------------------------------------------- def _read_u8(self, reg): - self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1) + try: + self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1) + except OSError as exc: + raise ISM330DLIOError("Read register 0x{:02X}".format(reg)) from exc return self._buffer_1[0] def _write_u8(self, reg, value): - self._buffer_1[0] = value & 0xFF - self.i2c.writeto_mem(self.address, reg, self._buffer_1) + try: + self._buffer_1[0] = value & 0xFF + self.i2c.writeto_mem(self.address, reg, self._buffer_1) + except OSError as exc: + raise ISM330DLIOError("Write register 0x{:02X}".format(reg)) from exc def _read_bytes(self, reg, n): - return self.i2c.readfrom_mem(self.address, reg, n) + try: + return self.i2c.readfrom_mem(self.address, reg, n) + except OSError as exc: + raise ISM330DLIOError("Read {} bytes from 0x{:02X}".format(n, reg)) from exc def _read_i16(self, reg): data = self._read_bytes(reg, 2) From 7aa7f2fb5009223fb190e655aa36454a956bc392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 15:57:29 +0100 Subject: [PATCH 10/11] ism330dl: Fix default I2C address to 0x6B matching STeaMi board. --- lib/ism330dl/README.md | 2 +- lib/ism330dl/ism330dl/const.py | 2 +- tests/scenarios/ism330dl.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ism330dl/README.md b/lib/ism330dl/README.md index 232abbda..d275e36b 100644 --- a/lib/ism330dl/README.md +++ b/lib/ism330dl/README.md @@ -46,7 +46,7 @@ The sensor can use two I²C addresses depending on the **SA0 pin**: | 0 | `0x6A` | | 1 | `0x6B` | -The STeaMi board uses **0x6A** (default). +The STeaMi board uses **0x6B** (default). --- diff --git a/lib/ism330dl/ism330dl/const.py b/lib/ism330dl/ism330dl/const.py index 545c34cf..517e97e2 100644 --- a/lib/ism330dl/ism330dl/const.py +++ b/lib/ism330dl/ism330dl/const.py @@ -5,7 +5,7 @@ # --------------------------------------------------------------------- ISM330DL_I2C_ADDR_LOW = const(0x6A) ISM330DL_I2C_ADDR_HIGH = const(0x6B) -ISM330DL_I2C_DEFAULT_ADDR = ISM330DL_I2C_ADDR_LOW +ISM330DL_I2C_DEFAULT_ADDR = ISM330DL_I2C_ADDR_HIGH ISM330DL_WHO_AM_I_VALUE = const(0x6A) diff --git a/tests/scenarios/ism330dl.yaml b/tests/scenarios/ism330dl.yaml index ee0a9aa6..819f7f46 100644 --- a/tests/scenarios/ism330dl.yaml +++ b/tests/scenarios/ism330dl.yaml @@ -1,6 +1,6 @@ driver: ism330dl driver_class: ISM330DL -i2c_address: 0x6A +i2c_address: 0x6B i2c: id: 1 From 408be947eb5637f699fb87dfae03e8e06aecb11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sat, 14 Mar 2026 16:07:24 +0100 Subject: [PATCH 11/11] ism330dl: Address Copilot review on PR #50. --- lib/ism330dl/ism330dl/const.py | 4 +++ lib/ism330dl/ism330dl/device.py | 4 +++ tests/scenarios/ism330dl.yaml | 54 ++++++++++++++++++++++++++------- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/lib/ism330dl/ism330dl/const.py b/lib/ism330dl/ism330dl/const.py index 517e97e2..c13d8cc2 100644 --- a/lib/ism330dl/ism330dl/const.py +++ b/lib/ism330dl/ism330dl/const.py @@ -65,6 +65,8 @@ ACCEL_ODR_833HZ = const(0x07) ACCEL_ODR_1660HZ = const(0x08) +ACCEL_ODR_VALUES = (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) + # --------------------------------------------------------------------- # Accelerometer full scale @@ -102,6 +104,8 @@ GYRO_ODR_833HZ = const(0x07) GYRO_ODR_1660HZ = const(0x08) +GYRO_ODR_VALUES = (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) + # --------------------------------------------------------------------- # Gyroscope full scale diff --git a/lib/ism330dl/ism330dl/device.py b/lib/ism330dl/ism330dl/device.py index 57f71e42..efc432c2 100644 --- a/lib/ism330dl/ism330dl/device.py +++ b/lib/ism330dl/ism330dl/device.py @@ -93,6 +93,8 @@ def reset(self): # -------------------------------------------------- def configure_accel(self, odr, scale): + if odr not in ACCEL_ODR_VALUES: + raise ISM330DLConfigError("Invalid accel ODR") if scale not in ACCEL_FS_BITS: raise ISM330DLConfigError("Invalid accel scale") self._accel_scale = scale @@ -100,6 +102,8 @@ def configure_accel(self, odr, scale): self._write_u8(REG_CTRL1_XL, value) def configure_gyro(self, odr, scale): + if odr not in GYRO_ODR_VALUES: + raise ISM330DLConfigError("Invalid gyro ODR") if scale == GYRO_FS_125DPS: value = (odr << 4) | 0x02 else: diff --git a/tests/scenarios/ism330dl.yaml b/tests/scenarios/ism330dl.yaml index 819f7f46..4a2a08c7 100644 --- a/tests/scenarios/ism330dl.yaml +++ b/tests/scenarios/ism330dl.yaml @@ -171,21 +171,27 @@ tests: # ----- Configuration ----- - - name: "Configure accel changes scale" + - name: "Configure accel writes correct register value" action: script script: | from ism330dl.const import ACCEL_ODR_104HZ, ACCEL_FS_4G + i2c.clear_write_log() dev.configure_accel(ACCEL_ODR_104HZ, ACCEL_FS_4G) - result = dev._accel_scale == ACCEL_FS_4G + log = i2c.get_write_log() + reg, data = log[-1] + result = reg == 0x10 and data[0] == (0x04 << 4) | (0x02 << 2) expect_true: true mode: [mock] - - name: "Configure gyro changes scale" + - name: "Configure gyro writes correct register value" action: script script: | from ism330dl.const import GYRO_ODR_104HZ, GYRO_FS_500DPS + i2c.clear_write_log() dev.configure_gyro(GYRO_ODR_104HZ, GYRO_FS_500DPS) - result = dev._gyro_scale == GYRO_FS_500DPS + log = i2c.get_write_log() + reg, data = log[-1] + result = reg == 0x11 and data[0] == (0x04 << 4) | (0x01 << 2) expect_true: true mode: [mock] @@ -201,6 +207,18 @@ tests: expect_true: true mode: [mock] + - name: "Configure accel rejects invalid ODR" + action: script + script: | + from ism330dl.const import ACCEL_FS_2G + try: + dev.configure_accel(99, ACCEL_FS_2G) + result = False + except Exception: + result = True + expect_true: true + mode: [mock] + - name: "Configure gyro rejects invalid scale" action: script script: | @@ -213,15 +231,28 @@ tests: expect_true: true mode: [mock] + - name: "Configure gyro rejects invalid ODR" + action: script + script: | + from ism330dl.const import GYRO_FS_250DPS + try: + dev.configure_gyro(99, GYRO_FS_250DPS) + result = False + except Exception: + result = True + expect_true: true + mode: [mock] + # ----- Power ----- - name: "Power down writes zero to CTRL registers" action: script script: | + i2c.clear_write_log() dev.power_down() - ctrl1 = dev._read_u8(0x10) - ctrl2 = dev._read_u8(0x11) - result = ctrl1 == 0 and ctrl2 == 0 + log = i2c.get_write_log() + writes = {reg: data[0] for reg, data in log} + result = writes.get(0x10) == 0 and writes.get(0x11) == 0 expect_true: true mode: [mock] @@ -230,11 +261,12 @@ tests: - name: "Reset sets BDU and IF_INC in CTRL3_C" action: script script: | + i2c.clear_write_log() dev.reset() - ctrl3 = dev._read_u8(0x12) - bdu = bool(ctrl3 & (1 << 6)) - if_inc = bool(ctrl3 & (1 << 2)) - result = bdu and if_inc + log = i2c.get_write_log() + ctrl3_writes = [data[0] for reg, data in log if reg == 0x12] + bdu_if_inc = (1 << 6) | (1 << 2) + result = len(ctrl3_writes) == 2 and ctrl3_writes[1] == bdu_if_inc expect_true: true mode: [mock]