MicroPython Driver for BMA400 Accelerometer

BMA400 Driver Code for micro:bit

Download as zip file

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

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

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

_REG_CHIP_ID     = const(0x00)
_REG_STATUS      = const(0x03)
_REG_ACC_X       = const(0x04)
_REG_ACC_Y       = const(0x06)
_REG_ACC_Z       = const(0x08)
_REG_TIME        = const(0x0A)
_REG_INT_STAT0   = const(0x0E)
_REG_INT_STAT1   = const(0x0F)
_REG_TEMP_DATA   = const(0x11)
_REG_ACC_CONFIG0 = const(0x19)
_REG_ACC_CONFIG1 = const(0x1A)
_REG_ACC_CONFIG2 = const(0x1B)
_REG_INT_CONFIG0 = const(0x1F)
_REG_INT_CONFIG1 = const(0x20)

_G_RANGE = (2, 4, 8, 16)
_G_RANGE_CONFIG = {2:0x00, 4:0x40,
                   8:0x80, 16:0xC0}
_G_RANGE_SCALE = {2:1024, 4:512,
                  8:256, 16:128}

SLEEP = 0
LOW_POWER = 1
NORMAL = 2
_CMD_PWR_MODE = (0, 1, 2)

class BMA400:
    def __init__(self, ADDR=0x14):
        self.ADDR = ADDR
        self.GRange = 4
        self.Filter = 1
        self.OSR = 3
        self.ODR = 0x08
        self._latchInt()
        self._enableDataReady()
        self.SetPowerMode(NORMAL)

    # Sets the power mode. One of:
    # SLEEP, LOW_POWER or NORMAL
    def SetPowerMode(self, Mode=NORMAL):
        if Mode not in (SLEEP, LOW_POWER, NORMAL):
            self.Mode = NORMAL
        else:
            self.Mode = Mode
        self._writeReg([_REG_ACC_CONFIG0, self.Mode])
        self.SetGRange(self.GRange)
        if self.Mode == NORMAL:
            self.SetFilter(self.Filter)
        elif self.Filter == 2:
            self.SetFilter(1)
        self.SetOSR(self.OSR)
        self.SetODR(self.ODR)
        sleep(80)

    # Sets ODR (output data rate)
    # for NORMAL mode.
    def SetODR(self, ODR = 0x08):
        if ODR not in range(0x05, 0x0c):
            self.ODR = 0x08
        else:
            self.ODR = ODR
        buf = self._readReg(_REG_ACC_CONFIG1, 1)
        r = buf[0]
        r = (r >> 4) << 4
        r = r | self.ODR
        self._writeReg([_REG_ACC_CONFIG1, r])

    # Sets level of oversampling (OSR).
    # Value of 0..3
    def SetOSR(self, OSR=3):
        if OSR not in range(4):
            self.OSR = 3
        else:
            self.OSR = OSR
        # Set OSR for NORMAL mode.
        buf = self._readReg(_REG_ACC_CONFIG1, 1)
        r = buf[0]
        r = r & 0b11001111
        r = r | (self.OSR << 4)
        self._writeReg([_REG_ACC_CONFIG1, r])
        # Set OSR for LOW_POWER mode.
        buf = self._readReg(_REG_ACC_CONFIG0, 1)
        r = buf[0]
        r = r & 0b10011111
        r =r | (self.OSR << 5)
        self._writeReg([_REG_ACC_CONFIG0, r])

    # Sets G range.
    # One of 2, 4, 8, 16.
    def SetGRange(self, Range=4):
        if Range not in _G_RANGE:
            self.GRange = 4
        else:
            self.GRange = Range
        buf = self._readReg(_REG_ACC_CONFIG1, 1)
        config = buf[0]
        config = config & 0b00111111
        r = self.GRange
        config = config | _G_RANGE_CONFIG[r]
        self._writeReg([_REG_ACC_CONFIG1, config])

    # Sets bandwidth filtering for NORMAL mode.
    # 1 : Variable ODR
    # 2 : ODR = 100Hz, Low-pass bandwidth filter @ 1Hz
    def SetFilter(self, Filter=1):
        if Filter not in (1, 2):
            self.Filter = 1
        else:
            self.Filter = Filter
        if self.Filter == 1:
            self._writeReg([_REG_ACC_CONFIG2, 0])
        else:
            self._writeReg([_REG_ACC_CONFIG2, 8])
        sleep(20)

