how to configure the accelerometer range on the microbit using micropython

This article details how to set the range of sensitivity on the accelerometer on the microbit board using micropython and the i2c interface. I am using v1.7.9 of micropython for the microbit, the mu editor and linux mint v17.
 

After listening to Joe Finney talk about his role in developing the microbit board I realised I could use it for some of my hand gesture assistive technology work. The accelerometer on the microbit board is an MMA8653FC, data sheet here. There are programming notes for this chip here. The default range for this chip is +/-2g. This can be reconfigured to be +/-4g or +/-8g. For some of the students I work with on gesture recognition I need the higher ranges. So I entered the world of microbit i2c programming. I chose the micropython platform as python is always the ‘second best choice’ for any programming application. Actually, I’m a fan of using C for embedded hardware, but in this case using micropython looked to be fastest way of getting a solution. I used the simple mu editor. Long story short, it’s all about syntax. Thanks go to fizban for his example microbit code to interface a microbit with an lcd display using i2c. After reading this code I fixed the mistake(s) I’d been making. The documentation for the i2c microbit micropython is here.

Here’s my working code:

''' microbit i2c communications with onboard accelerometer '''
from microbit import *

ACCELEROMETER = 0x1d
ACC_2G = [0x0e, 0x00]
ACC_4G = [0x0e, 0x01]
ACC_8G = [0x0e, 0x02]
CTRL_REG1_STANDBY = [0x2a, 0x00]
CTRL_REG_1_ACTIVE = [0x2a, 0x01]
PL_THS_REG = [0x14] # returns b'\x84'
PL_BF_ZCOMP = [0x13] # returns b'\44' = 'D'
WHO_AM_I = [0x0d] # returns 0x5a=b'Z'
XYZ_DATA_CFG = [0x0e]

def command(c):
''' send command to accelerometer '''
i2c.write(ACCELEROMETER, bytearray(c))

def i2c_read_acc(register):
''' read accelerometer register '''
i2c.write(ACCELEROMETER, bytearray(register), repeat=True)
read_byte = i2c.read(ACCELEROMETER, 1)
print('read: {}'.format(read_byte))

def main_text():
''' send accelerometer data as a string '''
print('starting main')
counter = 0
while True:
x = accelerometer.get_x()
y = accelerometer.get_y()
z = accelerometer.get_z()
counter = counter + 1
print('{} {} {} {}'.format(counter, x, y, z))
sleep(250)

print("sending i2c commands...")
print('reading PL_BF_ZCOMP :')
print(i2c_read_acc(PL_BF_ZCOMP))
print('reading WHO_AM_I')
print(i2c_read_acc(WHO_AM_I))
# check the initial accelerometer range
print('reading XYZ_DATA_CFG:')
print(i2c_read_acc(XYZ_DATA_CFG))
# change the accelerometer range
command(CTRL_REG1_STANDBY)
command(ACC_4G)
command(CTRL_REG_1_ACTIVE)
print('commands sent')
# check the accelerometer range
print('reading XYZ_DATA_CFG:')
print(i2c_read_acc(XYZ_DATA_CFG))
display.show(Image.MEH)
# main_text()

output:

reading PL_BF_ZCOMP :
read: b'D'
None
reading WHO_AM_I
read: b'Z'
None
reading XYZ_DATA_CFG:
read: b'\x00'
None
commands sent
reading XYZ_DATA_CFG:
read: b'\x01'
None

The onboard accelerometer has an i2c address of 0x1d. There is a good article on how to scan for and verify this address here. I set the variable ACCELEROMETER to be this value in line 4 so that I could refer to it throughout the code without having to remember the hex value. Too many hex values flying around – I’d be bound to make a mistake if I didn’t give them names.

To send a command over i2c, as shown in line 18 of the example code, you need to address the target then send the commands as a bytearray. In this case the target is the accelerometer. Typically we send two bytes to the accelerometer. The first specifies the register we want to change, the second the value we want to write to this register. For example, to set the accelerometer’s range of sensitivity, we need to set the value of the register called XYZ_DATA_CFG to the value that corresponds with the range we are after. The address of this register is 0x0e. To set the +/4G range, we want to set this register to be 0x01. Now the variable I set in line 6 should make sense. Look in the data sheet linked above for more details. Before we can change this register we have to set CTRL_REG1 to be inactive by writing 0x00 to it. After changing the XYZ_DATA_CFG register we have to set CTRL_REG1 to be active again by writing 0x01 to it. This is detailed in the accelerometer application notes which I linked at the start of this article.

