XBee series 1 accelerometer sampling
Last updated: Jan 24, 2023
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 use 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).
-
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.
-
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). 3x16=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 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:
time since last sample in ms, frequency of sampling
adc-0, adc-1, adc-2
in ms since the last sample, resulting frequency = 1000/time in ms since last sample
We should see 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 filter. 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
The data in the string is:
averaged interval in ms, last sample interval in ms, frequency calculated from last interval in Hz, 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'])))