Source code for adafruit_lsm303_accel

# SPDX-FileCopyrightText: 2019 Bryan Siepert for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_lsm303_accel`
====================================================


CircuitPython driver for the accelerometer in LSM303 sensors.

* Author(s): Dave Astels, Bryan Siepert

Implementation Notes
--------------------

**Hardware:**

* Adafruit `Triple-axis Accelerometer+Magnetometer (Compass) Board - LSM303
  <https://www.adafruit.com/product/1120>`_ (Product ID: 1120)
* Adafruit `FLORA Accelerometer/Compass Sensor - LSM303 - v1.0
  <https://www.adafruit.com/product/1247>`_ (Product ID: 1247)

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://circuitpython.org/downloads
* Adafruit's Bus Device library:
  https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""

import struct

from micropython import const
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register.i2c_struct import UnaryStruct
from adafruit_register.i2c_bit import RWBit, ROBit
from adafruit_register.i2c_bits import RWBits
from adafruit_register.i2c_struct_array import StructArray

try:
    from typing import Optional, Tuple
    from typing_extensions import Literal
    from busio import I2C
except ImportError:
    pass

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LSM303_Accel.git"

_ADDRESS_ACCEL = const(0x19)  # (0x32 >> 1)       // 0011001x
_ADDRESS_MAG = const(0x1E)  # (0x3C >> 1)       // 0011110x
_ID = const(0xD4)  # (0b11010100)

# Accelerometer registers

_REG_ACCEL_WHO_AM_I = const(0x0F)
_REG_ACCEL_CTRL_REG1_A = const(0x20)
_REG_ACCEL_CTRL_REG2_A = const(0x21)
_REG_ACCEL_CTRL_REG3_A = const(0x22)
_REG_ACCEL_CTRL_REG4_A = const(0x23)
_REG_ACCEL_CTRL_REG5_A = const(0x24)
_REG_ACCEL_CTRL_REG6_A = const(0x25)
_REG_ACCEL_REFERENCE_A = const(0x26)
_REG_ACCEL_STATUS_REG_A = const(0x27)
_REG_ACCEL_OUT_X_L_A = const(0x28)
_REG_ACCEL_OUT_X_H_A = const(0x29)
_REG_ACCEL_OUT_Y_L_A = const(0x2A)
_REG_ACCEL_OUT_Y_H_A = const(0x2B)
_REG_ACCEL_OUT_Z_L_A = const(0x2C)
_REG_ACCEL_OUT_Z_H_A = const(0x2D)
_REG_ACCEL_FIFO_CTRL_REG_A = const(0x2E)
_REG_ACCEL_FIFO_SRC_REG_A = const(0x2F)
_REG_ACCEL_INT1_CFG_A = const(0x30)
_REG_ACCEL_INT1_SOURCE_A = const(0x31)
_REG_ACCEL_INT1_THS_A = const(0x32)
_REG_ACCEL_INT1_DURATION_A = const(0x33)
_REG_ACCEL_INT2_CFG_A = const(0x34)
_REG_ACCEL_INT2_SOURCE_A = const(0x35)
_REG_ACCEL_INT2_THS_A = const(0x36)
_REG_ACCEL_INT2_DURATION_A = const(0x37)
_REG_ACCEL_CLICK_CFG_A = const(0x38)
_REG_ACCEL_CLICK_SRC_A = const(0x39)
_REG_ACCEL_CLICK_THS_A = const(0x3A)
_REG_ACCEL_TIME_LIMIT_A = const(0x3B)
_REG_ACCEL_TIME_LATENCY_A = const(0x3C)
_REG_ACCEL_TIME_WINDOW_A = const(0x3D)
_REG_ACCEL_ACT_THS_A = const(0x3E)
_REG_ACCEL_ACT_DUR_A = const(0x3F)
# note:: Tap related registers are called ``CLICK_`` in the datasheet
# Conversion constants
_LSM303ACCEL_MG_LSB = 16704.0  # magic!
_GRAVITY_STANDARD = 9.80665  # Earth's gravity in m/s^2
_SMOLLER_GRAVITY = 0.00980665


