filtering accelerometer data

I'm developing an application with an accelerometer and have been experimenting with various methods of filtering the data retrieved from the chip.

I'm using an ADXL335 chip (3 axis analog output) with an adafruit breakout board. (duemillanove, Arduino 0017, MacOS)

As expected, the data seems to be fairly noisy and my testing by letting it run just sitting on a table still gives values from analogRead () that fluctuate +/- 2. That's +/- 2/1024 of 5V = 9.7 mV. I believe that this will not be acceptable for my application. (I'll need to calculate tilt angles from the data.)

I have implemented a running average filter on the received data, with a ring buffer scheme and various size buffers (8, 16, 32, and upwards), but this does not remove the noise.

I've also been researching some of the signal processing theory to explore other filter schemes, but wondered if anyone has any experience with this kind of data filtering.

Any advice or pointers most appreciated.

.andy

Trying to measure tilt with a +/- 3g accelerometer and a 10 bit ADC is not an ideal setup.

If your 1024 count range is spread out over 6g, 1g is covered by just 170 counts.
So, a tilt of just 1 degree gives a shade under 3 counts (sin (1) * 170), probably swamped by noise.

Maybe try a more sensitive accelerometer, or an external A/D with more bits.

[EDIT] {smacks forehead} D'oh! Or you could try using a lower analogue reference voltage, one slightly higher than your zero acceleration point (Vcc / 2 for the accelerometer). This assumes you don't want to measure very high angles of tilt.

Thanks for the cogent analysis. But as I dig deeper, I think it's worse than that, no? Here's my reasoning; see what you think.

The Arduino ADC converts a 5V analog signal into a 10-bit digital value (reference). That means each analogRead count measures .2048 mV [1024/5000].

The ADXL running at Vs = 3V has a sensitivity of 300 mV/g, a zero g bias of half of Vs, and the data sheet says a typical range of +/- 3.6g (not 3.0g as I thought). So that means it will generate output voltages for -3.6 to +3.6g in the range of 420 - 2580 mV. [0g = 1500 mV; 3.6g = 1500mV + 300mV * 3.6g = 2580]

When the Arduino ADC converts those voltages to digital values, they will range from 86 to 528, with 0g = 307. This, by my reasoning, is because the ADC takes input from 0 - 5000mV. [528 = 2580mV * .2048 count/mV] This means a range of 528 - 86 = 442 counts over 7.2g = 61 counts per 1g.

And since the ADXL is ratiometric, it says the output voltage is proportional to the input voltage. Doesn't that mean that if I give it Vs = 2V, then the output range over the 7.2g will be even less and therefore the Arduino will have a smaller number of counts per g?

I made a little picture to illustrate all of this, here.

In any case, I think your point about this accelerometer not being the right choice for my tilt app is correct. I only need to detect change in about 10 degree increments, from 0 to 45 or so, but I still think it would be questionable. And using an ADC with more bits would only solve the noise problem, not the range.

I see that SparkFun has a number of other ADXL accelerometers in the +/- 1.2 and 1.5 range. Perhaps one of these might be a better choice for me?

Thanks again for the good counsel.

.ad

It should be able to give you a stable enough value for the kind of resolution you said you need.

I have noticed that you haven't posted any of your code yet. Taking a look at that might help us figure out why your noise filtering code isn't working they way you expect.

That means each analogRead count measures .2048 mV [1024/5000].

No, each count measures 5/1024 = 4.88mV.
Or, if you prefer, in g 7.2/1024 = 7.03mg.
Put more simply, 1g gives 1024/ 7.2 = 142.2 counts.
Now, sin(1) * 142.2 = 2.47.
In other words, a tilt of 1 degree from horizontal will give a reading change of 2 counts.
Five degrees gives 12 counts.