# *******************************************
#              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_ACC_X, 6)
       X_unscaled = buf[0] + buf[1] * 256
       Y_unscaled = buf[2] + buf[3] * 256
       Z_unscaled = buf[4] + buf[5] * 256
       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_ACC_X, 2)
        unscaled = buf[0] + buf[1] * 256
        return self._Scale(unscaled)

    # Returns Y-axis acceleration in g units.
    @property
    def Y(self):
        buf = self._readReg(_REG_ACC_Y, 2)
        unscaled = buf[0] + buf[1] * 256
        return self._Scale(unscaled)

    # Returns Z-axis acceleration in g units.
    @property
    def Z(self):
        buf = self._readReg(_REG_ACC_Z, 2)
        unscaled = buf[0] + buf[1] * 256
        return self._Scale(unscaled)

    # Returns temperature in degrees Celsius.
    @property
    def Temperature(self):
        buf = self._readReg(_REG_TEMP_DATA, 1)
        temp = buf[0]
        if temp > 127:
            temp -= 256
        return temp * 0.5 + 23

    # Returns Power mode.
    # 0 : SLEEP
    # 1 : LOW_POWER
    # 2 : NORMAL
    @property
    def GetPowerMode(self):
        return self.Mode

    # Returns oversampling rate (OSR)
    # Value of 0..3
    @property
    def GetOSR(self):
        return self.OSR

    # Returns G range.
    # One of 2, 4, 8, 16.
    @property
    def GetGRange(self):
        return self.GRange

    # Returns ODR (output data rate)
    # for NORMAL mode.
    @property
    def GetODR(self):
        return self.ODR

    # Returns the NORMAL mode filter setting.
    # 0 : low bandwidth filtering
    # 1 : high bandwidth filtering.
    @property
    def GetFilter(self):
        return self.Filter

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

    # Returns the system clock value.
    # Milliseconds from last power-on or reset.
    @property
    def Time(self):
        buf = self._readReg(_REG_TIME, 3)
        ttemp = buf[0] + buf[1]*256 + buf[2]*65536
        return int(ttemp * 0.3125)

    # Returns True if there is a new
    # acceleration data set ready.
    @property
    def IsDataReady(self):
        buf = self._readReg(_REG_INT_STAT0, 1)
        return (buf[0] & 0b10000000) == 0b10000000

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

    # Scales raw acceleration.
    # Scaling is dependent on
    # current G range set.
    def _Scale(self, Raw):
        raw = Raw
        if raw > 2047:
            raw -= 4096
        scale = _G_RANGE_SCALE[self.GRange]
        return raw / scale

    # Enable interrupt latching
    def _latchInt(self):
        buf = self._readReg(_REG_INT_CONFIG1, 1)
        r = buf[0]
        r = r | 0b10000000
        self._writeReg([_REG_INT_CONFIG1, r])

    # Enable data ready interrupt
    def _enableDataReady(self):
        buf = self._readReg(_REG_INT_CONFIG0, 1)
        r = buf[0]
        r = r | 0b10000000
        self._writeReg([_REG_INT_CONFIG0, r])

    # 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


          

Comparing OSR Settings

Oversampling (OSR) is a common technique used to increase measurement accuracy. The BMA400 has four levels (0, 1, 2,3) of oversampling builtin and is user selectable.

Accuracy would be expected to increase as the OSR value is increased. The following program cycles through the four levels of OSR, calculating the X-axis angle multiple times at each OSR level. At each OSR level the average X-axis angle and the standard deviation is calculated.

As the OSR rate increases it would be expected that the standard deviation becomes smaller.

The Code:

# This program calculates an X-axis angle
# multiple times for each of the
# oversampling (OSR) settings.

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

from fc_bma400 import *
from math import sqrt
from microbit import sleep

Samples = 15
L = [' '] * Samples

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 = BMA400()
sensor.SetGRange(2)
sensor.SetODR(8) # 100Hz
sleep(200)
dummy = sensor.Reading # Clear the data registers.

# Cycle through the OSR settings.
# For each OSR collect some samples and
# calculate average and standard deviation.
for osr in range(4):
    sensor.SetOSR(osr)
    for s in range(Samples):
        sleep(200)
        L[s] = sensor.Xangle
    avg = round(Avg(L), 3)
    sd = round(SD(L, avg), 3)
    print('OSR =', osr, ' Angle =', avg,
          '   SD =', sd)

Typical Output:
OSR = 0  Angle = 16.129    SD = 0.19
OSR = 1  Angle = 16.588    SD = 0.167
OSR = 2  Angle = 15.629    SD = 0.106
OSR = 3  Angle = 16.276    SD = 0.079

          

As expected, the standard deviation does trend smaller as the oversampling rate increases. This indeed does indicate that greater accuracy can be expected at higher oversampling rates.

BMA400 breakout board connected to the micro:bit
BMA400 breakout board connected to the micro:bit's I2C bus