# pylint: disable=too-few-public-methods
[docs] class Rate: """Options for `data_rate`""" RATE_SHUTDOWN = const(0) RATE_1_HZ = const(1) RATE_10_HZ = const(2) RATE_25_HZ = const(3) RATE_50_HZ = const(4) RATE_100_HZ = const(5) RATE_200_HZ = const(6) RATE_400_HZ = const(7) RATE_1620_HZ = const(8) RATE_1344_HZ = const(9)
[docs] class Mode: """Options for `mode`""" MODE_NORMAL = const(0) MODE_HIGH_RESOLUTION = const(1) MODE_LOW_POWER = const(2)
[docs] class Range: """Options for `range`""" RANGE_2G = const(0) RANGE_4G = const(1) RANGE_8G = const(2) RANGE_16G = const(3)
# pylint: enable=too-few-public-methods
[docs] class LSM303_Accel: # pylint:disable=too-many-instance-attributes """Driver for the LSM303's accelerometer. :param ~busio.I2C i2c: The I2C bus the device is connected to. **Quickstart: Importing and using the device** Here is an example of using the :class:`LSM303_Accel` class. First you will need to import the libraries to use the sensor .. code-block:: python import board import adafruit_lsm303_accel Once this is done you can define your `board.I2C` object and define your sensor object .. code-block:: python i2c = board.I2C() # uses board.SCL and board.SDA sensor = adafruit_lsm303_accel.LSM303_Accel(i2c) Now you have access to the :attr:`acceleration` attribute .. code-block:: python acc_x, acc_y, acc_z = sensor.acceleration """ # Class-level buffer for reading and writing data with the sensor. # This reduces memory allocations but means the code is not re-entrant or # thread safe! _chip_id = UnaryStruct(_REG_ACCEL_WHO_AM_I, "B") _int2_int1_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 6) _int2_int2_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 5) _int1_latching = RWBit(_REG_ACCEL_CTRL_REG5_A, 3) _int2_latching = RWBit(_REG_ACCEL_CTRL_REG5_A, 1) _bdu = RWBit(_REG_ACCEL_CTRL_REG4_A, 7) _int2_activity_enable = RWBit(_REG_ACCEL_CTRL_REG6_A, 3) _int_pin_active_low = RWBit(_REG_ACCEL_CTRL_REG6_A, 1) _act_threshold = UnaryStruct(_REG_ACCEL_ACT_THS_A, "B") _act_duration = UnaryStruct(_REG_ACCEL_ACT_DUR_A, "B") _data_rate = RWBits(4, _REG_ACCEL_CTRL_REG1_A, 4) _enable_xyz = RWBits(3, _REG_ACCEL_CTRL_REG1_A, 0) _raw_accel_data = StructArray(_REG_ACCEL_OUT_X_L_A, "<h", 3) _low_power = RWBit(_REG_ACCEL_CTRL_REG1_A, 3) _high_resolution = RWBit(_REG_ACCEL_CTRL_REG4_A, 3) _range = RWBits(2, _REG_ACCEL_CTRL_REG4_A, 4) _int1_src = UnaryStruct(_REG_ACCEL_INT1_SOURCE_A, "B") _tap_src = UnaryStruct(_REG_ACCEL_CLICK_SRC_A, "B") _tap_interrupt_enable = RWBit(_REG_ACCEL_CTRL_REG3_A, 7, 1) _tap_config = UnaryStruct(_REG_ACCEL_CLICK_CFG_A, "B") _tap_interrupt_active = ROBit(_REG_ACCEL_CLICK_SRC_A, 6, 1) _tap_threshold = UnaryStruct(_REG_ACCEL_CLICK_THS_A, "B") _tap_time_limit = UnaryStruct(_REG_ACCEL_TIME_LIMIT_A, "B") _tap_time_latency = UnaryStruct(_REG_ACCEL_TIME_LATENCY_A, "B") _tap_time_window = UnaryStruct(_REG_ACCEL_TIME_WINDOW_A, "B") _BUFFER = bytearray(6) def __init__(self, i2c: I2C) -> None: self._accel_device = I2CDevice(i2c, _ADDRESS_ACCEL) self.i2c_device = self._accel_device self._data_rate = 2 self._enable_xyz = 0b111 self._int1_latching = True self._int2_latching = True self._bdu = True # self._write_register_byte(_REG_CTRL5, 0x80) # time.sleep(0.01) # takes 5ms self._cached_mode = 0 self._cached_range = 0
[docs] def set_tap( self, tap: Literal[0, 1, 2], threshold: int, *, time_limit: int = 10, time_latency: int = 20, time_window: int = 255, tap_cfg: Optional[int] = None, ) -> None: """ The tap detection parameters. :param int tap: 0 to disable tap detection, 1 to detect only single taps, and 2 to detect \ only double taps. :param int threshold: A threshold for the tap detection. The higher the value the less\ sensitive the detection. This changes based on the accelerometer range. Good values\ are 5-10 for 16G, 10-20 for 8G, 20-40 for 4G, and 40-80 for 2G. :param int time_limit: TIME_LIMIT register value. Defaults to :const:`10` :param int time_latency: TIME_LATENCY register value. Defaults to :const:`20` :param int time_window: TIME_WINDOW register value. Defaults to :const:`255` :param int tap_cfg: CLICK_CFG register value. Defaults to `None` """ if (tap < 0 or tap > 2) and tap_cfg is None: raise ValueError( "Tap must be 0 (disabled), 1 (single tap), or 2 (double tap)!" ) if threshold > 127 or threshold < 0: raise ValueError("Threshold out of range (0-127)") if tap == 0 and tap_cfg is None: # Disable click interrupt. self._tap_interrupt_enable = False self._tap_config = 0 return self._tap_interrupt_enable = True if tap_cfg is None: if tap == 1: tap_cfg = 0x15 # Turn on all axes & singletap. if tap == 2: tap_cfg = 0x2A # Turn on all axes & doubletap. # Or, if a custom tap configuration register value specified, use it. self._tap_config = tap_cfg self._tap_threshold = threshold # why and? self._tap_time_limit = time_limit self._tap_time_latency = time_latency self._tap_time_window = time_window
@property def tapped(self) -> bool: """ True if a tap was detected recently. Whether its a single tap or double tap is determined by the tap param on :meth:`set_tap`. :attr:`tapped` may be True over multiple reads even if only a single tap or single double tap occurred. """ tap_src = self._tap_src return tap_src & 0b1000000 > 0 @property def _raw_acceleration(self) -> Tuple[int, int, int]: self._read_bytes( self._accel_device, _REG_ACCEL_OUT_X_L_A | 0x80, 6, self._BUFFER ) return struct.unpack_from("<hhh", self._BUFFER[0:6]) @property def acceleration(self) -> Tuple[float, float, float]: """The measured accelerometer sensor values. A 3-tuple of X, Y, Z axis values in m/s^2 squared that are signed floats. """ raw_accel_data = self._raw_acceleration x = self._scale_data(raw_accel_data[0]) y = self._scale_data(raw_accel_data[1]) z = self._scale_data(raw_accel_data[2]) return (x, y, z) def _scale_data(self, raw_measurement: int) -> float: lsb, shift = self._lsb_shift() return (raw_measurement >> shift) * lsb * _SMOLLER_GRAVITY def _lsb_shift(self) -> Tuple[float, int]: # pylint:disable=too-many-branches # the bit depth of the data depends on the mode, and the lsb value # depends on the mode and range lsb = -1 # the default, normal mode @ 2G if self._cached_mode is Mode.MODE_HIGH_RESOLUTION: # 12-bit shift = 4 if self._cached_range is Range.RANGE_2G: lsb = 0.98 elif self._cached_range is Range.RANGE_4G: lsb = 1.95 elif self._cached_range is Range.RANGE_8G: lsb = 3.9 elif self._cached_range is Range.RANGE_16G: lsb = 11.72 elif self._cached_mode is Mode.MODE_NORMAL: # 10-bit shift = 6 if self._cached_range is Range.RANGE_2G: lsb = 3.9 elif self._cached_range is Range.RANGE_4G: lsb = 7.82 elif self._cached_range is Range.RANGE_8G: lsb = 15.63 elif self._cached_range is Range.RANGE_16G: lsb = 46.9 elif self._cached_mode is Mode.MODE_LOW_POWER: # 8-bit shift = 8 if self._cached_range is Range.RANGE_2G: lsb = 15.63 elif self._cached_range is Range.RANGE_4G: lsb = 31.26 elif self._cached_range is Range.RANGE_8G: lsb = 62.52 elif self._cached_range is Range.RANGE_16G: lsb = 187.58 if lsb is -1: raise AttributeError( "'impossible' range or mode detected: " f"range: {self._cached_range} mode: {self._cached_mode}" ) return (lsb, shift) @property def data_rate(self) -> int: """Select the rate at which the sensor takes measurements. Must be a `Rate`""" return self._data_rate @data_rate.setter def data_rate(self, value: int) -> None: if value < 0 or value > 9: raise AttributeError("data_rate must be a `Rate`") self._data_rate = value @property def range(self) -> int: """Adjusts the range of values that the sensor can measure, from +- 2G to +-16G Note that larger ranges will be less accurate. Must be a `Range`""" return self._cached_range @range.setter def range(self, value: int) -> None: if value < 0 or value > 3: raise AttributeError("range must be a `Range`") self._range = value self._cached_range = value @property def mode(self) -> int: """Sets the power mode of the sensor. The mode must be a `Mode`. Note that the mode and range will both affect the accuracy of the sensor""" return self._cached_mode @mode.setter def mode(self, value: int) -> None: if value < 0 or value > 2: raise AttributeError("mode must be a `Mode`") self._high_resolution = value & 0b01 self._low_power = (value & 0b10) >> 1 self._cached_mode = value def _read_u8(self, device: I2CDevice, address: int) -> int: with device as i2c: self._BUFFER[0] = address & 0xFF i2c.write_then_readinto(self._BUFFER, self._BUFFER, out_end=1, in_end=1) return self._BUFFER[0] def _write_u8(self, device: I2CDevice, address: int, val: int) -> None: with device as i2c: self._BUFFER[0] = address & 0xFF self._BUFFER[1] = val & 0xFF i2c.write(self._BUFFER, end=2) @staticmethod def _read_bytes( device: I2CDevice, address: int, count: int, buf: bytearray ) -> None: with device as i2c: buf[0] = address & 0xFF i2c.write_then_readinto(buf, buf, out_end=1, in_end=count)