MicroPython Driver for MMA8452 Accelerometer

MMA8452 Driver Code for micro:bit

Download as zip file

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

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

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

_REG_STATUS       = const(0x00)
_REG_OUT_X        = const(0x01)
_REG_OUT_Y        = const(0x03)
_REG_OUT_Z        = const(0x05)
_REG_WHO_AM_I     = const(0x0D)
_REG_XYZ_DATA_CFG = const(0xE)
_REG_CTRL_REG1    = const(0x2A)
_REG_CTRL_REG2    = const(0x2B)

RANGES = (2, 4, 8)
SENSITIVITIES = (1024, 512, 256)

class MMA8451():
    def __init__(self, ADDR=0x1C):
        self.ADDR = ADDR
        self.SetGRange() # +/-2g
        self.SetODR() # 100Hz
        self.SetOS() # Normal
        self.SetLowNoise(False) # Low Noise mode is off.
        self.SetActiveMode()

   # Sets active mode or standby mode.
    def SetActiveMode(self, Active=True):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        if Active == True:
            b = b | 0b00000001
        else:
            b = b & 0b11111110
        self._writeReg([_REG_CTRL_REG1, b])

    # Sets measurement range.
    # One of 2, 4, 8 (+/-g)
    def SetGRange(self, Range = 2):
        self.Range = Range if (Range in RANGES) else 2
        active = False
        # Must be in Standby mode.
        if self.IsActiveMode == True:
            active = True
            self.SetActiveMode(False)
        buf = self._readReg(_REG_XYZ_DATA_CFG, 1)
        b = buf[0]
        b = b & 0b11111100
        b = b | RANGES.index(self.Range)
        self._writeReg([_REG_XYZ_DATA_CFG, b])
        if active == True:
            self.SetActiveMode()

    # Sets output data rate (ODR).
    # 0 = 800Hz, 1 = 400Hz, 2 = 200Hz, 3 = 100Hz
    # 4 = 50Hz, 5 = 12.5Hz, 6 = 6.25Hz, 7 = 1.56Hz
    def SetODR(self, ODR = 3):
        odr = ODR if (ODR in range(8)) else 3
        active = False
        # Must be in Standby mode.
        if self.IsActiveMode == True:
            active = True
            self.SetActiveMode(False)
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b11000111
        b = b | (odr << 3)
        self._writeReg([_REG_CTRL_REG1, b])
        if active == True:
            self.SetActiveMode()

    # Sets the oversampling mode.
    # One of 0, 1, 2, 3
    # Highest data resolution is
    # at OS = 2 and ODR = 7.
    # See datasheet Table 66 and Table 67.
    def SetOS(self, OS=0):
        os = OS if (OS in range(4)) else 0
        active = False
        # Must be in Standby mode.
        if self.IsActiveMode == True:
            active = True
            self.SetActiveMode(False)
        buf = self._readReg(_REG_CTRL_REG2, 1)
        b = buf[0]
        b = b & 0b11111100
        b = b | os
        self._writeReg([_REG_CTRL_REG2, b])
        if active == True:
            self.SetActiveMode()

    # Sets Low Noise mode
    def SetLowNoise(self, LowNoise = True):
        active = False
        # Must be in Standby mode.
        if self.IsActiveMode == True:
            active = True
            self.SetActiveMode(False)
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        b = b & 0b11111011
        if LowNoise == True:
            b = b | 4
        self._writeReg([_REG_CTRL_REG1, b])
        if active == True:
            self.SetActiveMode()