You can increase sensitivity by using you 3.3V as Aref for the Arduino chip. This way you correct drift or noise between the 2 power supply.
As you are searching mV you should keep power and output of accelerometer AND Arduino really clean (power filtering, output with a ground screen).
Every manufacturers tell their users to halt the processor while a conversion is running on the ADC. It helps reducing the noise.

No, each count measures 5/1024 = 4.88mV.

Oops, right. A reciprocal brain slip. And thanks for the correction of my analysis. I see now.

In other words, a tilt of 1 degree from horizontal will give a reading change of 2 counts.
Five degrees gives 12 counts.

It seems that should be good enough for my app.

I have noticed that you haven't posted any of your code yet. Taking a look at that might help us figure out why your noise filtering code isn't working they way you expect.

My test program is below. All critique appreciated. I ran it and have put a file with the serial output here.

I had the accelerometer breakout board plugged into a small breadboard not mounted on the Arduino board. I let it just run without touching it, then towards the end picked it up and rotated it around and set it back down.

The noise, with the averaging, seems to be +/- 1 count, which is pretty good I think, since that is less than 1 degree of tilt. To be expected? Should I not worry about trying to filter the data to be perfectly flat?

Thanks to all for all the generous advice. Now on to the real math to actually do stuff with the data!

.ad

/* ADXL335test6
 Test of ADXL335 accelerometer
 Andy Davidson
 */

const boolean debugging = true;    // whether to print debugging to serial output
const boolean showBuffer = false;  // whether to dump details of ring buffer at each read

const int xPin = 3;  // analog: X axis output from accelerometer
const int yPin = 1;  // analog: Y axis output from accelerometer
const int zPin = 2;  // analog: Z axis output from accelerometer
const int led = 13;  // just to blink a heartbeat while running

const int totalAxes = 3;  // for XYZ arrays: 0=x, 1=y, 2=z

const int baseSamples = 1000;  // number of samples to average for establishing zero g base
const int bufferSize = 16;     // number of samples for buffer of data for running average
const int loopBlink = 100;     // number of trips through main loop to blink led

// array of pin numbers for each axis, so the constants above can be chnaged with impunity
const int pin [totalAxes] = {
  xPin, yPin, zPin};


// base value for each axis - zero g offset (at rest when sketch starts)
int base [totalAxes];

// ring buffer for running average of data, one for each axis, each with <bufferSize> samples
int buffer [totalAxes] [bufferSize];

// index into ring buffer of next slot to use, for each axis
int next [totalAxes] = {
  0,0,0};

// current values from each axis of accelerometer
int curVal [totalAxes];

// count of trips through main loop, modulo blink rate
int loops = 0;



void setup() {


  long sum [totalAxes]= {  // accumulator for calculating base value of each axis
    0,0,0        };


  Serial.begin   (9600);
  Serial.println ("***");

  // initialize all pins
  pinMode (led, OUTPUT);
  for (int axis=0; axis<totalAxes; axis++)
    pinMode (pin [axis], INPUT);    // not necessary for analog, really

  // read all axes a bunch of times and average the data to establish zero g offset
  // chip should be at rest during this time
  for (int i=0; i<baseSamples; i++) 
    for (int axis=0; axis<totalAxes; axis++)
      sum [axis] += analogRead (pin [axis]);
  for (int axis=0; axis<totalAxes; axis++)
    base [axis] = round (sum [axis] / baseSamples);

  // and display them
  Serial.print ("*** base: "); 
  for (int axis=0; axis<totalAxes; axis++) {
    Serial.print (base [axis]); 
    Serial.print ("\t");
  }
  Serial.println ();
  Serial.println ("***");

  // initialize the ring buffer with these values so the averaging starts off right
  for (int axis=0; axis<totalAxes; axis++) 
    for (int i=0; i<bufferSize; i++)
      buffer [axis] [i] = base [axis];

  // light up the led and wait til the user is ready to start (sends anything on serial)
  // so that the base values don't immediately shoot off the top of the serial window
  digitalWrite (led, HIGH);
  while (!Serial.available())  
    /* wait for <RETURN> */    ;
  digitalWrite (led, LOW);

}



