MovingAverageFilter
MovingAverageFilter smooths a noisy signal by averaging it over time, tuned by a sample count rather than by a frequency.7 minute read
MovingAverageFilter smooths a noisy signal by averaging it over time, tuned
by a sample count rather than by a frequency. It is the filter to use when
you think in samples: “average this over a hundred readings”. Its memory cost
is the same at any sample count, so a very long average is as cheap as a short
one.
It averages by weighted decay, not over a fixed window: older samples fade out
rather than dropping out. So output approaches a step gradually and does not
finish after numberOfSamples cycles — it reaches 63% at that point and 90% at
a little over twice it.
flowchart LR
i1(["input — signal to smooth"]) --> B["MovingAverageFilter"]
i2(["reset"]) --> B
i3(["disable"]) --> B
B --> o1(["output — smoothed signal"])
B --> o2(["isEnabled"])
Time constant $\tau = (N+1) \times$ task period [s], with $N$ =
numberOfSamples. At the default 100 on a 1 ms task that is 0.101 s, a cut-off of 1.58 Hz: 63% of a step in 0.1 s, 90% in 0.23 s. Phase lag at the cut-off is 45°, and that lag is the whole trade. Note the time constant scales with the task period, so the same sample count is a different filter in seconds on a different task rate.
For the first numberOfSamples cycles after a start or a reset the filter
converges faster than the numbers above, because it is averaging everything
it has seen rather than decaying. That is deliberate and useful.
Signals
Inputs
| Path | Unit | Range | Description |
|---|---|---|---|
input |
signal unit | unbounded | The signal to smooth. One element per channel; the channel count is fixed by the machine configuration. |
reset |
- | - | A rising value sets output to input immediately and restarts the fast-converging phase. It affects every channel at once, and it works whether the block is enabled or not. Use it after a large step you do not want the filter to average across. |
disable |
- | - | True bypasses the filter: input passes straight to output. Use it for a runtime override from a supervisor; use enable for the configured intent. |
Outputs
| Path | Unit | Description |
|---|---|---|
output |
signal unit | The smoothed signal, one element per channel. Equals input exactly while bypassed. After a controller start its first value is half of input, then it converges — gate the consumer if that matters. |
isEnabled |
- | True when enable is true and disable is false. A single value for the whole block, not one per channel. |
Parameters
| Path | Unit | Default | Range | Effect |
|---|---|---|---|---|
numberOfSamples |
samples | 100 | 0 upward; no upper limit | How heavily to average, shared by all channels. Higher removes more noise and adds more delay, at no extra cost in memory or computation. 0 is a pass-through. Any value is safe — this filter cannot be made unstable. |
enable |
- | true | - | False bypasses the filter: input passes through unchanged. |
Both parameters are persistent and survive a controller restart. The inputs and outputs do not. No parameters exist below this block.
Setup
-
Link the source signal:
…/sensor/rawPressure→…/pressureFilter/input.inputfollows the source on a trace. -
Set
enablefalse.outputequalsinputsample for sample andisEnabledreads false. -
Set
numberOfSamplesto 10. Any value is accepted, so there is nothing to read back and check. -
Set
enabletrue.isEnabledreads true andoutputtracksinputclosely, with the noise still visible. -
Raise
numberOfSamplesin steps of roughly double, watching the noise and the loop consumingoutput. Stop one step below where that loop feels soft or hunts.Step 5 changes what the controller sees. Each doubling of the sample count doubles the delay. Raise it with the axis at rest before trying it in motion — the added delay can destabilise a tightly tuned loop.
-
Step the input and time how long
outputtakes to reach 63% of the step. It should be aboutnumberOfSamplesplus one task periods. -
Trace every element of
inputandoutput. An element that reads zero while the machine moves is an unwired channel, not a filtered one.
Tuning
- Read the ripple you want gone off a trace of
input, and count its period in task cycles. SetnumberOfSamplesto at least a few times that count. - Measure the delay you have bought:
numberOfSamplesplus one task periods is the time constant, and the delay below the cut-off equals it. - Keep that delay below a tenth of the response time of the loop consuming
output. Above that, the loop pays for the smoothing in stability. - Double
numberOfSamplesto halve the noise. It is a cheap knob — there is no memory or computation penalty for a large value — so the only cost is delay. - Check the loop in motion, not at rest. Delay costs phase margin only while the axis is moving fast enough to need it.
- Re-check the sample count after any task-rate change. The count stays the same in samples but the delay in seconds moves with the task period.
- If you need a filter with a hard cut at a known frequency — to kill a mains harmonic, say — this block cannot do it. A finite-impulse-response filter with equal taps can.
Read the time constant off any curve as the moment it crosses 0.63.
| Symptom | Cause | Action |
|---|---|---|
Noise still present on output |
Sample count too low | Double numberOfSamples, then re-check the loop for softness |
| Loop went soft, hunts or oscillates after enabling | Delay too high for the loop | Halve numberOfSamples; if the noise returns, fix it at the source |
output had not finished settling after numberOfSamples cycles |
Expected: the filter decays rather than using a fixed window, so it reaches 63% at that point | Wait about 2.3 times the sample count for 90% |
| Setpoint tracking lags behind the command | The block is in the command path, not the feedback path | Lower numberOfSamples, or move the filter onto the measurement only |
| The filter converged quickly just after start, then got sluggish | Expected: it averages everything it has seen for the first numberOfSamples cycles, then switches to steady decay |
Use reset to get the fast phase back deliberately |
output was half the input on the first cycle after a start |
Expected behaviour of the first sample | Gate the consumer off a short delay after start |
output averaged across a step you wanted it to follow |
The filter has no way to know the step was intentional | Pulse reset at the moment of the step |
| Resetting one channel restarted the averaging on all of them | Expected: the sample count is shared across channels | Use a separate filter per channel if they need independent resets |
Changing numberOfSamples had no immediate effect |
Expected: the filter is still in its fast-converging phase, where the count does not yet apply | Pulse reset, or wait for the count to be reached |
output follows input exactly, with no smoothing at all |
numberOfSamples is 0, or enable is false, or disable is true |
Read isEnabled and numberOfSamples; 0 is a pass-through |
| The delay changed after a task-rate change | Expected: the time constant is the sample count times the task period | Rescale numberOfSamples by the ratio of the task periods |
| You need a hard cut at a specific frequency | This filter has no such thing — it is a gentle first-order roll-off | Use a finite-impulse-response filter with equal taps |
| Some channels smooth more than others | Not possible — all channels share one sample count | Use a separate filter per channel group |
output went to a non-numeric value |
A non-numeric value reached input |
Fix the source, then pulse reset — that clears the filter completely |
A conservative starting point for a pressure signal on a 1 ms task, feeding a loop with a 100 ms response time:
numberOfSamples = 50
enable = true
This is a starting point, not a final tuning. Work step 1 with a trace of your own signal.
Limits and errors
| Limit | Set by | What happens | Reported |
|---|---|---|---|
numberOfSamples |
Nothing | Not checked, and it does not need to be — no value can make this filter unstable. 0 is a pass-through | Not reported |
| Filter stability | Guaranteed | Stable at every sample count and every task rate. Unlike the frequency-tuned filters, there is no task-rate ceiling to respect | Not applicable |
output |
Nothing | Unbounded — whatever the input carries reaches the output. Limit it downstream if the consumer needs a bound | Not reported |
| Filter memory | reset, or a bypassed cycle |
Both set output to input and clear the filter completely. There is always a way out of a bad state |
Not reported |
| Channel count | Machine configuration | Fixed once the controller starts; it cannot be changed at runtime | Not reported |
The block raises no errors or warnings and logs nothing. Every failure above shows as a value on a trace, not as a message.
Verified against motorcortex-control3 3.30.0 (bc348fd).