0

I'm trying to use an accelerometer for user input to a device. The idea is that the user input will be tied to "taps" on the accelerometer, one tap means something, two taps something else, etc.

I'm using an Adafruit Feather M0 as my microcontroller, controlling an accelerometer that feeds me x,y,z-data at a sampling rate of 100 Hz. I now then want to high pass filter the data with a cutoff frequency of 10 Hz, since taps on the accelerometer give rise to much higher frequencies than other "noise", such as movement.

The accelerometer uses a FIFO buffer to output the data, so I collect 20 samples at a time, and I want to filter those 20 samples with a high pass filter to see if there is a tap within that sampling window. I am currently using the filter function on Arduino playground, https://playground.arduino.cc/Code/Filters.

However, and here is my problem; When I implement this in my code, the filtering "restarts" each window, so that my filtered data looks like this:

[X], [Y], [Z]
0.00,0.00,0.00
-39.67,94.13,-85.18
-29.89,71.66,-64.18
-24.02,53.99,-49.08
-17.39,40.78,-37.09
-13.00,30.51,-27.74
-9.80,22.99,-20.91
-7.58,17.00,-16.17
-7.01,13.88,-14.01
-4.86,12.04,-10.53
-4.02,9.96,-8.71
-2.44,8.05,-7.04
-1.99,6.57,-5.74
-1.60,5.26,-3.80
-2.76,3.27,-4.45
-1.43,2.67,-3.63
-1.16,2.16,-2.13
-0.94,2.55,-1.72
-0.76,1.26,-0.58
-0.62,0.21,-1.29

0.00,0.00,0.00
-40.26,94.51,-84.88
-30.38,70.55,-64.78
-22.13,53.89,-48.04
-17.42,40.60,-37.69
-13.91,29.87,-27.68
-10.99,23.61,-22.67
-10.05,19.04,-16.00
-5.78,14.79,-13.94
-4.69,12.82,-11.31
-3.84,9.69,-9.27
-3.14,7.91,-7.56
-2.52,6.35,-6.08
-2.07,5.22,-4.18
-0.88,4.29,-3.43
-2.35,4.32,-3.62
-0.29,3.54,-2.97
-1.07,2.94,-3.29
-1.63,0.74,-1.02
-0.50,1.38,-1.60

0.00,0.00,0.00
-40.51,95.10,-85.37
-30.32,70.41,-65.39
-22.86,53.10,-49.31
-17.26,40.83,-35.71
-13.45,31.81,-27.81
-9.37,22.43,-20.92
-7.80,17.62,-15.74
-6.50,13.01,-13.10
-4.04,9.56,-8.16
-3.02,7.15,-6.85
-2.29,5.42,-4.43
-2.70,3.63,-4.46
-1.38,4.56,-3.61
-1.14,2.95,-2.16
-1.73,2.38,-1.75
-1.32,2.59,-2.10
-1.05,0.47,-1.67
-0.04,1.12,-1.28
-0.03,0.90,-1.03

I don't understand why this happens, and why the data in the subsequent windows aren't suppressed from the start. I have implemented a very similar solution in Python which works very well with the filtering. Have I misunderstood how Arduinos filtering functions work?

Here is my code:

// Include SPI library
#include <SPI.h>
#include <Filters.h>

// List of addresses: 7-bit adresses, 8th bit is R/W bit, and is set in the functions readRegister and writeRegister.
char X_data_low = 0x28;
char X_data_high = 0x29;
char Y_data_low = 0x2A;
char Y_data_high = 0x2B;
char Z_data_low = 0x2C;
char Z_data_high = 0x2D;

char CTRL_REG1 = 0x20; // Controls data rate selection, low power mode, and X-, Y- and Z-axis enable
char CTRL_REG1_value = 0b00101111; // 0001 1111 = 1 Hz, LP mode, XYZ enable.

char CTRL_REG5 = 0x24; // Enables FIFO mode, among others.
char CTRL_REG5_value = 0b01000000; // 0100 0000, FIFO enabled, all else is zero.

char STATUS_REG = 0x27; // Flags for new XYZ values.

char FIFO_CTRL_REG = 0x2E; // Controls FIFO mode (Bypass, FIFO, Stream, Stream-To-FIFO)
char FIFO_CTRL_REG_value = 0b01000000; // Selects FIFO mode

