MicroPython Driver for LIS3DSH Accelerometer

LIS3DSH Driver Code for micro:bit

Download as zip file

'''LIS3DSH 3-Axis Accelerometer
MicroPython driver for BBC micro:bit

AUTHOR: fredscave.com
DATE  : 2025/09
VERSION : 1.00
'''

from microbit import *
from micropython import const
from math import atan, sqrt, pi

_REG_OUT_T       = const(0x0C)
_REG_WHO_AM_I    = const(0x0F)
_REG_CTRL_REG4   = const(0x20)
_REG_CTRL_REG5   = const(0x24)
_REG_STATUS      = const(0x27)
_REG_OUT_X       = const(0x28)
_REG_OUT_Y       = const(0x2A)
_REG_OUT_Z       = const(0x2C)

# Measurement ranges configurations
_RANGES = {2:0, 4:1, 6:2, 8:3, 16:4}
_G_RANGE_MASK = (0x00, 0x08, 0x10, 0x18, 0x20)
_SENSITIVITY = (0.06, 0.12, 0.18, 0.24, 0.73)


class LIS3DSH():
    def __init__(self, ADDR=0x1D):
        self.ADDR = ADDR
        sleep(10)
       # Disable sensor output updates while
        # reading acceleration registers.
        self._blockDataUpdate
        self.SetGRange(2)
        # Set ODR = 100Hz, turn on sampling.
        self.SetODR()

    # Sets the acceleration +/- measurement range.
    # Valid values are 2, 4, 6, 8, 16.
    def SetGRange(self, Range=2):
        if Range not in (2, 4, 6, 8, 16):
            self.Range = 2
        else:
            self.Range = Range
        buf = self._readReg(_REG_CTRL_REG5, 1)
        b = buf[0]
        b = b & 0b11000000
        b = b | _G_RANGE_MASK[_RANGES[self.Range]]
        self._writeReg([_REG_CTRL_REG5, b])
        sleep(10)
        dummy = self.Reading

    # Sets the output data rate (ODR).
    # Valid values are 0 to 9.
    # A value of 0 powers down the LIS3DSH.
    # See Table 55 (page 39) of the datasheet
    # for the equivalent sampling frequencies.
    def SetODR(self, ODR=6):
        self.ODR = ODR if (ODR in range(10)) else 6
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b & 0b00001111
        b = b | (self.ODR << 4)
        self._writeReg([_REG_CTRL_REG4, b])
        dummy = self.Reading

# *******************************************
#              Properties
# *******************************************

    # Returns X-axis, Y-axis, Z-axis acceleration
    # in that order in a tuple in g units.
    @property
    def Reading(self):
       buf = self._readReg(_REG_OUT_X, 6)
       X_unscaled = buf[1] << 8 | buf[0]
       Y_unscaled = buf[3] << 8 | buf[2]
       Z_unscaled = buf[5] << 8 | buf[4]
       X = self._Scale(X_unscaled)
       Y = self._Scale(Y_unscaled)
       Z = self._Scale(Z_unscaled)
       return (X, Y, Z)

    @property
    # Returns X-axis acceleration in g units.
    def X(self):
        buf = self._readReg(_REG_OUT_X, 2)
        unscaled = buf[1] << 8 | buf[0]
        return self._Scale(unscaled)

    @property
    # Returns Y-axis acceleration in g units.
    def Y(self):
        buf = self._readReg(_REG_OUT_Y, 2)
        unscaled = buf[1] << 8 | buf[0]
        return self._Scale(unscaled)

    @property
    # Returns Z-axis acceleration in g units.
    def Z(self):
        buf = self._readReg(_REG_OUT_Z, 2)
        unscaled = buf[1] << 8 | buf[0]
        return self._Scale(unscaled)

    @property
    def GetGRange(self):
        return self.Range
        
    @property
    def GetODR(self):
        return self.ODR

    # Returns the chip's ID
    @property
    def GetID(self):
        id = self._readReg(_REG_WHO_AM_I, 1)
        return hex(id[0])

    # Returns True if there is valid acceleration
    # ready for reading.
    @property
    def IsDataReady(self):
        buf = self._readReg(_REG_STATUS, 1)
        b = buf[0]
        return (b & 0b00001000) != 0

    # Returns temperature in degrees Celsius.
    @property
    def Temperature(self):
        buf = self._readReg(_REG_OUT_T, 1)
        t = buf[0]
        t = t if (t < 128) else t - 256
        return t + 25

    # Returns inclination of X-axis from the
    # horizontal in degrees. Offsets are
    # subtracted if a calibration has been done.
    @property
    def Xangle(self):
        t = self.Reading
        X, Y, Z = t[0], t[1], t[2]
        p = atan(X / sqrt((Y*Y + Z*Z))) * 180 / pi
        return p

    # Returns inclination of Y-axis from the
    # horizontal in degrees. Offsets are
    # subtracted if a calibration has been done.
    @property
    def Yangle(self):
        t = self.Reading
        X, Y, Z = t[0], t[1], t[2]
        p = atan(Y / sqrt((X*X + Z*Z))) * 180 / pi
        return p