If you uncomment the last line, then the raw accelerometer values will stream out. The last column are the values for the z-axis of the accelerometer. Lay the board flat on the table. With the default +/-2g range you will see the z-axis values being around +1024 or -1024 depending on if the board is face up or down. This corresponds to +/-1g on the +/-2g range. Now that the board is set to +/-4g, the values for +/-1 g will be +/-512. The maximum and minimum value for the accelerometer stays as +/-2048, but it is now spread over +/-4g. Similarly, if you go crazy and set the range to be +/-8g, then you will see +/-256 for the z-axis value from the accelerometer for the board laying flat. As you would expect, you have to wave the board harder to get it to max out when you set the sensitivity to the higher ranges compared with the default +/-2g range.

So what about the PL_BF_ZCOMP and WHO_AM_I registers that I read from in lines 43 and 45? These are two read only directories. Reading the values stored in these is a sanity check that the chip is turned on and I have working code. I read the XYZ_DATA_CFG before and after setting it to verify that the sensitivity range has been set. Read up on these registers in the data sheet.

Look at line 23. The repeat=True flag has to be set. This clears the ‘message end’ flag in the write command. The default for this flag is False, which means that the i2c write command has a ‘message end’ flag at the end of it, which terminates the operation. As we want to read from the chip in line 24, we need to not set the ‘message end’ flag. Otherwise you will just read 0xff. Can you guess why? The data line is held high for i2c, so if there is nothing coming out of the chip you are trying to read from, you just read a bunch of ‘1s’. Line 24 means ‘read 1 byte from the device with address ACCELEROMETER’.

Where I initially came unstuck was by sending data as individual bytes, using e.g. b’\x0e’ followed by b’\x02′ to try and change the XYZ_DATA_CFG register. This looks to be valid for the Adafruit implementation of micropython, but I couldn’t get it work.

XBee series 1 accelerometer sampling

XBee modules have a built in ADC, so why not sample an analog accelerometer directly? This will allow me to make a smaller wireless accelerometer that I can strap to my participants for testing with. Long term I want a microcontroller in the system for onboard signal processing. But for initial data collection, the smaller and simpler the better. Make it work. Make it fast. Make it right.. I am using the ADXL335 analog output 3-axis accelerometer connected to D0, D1 and D2 of an XBee series 1. This idea is nothing new, I got the idea for this build from a website made by Dr. Eric Ayars, Associate Professor of Physics at the California State University, Chico here. Thanks Eric! Initially I tried lashing up his design with the series 2 XBees that I had to hand. The issues with this are the two main differences that I found between the Series 1 and the Series 2 XBee ADC (analog to digital converter).

1. With the Series 2 XBee, the range of analog input that can be read by the ADC is set to be ground to 1.2V. With the Series 1 module, you set the top voltage that the ADC can sample by connecting that voltage to the VRef pin on the module. There is a VRef pin labelled on the Series 2, but it is not connected to anything. Usually you connect the voltage that you are using to power the module with (e.g. 3.3V) to the VRef pin on the Series 1 to enable the ADC to sample from ground to the supply voltage. You cannot connect a higher voltage than the supply voltage to this pin. Or the World will End. The output from the ADXL335 is centered around half of the voltage that it is powered with. In my case this is 3.3/2 = 1.65V. The output for each of the 3 accelerometers in the chip varies by 330mV/g. So the outputs will rarely dip below 1.2V and be sampled by a Series 2 XBee. Of course I could use a simple resistor network to bring the voltage output from the accelerometer down to be centered around 0.6V and be in with a chance of reading it with the XBee series 2. But this brings us on to issue 2.
 
2. The sample rate of the Series 2 XBee is lower than that of the Series 1. Using the Digi International XCTU tool for configuring the modules, with the Series 2, the fastest sample rate that I am allowed to set is 50ms. When I tested it, I was only getting about 16Hz. Thinking for a little while, I realise that the 50Hz sampling was being split across the 3 analog inputs that I am sampling (x, y and z axis). 3×16=48, so it all kind of makes sense. The Series 1 can be set to sample silly fast, down to 1ms. However, this brings us on to reading some XBee series 1 data and information sheets. This article from Digi International states that the maximum sample rate for the Series 1 is 50Hz, but it can be set to sample at up to 1KHz. I am interested in seeing just how fast this module can go…
 
The picture below shows the XBee series 1 module connected with an ADXL335 board – which is on the right of the photo. On the left there is a AAA battery connected to a DC-DC converter board, which provides an output of 3.3V for the ADXL335 and the XBee module. The same 3.3V rail is used as an input to the VREF pin on the XBee module. So the ADC should work from ground to 3.3V. I would imagine that the ADC will stall at about a diode drop (0.6V) from either limit.
I set the sample rate on the XBee 1 to be 5ms using the XCTU tool, which equates to 200Hz.
I lashed up some code based on the XBee API samples. I use Python 3, which allows me to leverage the time.perf_counter() function in lines 12 and 16 to get microsecond timing. Please see the initial code and output below.
 