# *******************************************
#              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[0] << 4) | (buf[1] >> 4)
       Y_unscaled = (buf[2] << 4) | (buf[3] >> 4)
       Z_unscaled = (buf[4] << 4) | (buf[5] >> 4)
       X = self._Scale(X_unscaled)
       Y = self._Scale(Y_unscaled)
       Z = self._Scale(Z_unscaled)
       return (X, Y, Z)

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

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

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

    # Returns True if a new acceleration dataset
    # is available for all three axis (X, Y, Z)
    @property
    def IsDataReady(self):
        buf = self._readReg(_REG_STATUS, 1)
        b = buf[0]
        return ((b & 0b00001000) >> 3) == 1

    # Returns True if new X-axis acceleration
    # data is available.
    @property
    def IsXDataReady(self):
        buf = self._readReg(_REG_STATUS, 1)
        b = buf[0]
        return (b & 1)  == 1

    # Returns True if new Y-axis acceleration
    # data is available.
    @property
    def IsYDataReady(self):
        buf = self._readReg(_REG_STATUS, 1)
        b = buf[0]
        return (b & 0b00000010)  == 2

    # Returns True if new Z-axis acceleration
    # data is available.
    @property
    def IsZDataReady(self):
        buf = self._readReg(_REG_STATUS, 1)
        b = buf[0]
        return (b & 0b00000100)  == 4

    # Returns True if in Active mode.
    # Returns False if in Standby mode.
    @property
    def IsActiveMode(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        return (buf[0] & 1) == 1

    # Returns measurement range.
    # One of 2, 4, 8 (+/-g)
    @property
    def GetGRange(self):
        buf = self._readReg(_REG_XYZ_DATA_CFG, 1)
        b = buf[0]
        return RANGES[(b & 0b00000011)]

    # Returns output data rate (ODR).
    # One of 0 to 7.
    @property
    def GetODR(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        return (b & 0b00111000) >> 3

    # Returns the oversampling mode.
    # One of 0, 1, 2, 3
    # See datasheet Table 66 and Table 67.
    @property
    def GetOS(self):
        buf = self._readReg(_REG_CTRL_REG2, 1)
        b = buf[0]
        return b & 3

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

    # Returns True if in Low Noise mode.
    @property
    def IsLowNoise(self):
        buf = self._readReg(_REG_CTRL_REG1, 1)
        b = buf[0]
        return (b & 4) == 4

    # 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
# *******************************************

    # Scales raw acceleration.
    # Scaling is dependent on
    # current G range set.
    def _Scale(self, Raw):
        raw = Raw
        raw = raw if (raw < 2048) else raw - 4096
        scale = SENSITIVITIES[RANGES.index(self.Range)]
        return raw / scale

    # Writes one or more bytes to register.
    # Bytes is expected to be a list.
    # First element is the register address.
    # NOTE: A stop bit is not sent.
    def _writeReg(self, Bytes):
        i2c.write(self.ADDR, bytes(Bytes), repeat=True)

    # 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
          

Exploring the Noise Modes

The MMA8452 offers two different noise reduction strategies to improve resolution and accuracy.

Oversampling is a widely used technique. The simplest approach is to internally take multiple samples then report the average through the sensor's digital interface. This approach is used by this accelerometer. The application of oversampling is not trivial though and the user should consult the datasheet.

Additional, the MMA8452 also provides a separate low noise setting. There's very little information in the datasheet other than how to turn the setting on or off.

The MMA8452 development board, mounted on a breadboard, was set at a random angle before running the code in the micro:bit.


Code:
# This program reads the X-axis acceleration
# multiple times for three separate scenarios.

# SCENERIO 1: Low noise off, Lowest level of oversampling used.
# SCENERIO 2: Low noise off, Highest level of oversampling used.
# SCENERIO 3: Low noise on, Lowest level of oversampling used.

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

from fc_mma8452 import *
from math import sqrt
from microbit import sleep

Samples = 20
sensor = MMA8452()

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))

def GetSamples(Scenerio):
    L = [' '] * Samples
    for s in range(Samples):
        while not sensor.IsXDataReady:
            sleep(1)
        L[s] = sensor.Xangle
    avg = round(Avg(L), 4)
    sd = round(SD(L, avg), 4)
    print('SCENARIO', Scenerio,
          'Angle =', avg, ' SD =', sd)

# main program
print()
# SCENERIO 1
sensor.SetODR(0) # 800Hz
sensor.SetLowNoise(False) # Low noise off
sensor.SetOS(0) # Minimum oversampling
sensor.Reading # Clear the data registers.
GetSamples(Scenerio=1)

# SCENERIO 2
sensor.SetODR(7)  # 1.56Hz
sensor.SetLowNoise(False)  # Low noise off
sensor.SetOS(2)  # Maximum oversampling
GetSamples(Scenerio=2)

# SCENERIO 3
sensor.SetODR(0)  # 800Hz
sensor.SetLowNoise(True)  # Low noise on
sensor.SetOS(0)  # Minimum oversampling
GetSamples(Scenerio=3)

Typical Output:
SCENARIO 1 Angle = 22.3118  SD = 0.241
SCENARIO 2 Angle = 22.1666  SD = 0.0258
SCENARIO 3 Angle = 22.3887  SD = 0.1348
          

This test program was run multiple times, with each run yielding similar results. The low noise setting does reduce spread and is a useful option to consider if resolution is important at high output data rates.

However, the use of oversampling (but needing lower data rate output) is the best option if maximum resolution and accuracy is required.

MMA8451 breakout board connected to the micro:bit.
MMA8452 breakout board connected to the micro:bit