void loop() {

  //increment the loop counter and blink the led periodically

  loops = (loops + 1) % loopBlink;
  digitalWrite (led, loops == 0);

  // get new data from each axis by calling a routine that returns
  // the running average, instead of calling analogRead directly

  for (int axis=0; axis<totalAxes; axis++) {
    curVal [axis] = getVal (axis, showBuffer);
    if (debugging) {
      Serial.print (curVal [axis]); 
      Serial.print ("\t");
    } 
  }
  if (debugging)    
    Serial.println ();

  // here we will do all of the real work with curVals

}



int getVal (int axis, boolean show) {

  // returns the current value on <axis>, averaged across the previous <bufferSize> reads
  // print details if <show> is true


  long sum;  // to hold the total for aaveraging all values in the buffer


  // read the data into the next slot in the buffer and stall for a short time
  // to make sure the ADC can cleanly finish multiplexing to another pin

  buffer [axis] [next [axis]] = analogRead (pin [axis]);
  delay (10);    // probably not necessary given the stuff below

  // display the buffer if requested

  if (show) {
    for (int i=0; i<bufferSize; i++) {
      if (i == next [axis]) Serial.print ("*");
      Serial.print (buffer [axis] [i]); 
      Serial.print (" ");
    }
    Serial.println ();
  }

  // bump up the index of the next available slot, wrapping around

  next [axis] = (next [axis] + 1) % bufferSize;

  // add up all the values and return the average, 
  // taking into account the offset for zero g base

  sum = 0;
  for (int i=0; i<bufferSize; i++) 
    sum += buffer [axis] [i];

  return (round (sum / bufferSize) - base [axis]);

}

It's a little inefficient. You're stepping through the whole array and adding up all the values each time you call for a reading. You could maintain a variable and each time you put a new value into the array, subtract the old one and add the new one to that variable.

You can set up an ultra-simple averaging method though. Just make a global float, say filteredADC0. Then whenever you sample the ADC, you do it like this:

filteredADC0 = 0.99 * filteredADC0 + 0.01 * analogRead(0);

And that's kind of like a 100-element running average.

You might also be picking up noise from various sources if you have long wires, etc. I would also bet you're seeing some bobble from doing all that math as integers. If the accelerometer is on a threshold of something not perfectly divisible you're going to see some LSB wiggles.

Or you could probably maintain a bit more precision with a 16.16 fixed point format:

unsigned long filteredADC0 = 0;
...
filteredADC0 = (filteredADC0 - (filteredADC0 >> 6)) + ((unsigned long) analogRead(0) << 10);

This example will give a rolling average over 64 samples.

Question from the peanut gallery: (great tips in this thread btw - thanks)

Or you could probably maintain a bit more precision with a 16.16 fixed point format:

This example will give a rolling average over 64 samples.

I don't quite understand how that code extrapolates out to 64 samples - could you expand on how that comes out to 64 samples for me ?

The code simply takes 1/64 th of the current average off the current average, and adds the current sample in divided by 64.
(it doesn't look like it is divided, but it is!)

The integer part of the result can be obtained by dividing
filteredADC0 by 65536, or in other words, filteredADC0 >>= 16;

Think of it like Macegr's example - "x * 0.99" is the same as "x - (x * 0.01)"

thanks for that - the bitmath confused me for a bit...

Thanks very much to all of you who provide such informative advice.

I am constantly amazed at and appreciative of the generosity of all the experienced people who inhabit these forums and seem always willing and able to share their knowledge.

I am a teacher, so I understand the desire to help others, but many of you go vastly above and far beyond what I would expect in a volunteer community, seemingly without limits on patience or stamina.

It is very inspiring to me as an educator.

Grazie mille!

.andy

You may want to read this http://didier.longueville.free.fr/arduinoos/?p=1389 :wink: