37

I'm looking for a time and memory efficient solution to calculate a moving average in C. I need to avoid dividing because I'm on a PIC 16 which has no dedicated division unit.

At the moment, I just store all values in a ring buffer and simply store and update the sum each time a new value arrives. This is really efficient, but unfortunately uses most of my available memory...

Kevin Vermeer
  • 20,067
  • 8
  • 57
  • 102
sensslen
  • 473
  • 1
  • 5
  • 8
  • 3
    I don't think there's any more space efficient way to do this. – Rocketmagnet Apr 20 '12 at 07:44
  • 4
    @JobyTaffey well, it's a quite widely used algorithm on control systems, and it requires dealing with limited hardware resources. So I think he'll find more help here than on SO. – clabacchio Apr 20 '12 at 11:52
  • 3
    @Joby: There are some wrinkles about this question that are relevant to small resource-limited systems. See my answer. You'd do this very differently on a large system like the SO folks are used to. This has come up a lot in my experience of designing electronics. – Olin Lathrop Apr 20 '12 at 12:35
  • 1
    I agree. This is quite appropriate for this forum, as it relates to embedded systems. – Rocketmagnet Apr 20 '12 at 12:48
  • I retract my objection – Toby Jaffey Apr 20 '12 at 12:57

9 Answers9

62

As others have mentioned, you should consider a IIR (infinite impulse response) filter rather than the FIR (finite impulse response) filter you are using now. There is more to it, but at first glance FIR filters are implemented as explicit convolutions and IIR filters with equations.

The particular IIR filter I use a lot in microcontrollers is a single pole low pass filter. This is the digital equivalent of a simple R-C analog filter. For most applications, these will have better characteristics than the box filter that you are using. Most uses of a box filter that I have encountered are a result of someone not paying attention in digital signal processing class, not as a result of needing their particular characteristics. If you just want to attenuate high frequencies that you know are noise, a single pole low pass filter is better. The best way to implement one digitally in a microcontroller is usually:

FILT <-- FILT + FF(NEW - FILT)

FILT is a piece of persistant state. This is the only persistant variable you need to compute this filter. NEW is the new value that the filter is being updated with this iteration. FF is the filter fraction, which adjusts the "heaviness" of the filter. Look at this algorithm and see that for FF = 0 the filter is infinitely heavy since the output never changes. For FF = 1, it's really no filter at all since the output just follows the input. Useful values are in between. On small systems you pick FF to be 1/2N so that the multiply by FF can be accomplished as a right shift by N bits. For example, FF might be 1/16 and the multiply by FF therefore a right shift of 4 bits. Otherwise this filter needs only one subtract and one add, although the numbers usually need to be wider than the input value (more on numerical precision in a separate section below).

I usually take A/D readings significantly faster than they are needed and apply two of these filters cascaded. This is the digital equivalent of two R-C filters in series, and attenuates by 12 dB/octave above the rolloff frequency. However, for A/D readings it's usually more relevant to look at the filter in the time domain by considering its step response. This tells you how fast your system will see a change when the thing you are measuring changes.

To facilitate designing these filters (which only means picking FF and deciding how many of them to cascade), I use my program FILTBITS. You specify the number of shift bits for each FF in the cascaded series of filters, and it computes the step response and other values. Actually I usually run this via my wrapper script PLOTFILT. This runs FILTBITS, which makes a CSV file, then plots the CSV file. For example, here is the result of "PLOTFILT 4 4":

The two parameters to PLOTFILT mean there will be two filters cascaded of the type described above. The values of 4 indicate the number of shift bits to realize the multiply by FF. The two FF values are therefore 1/16 in this case.

The red trace is the unit step response, and is the main thing to look at. For example, this tells you that if the input changes instantaneously, the output of the combined filter will settle to 90% of the new value in 60 iterations. If you care about 95% settling time then you have to wait about 73 iterations, and for 50% settling time only 26 iterations.

The green trace shows you the output from a single full amplitude spike. This gives you some idea of the random noise suppression. It looks like no single sample will cause more than a 2.5% change in the output.

The blue trace is to give a subjective feeling of what this filter does with white noise. This is not a rigorous test since there is no guarantee what exactly the content was of the random numbers picked as the white noise input for this run of PLOTFILT. It's only to give you a rough feeling of how much it will be squashed and how smooth it is.

PLOTFILT, maybe FILTBITS, and lots of other useful stuff, especially for PIC firmware development is available in the PIC Development Tools software release at my Software downloads page.

In addition, a web-based port of PLOTFLIT can be found here.

Added about numerical precision

I see from the comments and now a new answer that there is interest in discussing the number of bits needed to implement this filter. Note that the multiply by FF will create Log2(FF) new bits below the binary point. On small systems, FF is usually chosen to be 1/2N so that this multiply is actually realized by a right shift of N bits.