# *******************************************
#              Private Methods
# *******************************************

    # Writes one or more bytes to register.
    # Bytes is expected to be a list.
    # First element is the register address.
    def _writeReg(self, Bytes):
        i2c.write(self.ADDR, bytes(Bytes))

    # Read a given number of bytes from
    # a register.
    def _readReg(self, Reg, Num):
        self._writeReg([Reg])
        buf = i2c.read(self.ADDR, Num)
        return buf

    # Ensures that while acceleration result registers
    # are being read they won't be updated by the sensor.
    def _blockDataUpdate(self):
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b | 0b00001000
        self._writeReg([_REG_CTRL_REG4, b])

    # Scales raw acceleration.
    # Scaling is dependent on
    # current G range set.
    def _Scale(self, Raw):
        raw = Raw
        if raw > 32767:
            raw -= 65536
        scale = _SENSITIVITY[_RANGES[self.Range]]
        return raw * scale/1000
          

Selecting the Correct Range

The LIS3DSH offers five different measurement ranges. It's important to select the appropriate range for the expected acceleration values.

If an inappropriate range is selected then there will either be a loss of precision or the acceleration value will be 'saturated' (out of range).

For example: if acceleration values in the order of 2g to 3g are expected but the measurement range ±16g is being used there can be quite a loss of precision. In this case a more appropriate measurement range would be ±4g.

The following program demonstrates this expected loss of precision if the measurement range is too large. The LIS3DSH development board was set a random angle before running the code in the micro:bit.


Code:
# This program calculates an X-axis angle
# multiple times for each of the measurement
# ranges.
# For each set of angle measurements per
# range, the average and standard deviation
# is calculated.

from fc_lis3dsh import *
from math import sqrt
from microbit import sleep

Samples = 10

def Avg(L):
    # Returns the average (mean) of
    # the elements of a list.           
    n = len(L)
    sum = 0.0
    for i in range(n):
        sum += L[i]
    return sum/n  

def SD(L, avg):
    # Returns the sample standard deviation
    # of the elements of a list.
    n = len(L)
    sum = 0
    for i in range(n):
        diff = (L[i] - avg) ** 2
        sum += diff
    return sqrt(sum / (n-1))

# main program
sensor = LIS3DSH()
sensor.SetGRange(2)
sensor.SetODR(1) # 6.25Hz

L = [' '] * Samples
gRanges = (2, 4, 6, 8, 16)

for r in range(len(gRanges)):
    sensor.SetGRange(gRanges[r])
    for a in range(Samples):
        while not sensor.IsDataReady:
            sleep(5)
        L[a] = sensor.Xangle
    avg = round(Avg(L), 3)
    sd = round(SD(L, avg), 3)
    print('Range (+/-g):', sensor.GetGRange,
          '   Angle:', avg, '   SD', sd)

Typical Output:
Range (+/-g): 2    Angle: 32.321    SD 0.092
Range (+/-g): 4    Angle: 32.476    SD 0.104
Range (+/-g): 6    Angle: 32.363    SD 0.135
Range (+/-g): 8    Angle: 32.511    SD 0.144
Range (+/-g): 16    Angle: 32.392    SD 0.335
          
LIS3DSH breakout board connected to the micro:bit.
LIS3DSH generic breakout board connected to the micro:bit. It is labelled LIS3DH but the sensor actually present is the LIS3DSH.

As the measurement range is increased so does the standard deviation of the set of acceleration readings taken. This indicates an increasing loss of precision as the measurement range increases.

Interestingly, the angle value also increased with increasing measuring range. The breadboard holding the LIS3DSH was not touched, so the actual angle remained unaltered.

The above code was run several times with consistently the same trend. This would suggest that the sensitivity constants (_SENSITIVITY tuple in the driver code) used in the acceleration calculation need calibration if truly accurate acceleration values are required.