MicroPython Driver for LIS3DH Accelerometer

LIS3DH Driver Code for micro:bit

Download as zip file

'''            
LIS3DH 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_TEMP     = const(0x0D)
_REG_WHO_AM_I     = const(0x0F)
_REG_CTRL_REG1    = const(0x20)
_REG_CTRL_REG4    = const(0x23)
_REG_STATUS_REG   = const(0x27)
_REG_TEMP_CFG_REG = const(0x1F)
_REG_OUT_X       = const(0x28)
_REG_OUT_Y       = const(0x2A)
_REG_OUT_Z       = const(0x2C)

# Power modes
LOW    = const(0)
NORMAL = const(1)
HIGH   = const(2)

# Measurement ranges configurations
RANGES = {2:0, 4:1, 8:2, 16:3}

# Sensitivity (Measurement range(Power mode))
# in units of mg/digit.
SENSITIVITY = ((16, 4, 1), (32, 8, 2),
               (64, 16, 4), (192, 48, 12))

class LIS3DH():
    def __init__(self, ADDR=0x19):
        self.ADDR = ADDR
        # Ensure X, Y, Z axis all active.
        self._writeReg([_REG_CTRL_REG1, 0b00000111])
        # Ensure self-test mode is off.
        # Turn on BDU which prevents sensor updates
        # while data registers are being read.
        self._writeReg([_REG_CTRL_REG4, 0b10000000])
        # Enable temperature sensor output
        self._writeReg([_REG_TEMP_CFG_REG, 0b11000000])
        self.SetODR() # 100Hz
        self.SetGRange() # +/-2g
        self.SetPowerMode() # High resolution

    # Set/reset Power-down mode
    def SetPowerOn(self, On=True):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b00001111
        if On:
            b = b | (self.ODR << 4)
            self._writeReg([_REG_CTRL_REG1, b])
        else:
            self._writeReg([_REG_CTRL_REG1, b]) 

    # Set output data rate (ODR)
    # Valid values: 1 to 7
    # Default = 5 (100Hz)
    def SetODR(self, ODR=5):
        if ODR not in range(1, 8):
            self.ODR = 5
        else:
            self.ODR = ODR
        buf =self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b00001111
        b = b | (self.ODR << 4)
        self._writeReg([_REG_CTRL_REG1, b])

    # Sets the power mode.
    # One of LOW, NORMAL, HIGH
    def SetPowerMode(self, Mode=HIGH):
        if Mode not in (LOW, NORMAL, HIGH):
            self.Mode = HIGH
        else:
            self.Mode = Mode
        if self.Mode == LOW:
            self._setPowerLow()
        elif self.Mode == NORMAL:
            self._setPowerNormal()
        else:
            self._setPowerHigh()

    # Sets the acceleration +/- measurement range.
    # Valid values are 2, 4, 8, 16.
    def SetGRange(self, Range=2):
        if Range not in RANGES:
            self.Range = 2
        else:
            self.Range = Range
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b & 0b10001000
        b = b | (RANGES[self.Range] << 4)
        self._writeReg([_REG_CTRL_REG4, b])

# *******************************************
#              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 | 0b10000000, 6)
       unscaledX = self._rawToInt(buf[1], buf[0])
       unscaledY = self._rawToInt(buf[3], buf[2])
       unscaledZ = self._rawToInt(buf[5], buf[4])
       X = self._scale(unscaledX)
       Y = self._scale(unscaledY)
       Z = self._scale(unscaledZ)
       return (X, Y, Z)

    # Return acceleration on the X-axis.
    @property
    def X(self):
       buf = self._readReg(_REG_OUT_X | 0b10000000, 2)
       unscaled = self._rawToInt(buf[1], buf[0])
       return self._scale(unscaled)

    # Return acceleration on the Y-axis.
    @property
    def Y(self):
       buf = self._readReg(_REG_OUT_Y | 0b10000000, 2)
       unscaled = self._rawToInt(buf[1], buf[0])
       return self._scale(unscaled)

    # Return acceleration on the Z-axis.
    @property
    def Z(self):
       buf = self._readReg(_REG_OUT_Z | 0b10000000, 2)
       unscaled = self._rawToInt(buf[1], buf[0])
       return self._scale(unscaled)

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

    # Get output data rate (ODR)
    # Returns a value 1..7
    # See Table 31 (page 35) of the datasheet.
    @property
    def GetODR(self):
        return self.ODR

    # Get power mode.
    # One of 0 (LOW), 1 (NORMAL), 2 (HIGH)
    @property
    def GetPowerMode(self):
        return self.Mode

    # Get measurement range
    # Returns one of 2, 4, 8 or 16.
    @property
    def GetGRange(self):
        return self.Range

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

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

    # 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

    # Set power mode to Low-power.
    def _setPowerLow(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b | 0b00001111
        self._writeReg([_REG_CTRL_REG1, b])
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b & 0b11110000
        self._writeReg([_REG_CTRL_REG4, b])

    # Set power mode to Normal.
    def _setPowerNormal(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b11110111
        self._writeReg([_REG_CTRL_REG1, b])
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b & 0b11110000
        self._writeReg([_REG_CTRL_REG4, b])

    # Set power mode to High resolution.
    def _setPowerHigh(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b11110111
        self._writeReg([_REG_CTRL_REG1, b])
        buf = self._readReg(_REG_CTRL_REG4, 1)
        b = buf[0]
        b = b | 0b00001000
        self._writeReg([_REG_CTRL_REG4, b])

    # Converts raw sensor binary value
    # to signed integer.
    def _rawToInt(self, MSB, LSB):
       if self.Mode == LOW:
           return MSB if (MSB < 128) else MSB - 256
       if self.Mode == NORMAL:
           Int = (MSB << 2) | (LSB >> 6)
           return Int if (Int< 512) else Int - 1024
       if self.Mode == HIGH:
           Int = (MSB << 4) | (LSB >> 4)
           return Int if (Int < 2048) else Int - 4096

    # Scale raw acceleration value.
    # The scale depends upon which power mode
    # and measurement range is configured.
    def _scale(self, unscaled):
        scale = SENSITIVITY[RANGES[self.Range]][self.Mode]
        return unscaled * scale/1000
          

Exploring the Power Modes

The LIS3DH offers three different power modes; Low-power, Normal and High-resolution. There is a tradeoff with power consumption versus precision across these modes.

The following program demonstrates the expected increase in accuracy with the increasing output data resolution across the power modes. The LIS3DH development board was set at 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 power modes.

# For each set of angle measurements per
# mode, the average and standard deviation
# is calculated.

from fc_lis3dh 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 = LIS3DH()
sensor.SetGRange(2)
sensor.SetODR(1) # 1Hz
sensor.Reading # Clear the data registers.

L = [' '] * Samples
Modes = {LOW:'Low-power:      ',
         NORMAL:'Normal:         ',
         HIGH:'High-resolution:'}

# Cycle through the modes.
# In each mode collect some samples and
# calculate average and standard deviation.
for r in range(len(Modes)):
    sensor.SetPowerMode(r)
    for s in range(Samples):
        while not sensor.IsDataReady:
            sleep(1)
        L[s] = sensor.Xangle
    avg = round(Avg(L), 3)
    sd = round(SD(L, avg), 3)
    print(Modes[r],
          ' Angle =', avg, '   SD =', sd)

Typical Output:
Low-power:        Angle = 50.103    SD = 1.111
Normal:           Angle = 51.098    SD = 0.688
High-resolution:  Angle = 50.518    SD = 0.322
          

As the power mode moves up from low-power (8-bit) to normal (10-bit) to high-resolution (12-bit) the standard deviation dramatically decreases as would be expected.

LIS3DH breakout board connected to the micro:bit.
LIS3DH breakout board connected to the micro:bit