FILT is therefore usually a fixed point integer. Note that this doesn't change any of the math from the processor's point of view. For example, if you are filtering 10 bit A/D readings and N = 4 (FF = 1/16), then you need 4 fraction bits below the 10 bit integer A/D readings. One most processors, you'd be doing 16 bit integer operations due to the 10 bit A/D readings. In this case, you can still do exactly the same 16 bit integer opertions, but start with the A/D readings left shifted by 4 bits. The processor doesn't know the difference and doesn't need to. Doing the math on whole 16 bit integers works whether you consider them to be 12.4 fixed point or true 16 bit integers (16.0 fixed point).

In general, you need to add N bits each filter pole if you don't want to add noise due to the numerical representation. In the example above, the second filter of two would have to have 10+4+4 = 18 bits to not lose information. In practise on a 8 bit machine that means you'd use 24 bit values. Technically only the second pole of two would need the wider value, but for firmware simplicity I usually use the same representation, and thereby the same code, for all poles of a filter.

Usually I write a subroutine or macro to perform one filter pole operation, then apply that to each pole. Whether a subroutine or macro depends on whether cycles or program memory are more important in that particular project. Either way, I use some scratch state to pass NEW into the subroutine/macro, which updates FILT, but also loads that into the same scratch state NEW was in. This makes it easy to apply multiple poles since the updated FILT of one pole is the NEW of the next one. When a subroutine, it's useful to have a pointer point to FILT on the way in, which is updated to just after FILT on the way out. That way the subroutine automatically operates on consecutive filters in memory if called multiple times. With a macro you don't need a pointer since you pass in the address to operate on each iteration.

Code Examples

Here is a example of a macro as described above for a PIC 18:

////////////////////////////////////////////////////////////////////////////////
//
//   Macro FILTER filt
//
//   Update one filter pole with the new value in NEWVAL.  NEWVAL is updated to
//   contain the new filtered value.
//
//   FILT is the name of the filter state variable.  It is assumed to be 24 bits
//   wide and in the local bank.
//
//   The formula for updating the filter is:
//
//     FILT <-- FILT + FF(NEWVAL - FILT)
//
//   The multiply by FF is accomplished by a right shift of FILTBITS bits.
//
/macro filter
  /write
         dbankif lbankadr
         movf    [arg 1]+0, w ;NEWVAL <-- NEWVAL - FILT
         subwf   newval+0
         movf    [arg 1]+1, w
         subwfb  newval+1
         movf    [arg 1]+2, w
         subwfb  newval+2

/write /loop n filtbits ;once for each bit to shift NEWVAL right rlcf newval+2, w ;shift NEWVAL right one bit rrcf newval+2 rrcf newval+1 rrcf newval+0 /endloop

/write movf newval+0, w ;add shifted value into filter and save in NEWVAL addwf [arg 1]+0, w movwf [arg 1]+0 movwf newval+0

     movf    newval+1, w
     addwfc  [arg 1]+1, w
     movwf   [arg 1]+1
     movwf   newval+1

     movf    newval+2, w
     addwfc  [arg 1]+2, w
     movwf   [arg 1]+2
     movwf   newval+2

/endmac

And here is a similar macro for a PIC 24 or dsPIC 30 or 33:

////////////////////////////////////////////////////////////////////////////////
//
//   Macro FILTER ffbits
//
//   Update the state of one low pass filter.  The new input value is in W1:W0
//   and the filter state to be updated is pointed to by W2.
//
//   The updated filter value will also be returned in W1:W0 and W2 will point
//   to the first memory past the filter state.  This macro can therefore be
//   invoked in succession to update a series of cascaded low pass filters.
//
//   The filter formula is:
//
//     FILT <-- FILT + FF(NEW - FILT)
//
//   where the multiply by FF is performed by a arithmetic right shift of
//   FFBITS.
//
//   WARNING: W3 is trashed.
//
/macro filter
  /var new ffbits integer = [arg 1] ;get number of bits to shift

/write /write " ; Perform one pole low pass filtering, shift bits = " ffbits /write " ;"

     sub     w0, [w2++], w0 ;NEW - FILT --&gt; W1:W0
     subb    w1, [w2--], w1

     lsr     w0, #[v ffbits], w0 ;shift the result in W1:W0 right
     sl      w1, #[- 16 ffbits], w3
     ior     w0, w3, w0
     asr     w1, #[v ffbits], w1

     add     w0, [w2++], w0 ;add FILT to make final result in W1:W0
     addc    w1, [w2--], w1

     mov     w0, [w2++]  ;write result to the filter state, advance pointer
     mov     w1, [w2++]

/write /endmac

Both these examples are implemented as macros using my PIC assembler preprocessor, which is more capable than either of the built-in macro facilities.

flaviut
  • 476
  • 5
  • 19