char FIFO_SRC_REG = 0x2F; // Watermark level, FIFO Overrun, FIFO empty, FSS. Can read FSS to know how many unread samples FIFO buffer contains currently.

//int smpls = 20; // Number of samples 

char values[240]; // buffer for storing read values.

int x_val[40]; // Arrays for storing values before filtering
int y_val[40];
int z_val[40];

float filtered_x_val[40]; // Arrays for storing values after filtering
float filtered_y_val[40];
float filtered_z_val[40];

int CS = 5; // Chip select pin

int concat_val = 0b00011111; // For concatenation with values[0] to find number of samples in FIFO buffer
int number_of_FIFO_values; // Number of FIFO samples 

// Testing Arduinos own filtering

// filters out changes slower that 10 Hz.
float filterFrequency = 10.0; 

// create a one pole (RC) lowpass filter
FilterOnePole highpassFilter_x( HIGHPASS, filterFrequency );
FilterOnePole highpassFilter_y( HIGHPASS, filterFrequency );
FilterOnePole highpassFilter_z( HIGHPASS, filterFrequency );

void setup() {
  // put your setup code here, to run once:
  // Opening pins
  pinMode(22, INPUT_PULLUP); // MISO
  pinMode(24, OUTPUT); // SCK
  pinMode(23, OUTPUT); // MOSI
  pinMode(CS, OUTPUT);  // CS
  pinMode(14, OUTPUT);  // VDD_IO, for use when no breadboard is available to connect both VDD and VDD_IO to 3.3V output
  digitalWrite(CS, HIGH);
  digitalWrite(14, HIGH);
  delay(50);

  // Communication with computer
  Serial.begin(9600);

  // SPI
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
  // Loading settings
  delay(10);
  writeRegister(CTRL_REG1,CTRL_REG1_value);
  delay(10);
  writeRegister(CTRL_REG5,CTRL_REG5_value);
  delay(10);
  writeRegister(FIFO_CTRL_REG,FIFO_CTRL_REG_value);
  delay(10);

}

void loop() {
  readRegister(0x2F,1,values);
  number_of_FIFO_values = values[0] & concat_val;

  if (number_of_FIFO_values >= 20){ // It is enough to read high values since LP mode is only 8 bits
    readRegister(0x28,120,values);
    int x_count = 0; // Counters to keep track of which value in x_val, y_val, z_val to be assigned
    int y_count = 0;
    int z_count = 0;
    for(int i=0; i<120; i++){
      if (i % 6 == 1) {
        x_val[x_count] = int(values[i]);
        if(x_val[x_count] > 127)
          x_val[x_count] -= 256;
        x_count += 1;
        }
      if (i % 6 == 3) {
        y_val[y_count] = int(values[i]);
        if(y_val[y_count] > 127)
          y_val[y_count] -= 256;
        y_count += 1;
        }
      if (i % 6 == 5) {
        z_val[z_count] = int(values[i]);
        if(z_val[z_count] > 127)
          z_val[z_count] -= 256;
        z_count += 1;
        }                
      }
    Serial.print('\n');
    filtering(x_val,y_val,z_val);
    writeRegister(FIFO_CTRL_REG,0b00000000);
    delay(10);
    writeRegister(FIFO_CTRL_REG,FIFO_CTRL_REG_value);
    delay(10);
  }
}

void filtering(int x_arr[], int y_arr[], int z_arr[]){
    for(int i=0; i<20; i++) {
      highpassFilter_x.input(x_arr[i]);
      highpassFilter_y.input(y_arr[i]);
      highpassFilter_z.input(z_arr[i]);
      filtered_x_val[i] = highpassFilter_x.output();
      filtered_y_val[i] = highpassFilter_y.output();
      filtered_z_val[i] = highpassFilter_z.output();
      Serial.print(filtered_x_val[i]);
      Serial.print(",");
      Serial.print(filtered_y_val[i]);
      Serial.print(",");
      Serial.print(filtered_z_val[i]);
      Serial.print('\n');  
    }
  }

