Filters

Spring 2020

The questions below are due on Tuesday February 23, 2021; 11:59:00 PM.
 
You are not logged in.

If you are a current student, please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.csail.mit.edu) to authenticate, and then you will be redirected back to this page.
Back to Exercise 03

Music for this Exercise

1) Writing an Averaging Filter

In Week 1 we talked about difference equations. In Lab 02A we then implemented a 3-way running average filter to smooth out our accelerometer readings a little bit. In difference equation form this took on the form:

y[n] = \frac{1}{3}x[n] + \frac{1}{3}x[n-1] + \frac{1}{3}x[n-2]

We could also say this is a "second-order" moving average filter.

What we'd now like to do is write some code that will allow us to write an arbitrarily large moving average filter of mth order (which would correspond to a m+1-way moving average) such that:

y[n] = \sum_{p=0}^{m}\frac{1}{1+m}x[n-p]

which for values of m\geq2 could be expanded out to look like this generally:

y[n] = \frac{1}{1+m}x[n] + \frac{1}{1+m}x[n-1]...\frac{1}{1+m}x[n-m]

For practical purposes, we'll restrict the range of m to be 0 \leq m \lt 50

Write a function averaging_filter that implements a variable-length moving average. This function should be more flexible/versatile, however since we can specify how many time steps back we use!

Our first attempt at a generic averaging filter will be used like this, for example:

uint32_t time_counter;
const uint32_t DT = 50; //sample period
const uint32_t FILTER_ORDER = 13; //size of filter
float values[50]; //used for remembering an arbitrary number of old previous values
void setup(){
  Serial.begin(115200);
  // Initialize vs to all zeros (to be safe)
  // Could also do to non-zero values if desired!
  for (int i=0; i<50; i++){ 
    values[i] = 0.0;
  }
  time_counter = millis();
}

void loop(){
  float input = some_function(); //get some input value from a function that returns measurements (maybe IMU for example)
  // Process the new input, and get a new output
  // In this case, we use a FILTER_ORDER moving average, but it can be anything
  float average = averaging_filter(input, values, FILTER_ORDER);
  Serial.println(average);
  while(millis()-time_counter < DT);
  time_counter = millis();
}

Concretely, we're going to call the function repeatedly, giving it a new input (x) each time.

The averaging_filter function should take in three inputs:

  • float input: the current input to the averaging filter, which has been x in our discussion so far.
  • float* stored_values: a pointer to a float array that is global in scope and at the time of the first function call has been initialized to all zeros. You may assume the array has a length of at least 50. You can assume that in normal use, no outside function or block of code will make changes to this array and that your modifications will "live on" between calls to averaging_filter.
  • int order: an integer indicating the "order" of the filter. For example a 0 should result in simply y[n] = x[n], a 1 should result in y[n] = 0.5x[n] + 0.5x[n-1], and so on. order should not be expected to go above 49 in value. The value of order will be the same in each function call to averaging_filter, within a given test case.

Make sure you use the values pointed to as inputs and do not assume global variable names. Doing this will allow us to potentially reuse this function so if we set up two global variable pairs in the following way:

float values1[50];
float values2[50];

we could proceed to do the following:

float output1 = averaging_filter(analogRead(A3), values1, 3); //step/update filter 1 with analog measurement
float output2 = averaging_filter(analogRead(A3), values2, 9); //step/update filter 2 with analog measurement

...code reusability is a virtue!

Your function should return the current output (y which is y[n]). The details of exactly how to use stored_values and are up to you!

float averaging_filter(float input, float* stored_values, int order) { //your code here }

2) A Different Way...

Chances are your previous solution required a lot of copying on each step. Most likely, each time through the loop you shifted values back (or forward) through the array, which can take extra time.

It probably looked something like the following:

One way of how a moving average filter could live in memory

If we were to expand this system to work with something like you'd see in research industry, let's say a 4096-point running average or something, the number of copies you have to do on each call to the function will be enormous.

One way around this is to change how you use the array. Instead of shifting the contents of the array, instead shift your frame of reference.

A different way of doing a moving average using an additional variable to help give a frame of reference for how to interpret data.

uint32_t time_counter;
const uint32_t DT = 50; //sample period
const uint32_t FILTER_ORDER = 13; //size of filter
float values[50]; //used for remembering an arbitrary number of old previous values
int indx = 0;
void setup(){
  Serial.begin(115200);
  // Initialize vs to all zeros (to be safe)
  // Could also do to non-zero values if desired!
  for (int i=0; i<50; i++){ 
    values[i] = 0.0;
  }
  time_counter = millis();
}

void loop(){
  int input = some_function(); //get some input value from a function that returns measurements (maybe IMU for example)
  // Process the new input, and get a new output
  // In this case, we use a FILTER_ORDER moving average, but it can be anything
  float average = averaging_filter(input, values, FILTER_ORDER, &amp;indx);
  Serial.println(average);
  while(millis()-time_counter < DT);
  time_counter = millis();
}

Concretely, we're going to call the function repeatedly, giving you a new input (x) each time.

The averaging_filter function should take in four inputs:

  • float input: the current input to the averaging filter, which has been x in our discussion so far.
  • float* stored_values: a pointer to a float array that is global in scope and at the time of the first function call has been initialized to all zeros. You may assume the array has a length of at least 50. We make no modifications to this array, and your modifications will "live on" between calls to averaging_filter.
  • int order: an integer indicating the "order" of the filter. For example a 0 should result in simply y[n] = x[n], a 1 should result in y[n] = 0.5x[n] + 0.5x[n-1], and so on. order should not be expected to go above 49 in value. The value of order will be the same in each function call to averaging_filter, within a given test case.
  • int* index: (NEW VARIABLE FROM BEFORE) A pointer to another global variable to use as you see fit. It will be initialized to zero. We make no modifications to this variable, and your modifications will "live on" between calls to averaging_filter.

Make sure you use the values pointed to as inputs and do not assume global variable names. Doing this will allow us to potentially reuse this function so if we set up two global variable pairs in the following way:

float values1[50];
int index1 = 0;
float values2[50];
int index2 = 0;

we could proceed to do the following:

float output1 = averaging_filter(analogRead(A3), values1, 3, &amp;index1);
float output2 = averaging_filter(analogRead(A3), values2, 9, &amp;index2);

...code reusability is a virtue!

Your function should return the current output (y which is y[n]).

the outputs of the checkers below will append small array checks to the end of the output stream to make sure you're using the array's correctly.

float averaging_filter(float input, float* stored_values, int order, int* index) { //your code here }

Back to Exercise 03