MicroPython Driver for LIS2DW12 Accelerometer

LIS2DW12 Driver Code for micro:bit

Download as zip file

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

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

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

_REG_WHO_AM_I = const(0x0F)
_REG_CTRL1   = const(0x20)
_REG_CTRL2   = const(0x21)
_REG_CTRL6   = const(0x25)
_REG_OUT_T   = const(0x26)
_REG_STATUS  = const(0x27)
_REG_OUT_X   = const(0x28)
_REG_OUT_Y   = const(0x2A)
_REG_OUT_Z   = const(0x2C)

FS = (2, 4, 8, 16)
LP1, LP2, LP3, LP4, HIGH = 0, 1, 2, 3, 4
SENSITIVITY = (0.244, 0.488, 0.976, 1.952)

class LIS2DW12():
    def __init__(self, ADDR = 0x19):
        self.ADDR = ADDR
        # Measurement not updated while
        # data registers are being read.
        # Increment register pointer after
        # each read or write.
        self._writeReg([_REG_CTRL2, 0b00001100])
        self.SetGRange()
        self.SetLowNoise()
        self.SetODR()
        self.SetPowerMode()


    # Turns low noise mode on or off.
    def SetLowNoise(self, On = True):
        self.LowNoise = On
        buf = self._readReg(_REG_CTRL6, 1)
        b = buf[0]
        if self.LowNoise == True:
            b= b | 0b00000100
        else:
            b = b & 0b11111000
        self._writeReg([_REG_CTRL6, b])

    # Sets the measurement range.
    # One of 2, 4, 8 or 16.
    def SetGRange(self, Range=2):
        if Range not in FS:
            self.Range = 2
        else:
            self.Range = Range
        buf = self._readReg(_REG_CTRL6, 1)
        b = buf[0]
        b = b & 0b11001100
        b = b | (FS.index(self.Range) << 4)
        self._writeReg([_REG_CTRL6, b])

    # Sets the output data rate (ODR).
    # One of 0 to 9.
    # A setting of 0 powers down the LIS2DW12.
    # Default ODR = 5 is 100Hz.
    # See Table 29 of the datasheet for Hz values.
    def SetODR(self, ODR = 5):
        if ODR not in range(10):
            self.ODR = 5
        else:
            self.ODR = ODR
        buf = self._readReg(_REG_CTRL1, 1)
        b = buf[0]
        b = b & 0b00001111
        b = b | (self.ODR << 4)
        self._writeReg([_REG_CTRL1, b])

    # Set power mode.
    # One of LP1, LP2, LP3, LP4 or HIGH.
    # See Table 30 and Table 31
    # of the datasheet for meaning.
    def SetPowerMode(self, Mode=HIGH):
        if Mode not in (LP1, LP2, LP3, LP4, HIGH):
            self.Mode = HIGH
        else:
            self.Mode = Mode
        buf = self._readReg(_REG_CTRL1, 1)
        b = buf[0]
        b = b & 0b11110000
        if self.Mode == HIGH:
            b = b | 0b00000100
        else:
            b = b | self.Mode
        self._writeReg([_REG_CTRL1, 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, 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, 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, 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, 2)
        unscaled = self._rawToInt(buf[1], buf[0])
        return self._scale(unscaled)


    # Returns the temperature.
    # This is only 8-bit resolution.
    @property
    def Temperature(self):
        buf = self._readReg(_REG_OUT_T, 1)
        b = buf[0]
        b = b if (b < 128) else b - 256
        return b + 25

    # Returns the output data rate (ODR).
    # See Table 29 of the datasheet for
    # explanation of values.
    @property
    def GetODR(self):
        return self.ODR

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

    # Returns the LIS2DW12 chip ID.
    # Should be 0x44
    @property
    def GetID(self):
        buf = self._readReg(_REG_WHO_AM_I, 1)
        return(hex(buf[0]))

    # Returns whether low noise mode
    # is on or off.
    @property
    def IsLowNoise(self):
        return self.LowNoise

    # Returns the power mode.
    # One 0f 0=LP1, 1=LP2, 2=LP3, 3=LP4, 4=HIGH.
    @property
    def GetPowerMode(self):
        return self.Mode

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

    # Returns inclination of X-axis from the
    # horizontal in degrees.
    @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.
    @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

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

    # Converts raw sensor binary value
    # to signed integer.
    def _rawToInt(self, MSB, LSB):
       if self.Mode != LP1: # 14-bit resolution
           Int = (MSB << 6) | (LSB >> 2)
           return Int if (Int < 8192) else Int - 16384
       else: # 12-bit resolution
           Int = (MSB << 4) | (LSB >> 4)
           return Int if (Int < 2048) else Int - 4096
          

Exploring the Power Modes

The LIS2DW12 offers five different power modes; 4 x Low-power and High-performance. There is a tradeoff with power consumption, precision and signal noise across these modes.

The following program demonstrates the expected increase in accuracy (and power consumption) with increasing precision and reducing signal noise across the power modes. The LIS2DW12 development board was set at a random angle before running the code in the micro:bit.


Code:
# This program calculates an Y-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_lis2dw12 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 = LIS2DW12()
sensor.Reading # Clear the data registers.

L = [' '] * Samples
Modes = {LP1:'LP1', LP2:'LP2', LP3:'LP3',
         LP4:'LP4', HIGH:'HIGH'}

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

Typical Output:
LP1  Angle = 4.193    SD = 0.515
LP2  Angle = 3.892    SD = 0.371
LP3  Angle = 4.111    SD = 0.285
LP4  Angle = 4.173    SD = 0.248
HIGH  Angle = 4.081    SD = 0.105
          

As the power mode moves up from low-power (LP1) to High-performance the standard deviation dramatically decreases as would be expected.

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