Simple Moving Average Filter

A visualised example of a averaging filter applied to noisy data. Jagged blue line represents raw data. Smooth, superimposed black curve represents averaged filter.
Image: http://www.biomecardio.com/matlab/sffilt.html

Filters are integral to working with sensors, and especially sensors that are naturally prone to high levels of fluctuation, i.e. noise. You can often see this when you trace the output of a noisy analogue reading to the Serial Port.

Examples of ‘noisy’ analogue sensors:

How do we ‘massage’ these raw sensor values to get a smoother response over time? We can implement signal filters in code. For the purpose of this studio, let's look at the most basic, but highly effective Simple Moving Average (SMA) filter.

Here's a summary of what this filter does:

  1. Store the last n number of readings of the sensor into an array (i.e. a list of numbers stored in memory)
  2. Find the average of these n readings – and use this averaged reading
  3. Continue to maintain the last n number of readings constantly (hence moving average)
  4. Repeat from step 2

It will therefore make sense that the size of the array, i.e. n, will impact the rate of change of the averaged reading. A higher n value will result in slower, less-responsive sensor reading but one that is extremely stable. Depending on your sensor and application scenario, you will then experiment with raising/lowering this n value to strike the right balance between responsiveness and stability.

Note that you can also design some filters using hardware (look for RC or LC circuits as an example), instead of writing code. The benefit of hardware filters is zero coding, but a deeper understanding of electrical characteristics of inductors, resistors and capacitors are required. This recipe will focus on software signal filters that we can run on the Photon.

Figure out how to use the above code snippet into your existing project. Noisy sensor no more!


Libraries Used

(learn how to import them in the Build IDE):


Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
simple moving average filter

The MIT License (MIT)
Copyright © 2017 Chuan Khoo

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/


#define FILTERSIZE  10    // adjust this to set the aggressiveness of your filter
#define INITIAL_VAL 0     // filter should have an initial value   

int smaBuf[FILTERSIZE];
int smaIdx = 0;
int smaTotal = INITIAL_VAL * FILTERSIZE;
int smaAvg = 0;

int rawReading;

void setup() {
    Serial.begin(9600);
    // pre-populate the filter buffer with INITIAL_VAL
    for(int i=0; i<FILTERSIZE; i++) {
        smaBuf[i] = INITIAL_VAL;
    }
}


void loop() {
    // here we read a fictitious analog sensor on A0
    rawReading = analogRead(A0);    

    // an example of using constrain to crop outlying values not reported by sensor
    rawReading = constrain(rawReading, 1000, 3000);

    runSMA();   // run the runSMA() function (below)
}


// a custom function that we call from loop() – keeps the main loop() uncluttered!
void runSMA() {
    smaTotal -= smaBuf[smaIdx];
    smaBuf[smaIdx] = rawReading;
    smaTotal += smaBuf[smaIdx];
    smaIdx++;
    if (smaIdx >= FILTERSIZE)
        smaIdx=0;
    smaAvg = smaTotal / FILTERSIZE;

    Serial.printlnf("avg: %i", smaAvg);
}