MicroPython Driver for LIS3DH Accelerometer
Contents
Introduction
This a MicroPython driver written specifically for the BBC micro:bit that will work with the LIS3DH accelerometer sensor.
The LIS3DH sensor is discussed in some detail here.
FIG 1 - LIS3DH breakout board: [Left] front view, [Right] rear view
The sensor is quite small and for projects using a breadboard its much easier to use a breakout board. The breakout boards are not particularly expensive.
Connecting the LIS3DH
This sensor has both I2C and SPI serial interfaces available. This driver will use I2C. Connecting the breakout board to the micro:bit is simple.
| micro:bit | Sensor board |
|---|---|
| 3.3V | VCC |
| GND | GND |
| Pin 19 | SCL |
| Pin 20 | SDA |
This utilises the standard I2C pins of the micro:bit.
Optionally, a jumper wire can be placed between the GND pin and the SDO pin on the breakout board. This changes the default I2C address and is discussed below.
Driver Overview
The driver implements the base functionality (i.e. acceleration measurements) offered by this sensor. Also included is a simple tool for measuring angles from the horizontal.
The temperature can be read from the chip's internal temperature sensor.
The FIFO and all interrupts (with the exception of Data Ready) are not available in this driver.
Driver codeThe driver code can be:
- Copied from this webpage onto the clipboard then pasted into the MicroPython editor e.g. the Mu Editor. It should be saved as fc_lis3dh.py - OR -
- Download as a zip file using the link. Unzip the file and save it as fc_lis3dh.py into the default directory where the MicroPython editor e.g. Mu Editor saves python code files.
After saving the fc_lis3dh.py file to the computer it should be copied to the small filesystem on the micro:bit. The examples on this page will not work if this step is omitted. Any MicroPython editor that recognises the micro:bit will have an option to do this.
The Mu Editor is recommended for its simplicity. It provides a Files button on the toolbar for just this purpose.
I2C addressLIS3DH breakout boards have a default I2C address of 0x19. This can be changed to 0x18 if a jumper wire is connected between the GND pin and SDO pin.
Class constructorThe driver is implemented as a class.The first thing to do is call the constructor of the LIS3DH class to obtain a sensor object.
Syntax:
LIS3DH(ADDR=0x19)
Where:
ADDR is the I2C address.
Example
from fc_lis3dh import *
# Declare a LIS3DH sensor object.
# The default I2C address is used.
sensor = LIS3DH()
This assumes that the file fc_lis3dh.py has been successfully copied to the micro:bit's filesystem as described above.
Methods and PropertiesThe driver provides methods and properties:
-
Power Mode settings:
SetPowerOn(), SetPowerMode(), GetPowerMode -
Setup sampling parameters:
SetODR(), GetODR
SetGRange(), GetGRange -
Read acceleration:
Reading, X, Y, Z, IsDataReady -
Read temperature:
Temperature -
Angle measurement:
Xangle, Yangle -
Chip ID:
ID
Power Modes
There are four possible power modes and this driver implements all of them:
- Power off : No sampling
- Low power : 8-bit precision
- Normal : 10-bit precision
- High resolution : 12-bit precision
Syntax:
SetPowerOn(On = True)
Powers up or powers down the LIS3DH.
If True is passed to this method the sensor is
powered up with the previous ODR and measurement
range, else the sensor is powered down.
SetPowerMode(Mode = High)
Method that sets the sampling power mode;
One of LOW, NORMAL or HIGH
GetPowerMode
Method that returns the sampling power mode.
One of 0 (LOW), 1 (NORMAL), 2 (HIGH)
Example 1
# Power down the LIS3DH.
# Take several readings to show that the sensor
# is not doing conversions.
# Power up the sensor and take some readings
from fc_lis3dh import *
from microbit import sleep
sensor = LIS3DH()
# Power down the sensor
sensor.SetPowerOn(False)
print('LIS3DH is powered down')
for __ in range(3):
sleep(500)
print(sensor.Reading)
# Power up the sensor
sensor.SetPowerOn()
print('\nLIS3DH is powered up')
for __ in range(3):
sleep(500)
print(sensor.Reading)
Typical Output
LIS3DH is powered down
(0.66, -0.032, 0.79)
(0.66, -0.032, 0.79)
(0.66, -0.032, 0.79)
LIS3DH is powered up
(0.685, -0.029, 0.8)
(0.671, -0.027, 0.793)
(0.673, -0.03, 0.796)
Example 2
# Test all three power modes of the LIS3DH.
from fc_lis3dh import *
from microbit import sleep
Modes = ('LOW', 'NORMAL', 'HIGH')
sensor = LIS3DH()
# Configure Low-power mode.
sensor.SetPowerMode(LOW)
print('Power Mode:', Modes[sensor.GetPowerMode])
sleep(200)
print(sensor.Reading)
# Configure Normal mode.
sensor.SetPowerMode(NORMAL)
print('\nPower Mode:', Modes[sensor.GetPowerMode])
sleep(200)
print(sensor.Reading)
# Configure High-resolution mode.
sensor.SetPowerMode(HIGH)
print('\nPower Mode:', Modes[sensor.GetPowerMode])
sleep(200)
print(sensor.Reading)
Typical Output
Power Mode: LOW
(0.656, -0.032, 0.8)
Power Mode: NORMAL
(0.664, -0.02, 0.808)
Power Mode: HIGH
(0.672, -0.026, 0.795)
Sampling Options
The user has the option to set the output data rate (ODR) and measurement range.
Syntax:
SetODR(ODR = 5)
Sets the output data rate (ODR).
One of 1 to 7.
This provides a range from 1Hz to 400Hz[1].
(1, 10, 25, 50, 100, 200, 400 all in Hz)
Default is 100Hz.
See Table 31 (page 35) in the datasheet.
GetODR
Returns the ODR in use.
One of 1 to 7.
SetGRange(Range=2)
Sets the measurement range.
One of 2, 4, 8, 16.
GetGRange
Returns the measurement range in use.
One of 2, 4, 8, 16.
Example:
from fc_lis3dh import *
# Declare a LIS3DH sensor object
sensor = LIS3DH()
# Report the default sampling options.
odr_freq = (0, 1, 10, 25, 50, 100, 200, 400)
odr = sensor.GetODR
print('ODR:', odr_freq[odr], 'Hz')
print('Range: +/-', sensor.GetGRange, 'g')
# Change ODR and measurement range.
sensor.SetODR(7) # 400Hz
sensor.SetGRange(4) # +/-4g
print('\nNew settings...')
odr = sensor.GetODR
print('ODR:', odr_freq[odr], 'Hz')
print('Range: +/-', sensor.GetGRange, 'g')
Output:
ODR: 100 Hz
Range: +/- 2 g
New settings...
ODR: 400 Hz
Range: +/- 4 g
Measuring Acceleration
It's a very simple process to read acceleration values from the sensor. The acceleration values have units of g (1g = 9.8 m/sec2).
Syntax:
Reading
Property that returns acceleration in all three axis.
Values are returned in a tuple in the follow order:
(X-axis, Y-axis, Z-axis)
X
Property that returns the acceleration in the X-axis.
Y
Property that returns the acceleration in the Y-axis.
Z
Property that returns the acceleration in the Z-axis.
IsDataReady
Property returns True if the sensor
has new data ready for reading.
Example:
from fc_lis3dh import *
# Declare a LIS3DH sensor object
sensor = LIS3DH()
# Read acceleration of all three axis.
acc = sensor.Reading
print('(X-axis, Y-axis, Z-axis):', acc)
# Read acceleration separately from each axis.
# Wait till data is ready.
while not sensor.IsDataReady:
sleep(1)
print('\nX-axis:', sensor.X)
print('Y-axis:', sensor.Y)
print('Z-axis:', sensor.Z)
Typical Output:
(X-axis, Y-axis, Z-axis): (0.006, -0.042, 1.0)
X-axis: 0.014
Y-axis: -0.019
Z-axis: 1.017
In the above example the sensor was lying on a flat horizontal surface (mounted in a breadboard). In this orientation the X and Y axis acceleration should be very close to 0g and the Z axis acceleration should be close to 1g (from the Earth's gravity).
Measuring Temperature
This driver reads the temperature sensor using only an 8-bit precision. The temperature reading produced should only be taken as indicative - definitely not for any serious process control!
Syntax:
Temperature
A property that returns the temperature
in degrees Celsius.
Example:
from fc_lis3dh import *
sensor = LIS3DH()
print('Temperature:', sensor.Temperature, 'C')
Typical Output:
Temperature: 23 C
Measuring Angles
Accelerometers measures:
- static : The sensor is stationary but is under the influence of the Earth's gravitational field
- dynamic : The component of acceleration resulting from the motion or changes in velocity experienced by the sensor over time.
When the accelerometer is stationary and lying perfectly perpendicular to the direction of the Earths gravity, the X-axis and Y-axis acceleration will both be 0g. The Z-axis acceleration will be 1g.
This fact along with some simple trigonometry can be used to measure angles in the X-Axis or Y-axis.
Syntax:
Xangle
Property returns inclination of X-axis from the
horizontal in degrees.
Yangle
Property returns inclination of Y-axis from the
horizontal in degrees.
Example:
# Demonstrates the calculation of the
# X-axis angle from the horizontal.
# While the program is running change
# the X-axis tilt angle to output different
# angle values.
from fc_lis3dh import *
from microbit import sleep
sensor = LIS3DH()
# Calculate the X-axis angle every two seconds.
# Terminate the program in the REPL with
# Ctrl + C on Windows or Command + C on Mac.
while True:
sleep(2000)
print(sensor.Xangle)
Typical Output:
0.6208561
0.8456769
56.70201
75.67381
85.11881
40.12904
15.85772
0.7916098
0.9575236
Traceback (most recent call last):
File "main.py", line 17, in <module>
KeyboardInterrupt:
In the above example the board was tilted at various angles between the two second reads.
Product ID
The LIS3DH has a product ID burnt into non-volatile memory at time of manufacture. This driver provides a simple property to read this ID.
Syntax:
GetID
Returns the product ID
Example:
from fc_lis3dh import *
sensor = LIS3DH()
print('LIS3DH product ID:', sensor.GetID)
Output:
LIS3DH product ID: 0x33
Enjoy!
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.