Olin Lathrop
  • 313,258
  • 36
  • 434
  • 925
  • 1
    +1 -- right on the money. The only thing I'd add, is that moving average filters do have their place when performed synchronously to some task (like producing a drive waveform to drive an ultrasound generator) so that they filter out harmonics of 1/T where T is the moving average time. – Jason S Apr 20 '12 at 12:45
  • You write "The red trace is the unit impulse response [...]". Shouldn't it be "step response" like the labeling of your graph? -- Anyway, nice answer +1 – PetPaulsen Apr 20 '12 at 13:15
  • @Jason: Yes, I agree that box filters have their uses due to the comb nature of the frequency response. But, most uses of them I see is as simple low pass filters to reduce high frequencies in general. I don't know why, but a box filter seems to be the knee jerk reaction of those that didn't pay attention in digital signal processing class. Hearing words like "moving average" with no mention of "convolution", "box filter", or "FIR", is usually a warning of this case. – Olin Lathrop Apr 20 '12 at 13:21
  • @PetPaulsen: Oops, good catch. Fixed. – Olin Lathrop Apr 20 '12 at 13:24
  • @Olin -- yup, I'm with you there. I don't want to know how much of the circuitry/software in products I rely on has been designed that way, e.g. just use method X without really understanding why. – Jason S Apr 20 '12 at 13:35
  • 2
    Nice answer, but just two things. First: it's not necessarily lack of attention that leads to the choice of a wrong filter; in my case, I've never been taught about the difference, and the same applies to non-graduated people. So sometimes it's just ignorance. But the second: why do you cascade two first-order digital filters instead of using a higher order one? (just to understand, I'm not criticizing) – clabacchio Apr 20 '12 at 13:51
  • @clabacchio: Two cascaded single pole filters is a higher order one. Write the algorithm for what you consider higher order, and you'll see the same number of operations. – Olin Lathrop Apr 20 '12 at 14:00
  • Fair enough; but I was seeing it as two cascaded IIR, and in my mind it implies more complexity because of redundancy...where am I wrong? – clabacchio Apr 20 '12 at 14:01
  • 3
    two cascaded single pole IIR filters are more robust to numerical issues, and easier to design, than a single 2nd-order IIR filter; the tradeoff is that with 2 cascaded stages you get a low Q (= 1/2?) filter, but in most cases that's not a huge deal. – Jason S Apr 20 '12 at 14:45
  • 1
    @clabacchio: Another issue I should have mentioned is firmware implementation. You can write a single pole low pass filter subroutine once, then apply it multiple times. In fact I usually write such a subroutine to take a pointer in memory to the filter state, then have it advance the pointer so that it can be called in succession easily to realize multi-pole filters. – Olin Lathrop Apr 20 '12 at 15:03
  • 1
  • thanks very much for your answers - all of them. I decided to use this IIR Filter, but this Filter is not used as a Standard LowPass Filter, since I need to average Counter Values and compare them to detect Changes in a certain Range. since these Values van be of very different dimensions depending on Hardware I wanted to take an average in order to be able to react to these Hardware specific changes automatically.
  • – sensslen May 21 '12 at 12:06
  • @OlinLathrop I think there is a problem with your description. With the shift inside the FILT calculation, any time there is an inflection, the output will stay constant until (NEW-FILT) > 16 or < -16. Won't this leave odd flat spots at inflections? I think the calculation and FILT should be left scaled by 16 and the shift done after the calculation to get output. Something like FILT <- FILT + FF(FILT)-NEW and then output FF(FILT) – C. Towne Springer Dec 31 '13 at 20:39
  • @user: I implement it as written, but add fraction bits to FILT as appropriate. I never said FILT is integer. It is usually fixed point. To not loose any data, you have to add Log2(1/FF) bits each filter pole. In practise you can make a tradeoff with where the noise floor is or what accuracy you care about. Note that the error you mention when using too few fraction bits is not accumulating since NEW-FILT gives you the complete error each time. – Olin Lathrop Dec 31 '13 at 20:55
  • @OlinLathrop To be precise, you wrote shift right by 4 bits on small systems, but I get your point. It works fine on fixed point, and certainly much smaller and faster than a circular buffer. – C. Towne Springer Dec 31 '13 at 22:59
  • @user: I glossed over this detail by saying "the numbers usually need to be wider than the input value". In any case, it seems we basically agree. Note that fixed point doesn't necessarily mean using more bits. On a 16 bit machine you can shift a 10 bit A/D reading left 5 bit to view a word as 11.5 fixed point. All the math operations work regardless of where you think the binary point is. – Olin Lathrop Jan 01 '14 at 00:08
  • @mars: So sorry you're annoyed. I'll refund your full payment. Oh, wait... In any case, I just checked and both PLOTFILT and CSVPLOT are included in the Full Runtime release on the page linked to above. From your description I can't tell what exactly went wrong. Did the installation program succeed? Obviously you can't expect the software to run correctly if it didn't install correctly or you failed to follow any of the directions. – Olin Lathrop Dec 02 '14 at 14:49
  • 1
    I have rewritten the filter analysis routine in Python. Anyone interested can get the code here: https://github.com/Miceuz/plotfilt – miceuz Mar 10 '15 at 12:22