//This function will write a value to a register on the sensor.
// Parameters:
//  char registerAddress - The register to write a value to
//  char value - The value to be written to the specified register.
// The bits to specify write or read operation on slave device, and single
// or multiple register reading in reading operation, are included in the 
// registeraddress.
// As defined by protocol, the MSB of the address indicates write/read, and the following bit indicate single/multiple reading. Cause ADXL345 support Byte operation, 
// The MSB is the 8th bit, the following bit is the 7th bit. (note: if indicating multiple bits reading, ADXL345 will increasing the register address automatically to the
// next successive register like a pointer. 
void writeRegister(char registerAddress, char value){
  //Set Chip Select pin low to signal the beginning of an SPI packet.
  digitalWrite(CS, LOW);
  //Transfer the register address over SPI.
  // All ADXL345 register address are smaller than 0100 0000, all are like 00** ****, so that any register address of ADXL345 can be used directly for register write operation
  // without any change on the MSB and its following bit, but the case for reading operation is the opposite.
  // Might need a delay here between CS going low and data transfer.
  SPI.transfer(registerAddress);
  //Transfer the desired register value over SPI.
  // ignored the reture of SPI.transfer in writer operation
  SPI.transfer(value);
  //Set the Chip Select pin high to signal the end of an SPI packet.
  digitalWrite(CS, HIGH);
}

//This function will read a certain number of registers starting from a specified address and store their values in a buffer.
//Parameters:
//  char registerAddress - The register addresse to start the read sequence from.
//  int numBytes - The number of registers that should be read.
//  char * values - A pointer to a buffer where the results of the operation should be stored.
void readRegister(char registerAddress, int numBytes, char * values){
  //Since we're performing a read operation, the most significant bit (bit 7(counting starts from bit 0)) of the register address should be set high.
  char address = 0x80 | registerAddress;
  //If we're doing a multi-byte read, bit 6 (counting starts from bit 0)needs to be set high as well.
  if(numBytes > 1)address = address | 0x40;

  //Set the Chip select pin low to start an SPI packet.
  digitalWrite(CS, LOW);
  //Transfer the starting register address that needs to be read.
  SPI.transfer(address);
  //Continue to read registers until we've read the number specified, storing the results to the input buffer.
  //SPI is full duplex transsmission. Every signle time SPI.transfer can and can only transmit 8 bits. the input parameter will be delived to slave, and it's return will be 
  // the data that master device and get from the slave device. The data returned during master device's write operation makes no sense, hence always been ignored by not 
  // doing any assignment operation.
  for(int i=0; i<numBytes; i++){
    values[i] = SPI.transfer(0x00);
  }
  //Set the Chips Select pin high to end the SPI packet.
  digitalWrite(CS, HIGH);
}
4

This filter library is sensitive to the timing of you calling FilterOnePole::input(). It assumes that the delays between consecutive calls are the same as the delays between the samples being measured. C.f. the first statement in the method, which is a call to micros(). The pauses between your 20-sample buffers are then interpreted like long times during which the input only changed very little.

If you are sampling at a constant data rate, I recommend you reimplement the filter yourself: it is simple enough and it will be way more efficient than the one you found on Playground.


Edit: Here is a simple implementation of a first-order high-pass filter like the one in the library, but using a constant sampling rate:

class HigPassFilter
{
public:
    HigPassFilter(float reduced_frequency)
    : alpha(1-exp(-2*M_PI*reduced_frequency)), y(0) {}
    float operator()(float x) {
        y += alpha*(x-y);
        return x-y;
    }
private:
    float alpha, y;
};

You would use it like this:

// Create the filters.
HigPassFilter highpassFilter_x(cutoffFrequency/samplingFerquency);
HigPassFilter highpassFilter_y(cutoffFrequency/samplingFerquency);
HigPassFilter highpassFilter_z(cutoffFrequency/samplingFerquency);

// Apply them.
filtered_x_val[i] = highpassFilter_x(x_arr[i]);
filtered_y_val[i] = highpassFilter_y(y_arr[i]);
filtered_z_val[i] = highpassFilter_z(z_arr[i]);
  • Huge thanks! I am however quite the beginner in C++, so I'm wondering that this line in your code do? Specifically, what is alpha in this context? : alpha(1-exp(-2*M_PI*reduced_frequency)), y(0) {} – Lars Petersson Feb 12 '18 at 13:00
  • @LarsPetersson: It initializes the filter constant alpha to the expression 1-exp(...) and the variable y to zero. This α would be equivalent to 1−ampFactor on the library, which is initialized here. The y is the initial value, which also defaults to zero in that library. – Edgar Bonet Feb 12 '18 at 13:09
  • @LarsPetersson, it's called an "initialization list". Google it, and see here for one example description: cprogramming.com/tutorial/initialization-lists-c++.html – Gabriel Staples Feb 22 '18 at 2:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.