from xbee import XBee
import serial
import time
PORT = '/dev/ttyUSB0'
BAUD_RATE = 115200 
# Open serial port
ser = serial.Serial(PORT, BAUD_RATE)
# Create XBee Series 1 object
xbee = XBee(ser, escaped=True)
print('created xbee at {} with baud {}'.format(PORT, BAUD_RATE))
print('listening for data...')
dt_old = time.perf_counter() 
# Continuously read and print packets
while True:
    dt_new = time.perf_counter()
    response = xbee.wait_read_frame()
    adc_dict=response['samples'][0]
    delta_millis = (dt_new-dt_old)*1000
    dt_old = dt_new
    try:
        print('{:.2f} {:.2f}'.format(delta_millis, 1000/delta_millis))
    except ZeroDivisionError as e:
        continue
    print(adc_dict['adc-0'], adc_dict['adc-1'], adc_dict['adc-2'])
ser.close()

output:

created xbee at /dev/ttyUSB0 with baud 115200
listening for data...
0.00 1428571.69
526 409 502
10.67 93.73
526 409 503
0.25 4058.74
526 411 503
10.62 94.19
522 406 500
0.40 2474.43
523 409 502
11.26 88.85
516 412 505
0.62 1604.76
523 408 502
10.65 93.86
522 407 498
0.39 2591.94
522 403 500
10.64 94.02

Ignore the first line of data, I expected that to be garbage. The lines of data should be:

adc-0, adc-1, adc-2 # which looks about right
time in ms since the last sample, resulting frequency = 1000/time in ms since last sample # these don't look about right

We should be seeing a uniform sample and frequency. But it oscillates between about 11ms and 0.5ms. Which averages to be about 6ms. For all three channels. So the ADC is working at a sample rate of around 2ms.

I modified the code to include a 100 sample averaging calculation. This is implemented using a deque data container, initialised in line 13. The sample times are added in line 24. Prior to that, the oldest one is removed in line 13. The values are averaged and printed in line 26. The try, except clause around this line are necessary as the ‘None’ values that the deque is intialised with cause the np.mean function to crash with a TypeError.


from collections import deque
import numpy as np
from xbee import XBee
import serial
import time

PORT = '/dev/ttyUSB0'
BAUD_RATE = 115200 
# Open serial port
ser = serial.Serial(PORT, BAUD_RATE)
# Create XBee Series 1 object
xbee = XBee(ser, escaped=True)
sample_deque = deque([None]*100, maxlen=100)
print('created xbee at {} with baud {}'.format(PORT, BAUD_RATE))
print('listening for data...')
dt_old = time.perf_counter() 
# Continuously read and print packets
while True:
    dt_new = time.perf_counter()
    response = xbee.wait_read_frame()
    adc_dict=response['samples'][0]
    delta_millis = (dt_new-dt_old)*1000
    sample_deque.pop()
    sample_deque.appendleft(delta_millis)
    try:
        print('{:.2f}'.format(np.mean(sample_deque)))
    except TypeError:
        continue
    dt_old = dt_new
    try:
        print('{:.2f} {:.2f}'.format(delta_millis, 1000/delta_millis))
    except ZeroDivisionError as e:
        continue
    print(adc_dict['adc-0'], adc_dict['adc-1'], adc_dict['adc-2'])
ser.close()

output after a few hundred samples:

5.44
13.52 73.96
521 404 500
5.45
2.06 485.59
523 408 502
5.44
0.91 1103.39
526 409 504
5.44
11.06 90.38
516 412 507
5.45

The data should be:

averaged interval in ms # looks about right
last sample interval in ms, frequency calculated from last interval in Hz # still oscillating
adc-0, adc-1, adc-2

The average of around 5.5ms is close enough to the programmed value of 5ms for my purposes. Why does the sample time fluctuate? Probably something to do with my code. If you have an answer, please leave it below.
The rigorous way to verify the accuracy and speed of this module is to plug in a function generator to the analog channels, record data then analyse that. How hard could that be? Errrr….. I think that what I have now is ‘good enough’ to try out shake gesture recognition.
The next step is to get an output in ‘g’ – that is units of gravity. As the sensitivity of the ADXL335 is 330mV/g with an input of 3.3V, the output is centred on half of the rail voltage and the ADC has a range of 0-1024:
g = (ADC_count-512)/102.5
I made a python lambda function to do the conversion:

g = lambda x: (x-512)/102.4

So I can output formatted accelerometer values in g by altering line 34 of the last listing to:

print('{:.2f} {:.2f} {:.2f}'.format(g(adc_dict['adc-0']), g(adc_dict['adc-1']), g(adc_dict['adc-2'])))