"""LIS2HH Driver - i2c Accelerometer
This module provides high-level functions to access the basic accelerometer features,
and low-level functions to allow access to the full register set for advanced usage.
Many advanced features are available - see `datasheet <https://www.st.com/resource/en/datasheet/lis2hh12.pdf>`_
"""
import machine
import time
# Device registers
LIS_TEMP_L = 0x0b
LIS_TEMP_L = 0x0c
LIS_DEV_ID = 0x0f # hardcoded WHO_AM_I ID
LIS_ACT_THS = 0x1e
LIS_ACT_DUR = 0x1f
LIS_CTRL_BASE = 0x1f
LIS_CTRL_REG1 = 0x20
LIS_CTRL_REG2 = 0x21
LIS_CTRL_REG3 = 0x22
LIS_CTRL_REG4 = 0x23
LIS_CTRL_REG5 = 0x24
LIS_CTRL_REG6 = 0x25
LIS_CTRL_REG7 = 0x26
LIS_STATUS_REG = 0x27
LIS_OUT_REG_MULTI = 0x28 # 3 axes XYZ, each 16bit 2's complement
LIS_FIFO_CTRL = 0x2e
LIS_FIFO_SRC = 0x2f
LIS_IG1_CFG = 0x30 # Interrupt Generator 1
LIS_IG1_SRC = 0x31
LIS_IG1_THRESH = 0x32 # IG1 thresholds
LIS_IG1_THRESH_X = 0x32
LIS_IG1_THRESH_Y = 0x33
LIS_IG1_THRESH_Z = 0x34
LIS_IG1_DURATION = 0x35 # number of ODR cycles (e.g. 1.25ms per LSB for 800Hz ODR)
LIS_IG2_CFG = 0x36 # Interrupt Generator 2
LIS_IG2_SRC = 0x37
LIS_IG2_THRESH = 0x38 # One threshold for all 3 axes
LIS_IG2_DURATION = 0x39
LIS_VFY_DEV_ID = 0x41
# Default values
LIS_DEF_CTRL1 = 0xef # ODR=800Hz, High-Rez mode, XYZ axes enabled (180uA fangs-out mode)
LIS_DEF_CTRL2 = 0x22 # High pass filter enabled (8Hz cutoff) interrupt on IG1 (data reg not filtered)
LIS_DEF_CTRL3 = 0x08 # IG1 interrupt on INT1 pad
LIS_DEF_CTRL4 = 0x04 # +/- 2G full-scale range, address auto-increment
LIS_DEF_CTRL5 = 0x00 # No decimation, INT active high PP
LIS_DEF_CTRL6 = 0x00 # Nothing on INT2 pad
LIS_DEF_CTRL7 = 0x00 # Non latching IRQ
[docs]class LIS2HH:
"""Device driver for LIS2HH accelerometer, controlled via i2c bus."""
def __init__(self, addr=0x1e, int1_pin=machine.Pin("PB8", machine.Pin.IN)):
self.addr = addr
self.int1_pin = int1_pin
self.i2c = machine.I2C(1, freq=400000) # Hardware I2C on port 1. Use 400kHz I2C freq.
self.orientation = (0,0,0)
self.buf1 = bytearray(1) # Pre-allocate command/resp buffer usable from IRQ
self.buf2 = bytearray(2)
self.reset()
[docs] def reset(self):
"""Init registers to default config"""
self.ctrl(1, LIS_DEF_CTRL1)
self.ctrl(2, LIS_DEF_CTRL2)
self.ctrl(3, LIS_DEF_CTRL3)
self.ctrl(4, LIS_DEF_CTRL4)
self.ctrl(5, LIS_DEF_CTRL5)
self.ctrl(6, LIS_DEF_CTRL6)
self.ctrl(7, LIS_DEF_CTRL7)
[docs] def ctrl(self, reg, cmd):
"""Set control registers"""
self.buf2[0] = LIS_CTRL_BASE + reg
self.buf2[1] = cmd
self.i2c.writeto(self.addr, self.buf2)
[docs] def set_byte(self, reg, val):
"""Set byte register"""
self.buf2[0] = reg
self.buf2[1] = val
self.i2c.writeto(self.addr, self.buf2)
def get_byte(self, reg):
self.buf1[0] = reg
self.i2c.writeto(self.addr, self.buf1)
self.i2c.readfrom_into(self.addr, self.buf1)
return self.buf1[0]
[docs] def verify_id(self):
"""Verify device ID - return True if correct"""
return self.get_byte(LIS_DEV_ID) == LIS_VFY_DEV_ID
[docs] @staticmethod
def test_exceeds(a, b, delta):
"""Test each iterable value: a[0..n] - b[0..n] > delta"""
for i,j in zip(a,b):
if i - j <= delta:
return False
return True
[docs] def self_test(self):
"""Run self-test and return device to default state. Return True if passed"""
result = False
normal = self.read()
self.ctrl(5, 0x04) # Induce positive force
time.sleep_ms(200)
pos_test = self.read()
if self.test_exceeds(normal, pos_test, 1000):
self.ctrl(5, 0x08) # Induce negative force
time.sleep_ms(200)
neg_test = self.read()
if self.test_exceeds(neg_test, normal, 1000):
result = True
self.ctrl(5, LIS_DEF_CTRL5)
return result
@staticmethod
def signed16(buf):
val = (buf[1] << 8) | buf[0] # Little endian 16-bit 2's complement value
return (val & 0x8000) - (val & 0x7fff) # Convert to signed integer
[docs] def read(self):
"""Read current XYZ axis values and update 'self.orientation' tuple
Returns:
`tuple` of (x, y, z) axis values.
- Default full-scale range is ±2g.
- Values are signed 16-bit integer (-32767 to +32768)
"""
self.i2c.writeto(self.addr, bytearray((LIS_OUT_REG_MULTI,)))
s = self.i2c.readfrom(self.addr, 6)
x = self.signed16(s[0:2])
y = self.signed16(s[2:4])
z = self.signed16(s[4:6])
self.orientation = (x,y,z)
return self.orientation
[docs] def wake(self):
"""Recover from sleep"""
self.ctrl(1, LIS_CTRL_VAL1)
[docs] def sleep(self):
"""Shutdown operation - draws about 5uA"""
self.ctrl(1, 0x00) # ODR=0 is power down
[docs] def dump_axes(self):
"""Debug - send axis values to stdout"""
print("X={}, Y={}, Z={}".format(*self.read()))
def handle_irq(self, pin):
# Fetch IRQ source axes and clear interrupt
src = self.get_byte(LIS_IG1_SRC)
axes = (bool(src & 0x02), bool(src & 0x08), bool(src & 0x20)) # (x,y,z) event occurred
if self.user_int1_cb:
self.user_int1_cb(axes)
else:
print ("INT1={}".format(axes))
[docs] def enable_int1(self, threshold=20, duration=5, cb=None):
"""Enable inertial interrupt on all axes. Threshold is upper byte of 16-bit value, 0 disables."""
if threshold:
self.user_int1_cb = cb
threshold &= 0x7f
self.set_byte(LIS_IG1_CFG, 0x2a) # AND of abs(accel) > THR for all axes
self.set_byte(LIS_IG1_THRESH_X, threshold)
self.set_byte(LIS_IG1_THRESH_Y, threshold)
self.set_byte(LIS_IG1_THRESH_Z, threshold)
self.set_byte(LIS_IG1_DURATION, duration)
# Enable IRQ, using "soft IRQ handler" which automatically does a micropython.schedule to defer to main context
self.int1_pin.irq(self.handle_irq, trigger=machine.Pin.IRQ_RISING, hard=False)
else:
# Disable interrupt
self.int1_pin.irq(handler=None)
self.set_byte(LIS_IG1_CFG, 0x00)