You can keep a rolling window of that last n values, when a value ages out of the oldest end of the window, we subtract that value from a total, when we add a new latest value in, we add it to the total. Suggest doing as you did and taking the total and subtracting the values
static size_t youngest_ix = 1;
static size_t oldest_ix = 0;
/* remove oldest from the total */
int oldest_val = circular_buffer[ oldest_ix ];
static int moving_total -= oldest_val;
oldest_ix = wrap_around( oldest_ix + 1 ); // size_t wrap_around (size_t x ) { return x >= circular_buf_size ? 0 : x; }
/* add in most recent value into the total */
int youngest_val = circular_buffer[ youngest_ix ];
moving_total += youngest_val;
youngest_ix = wrap_around( youngest_ix + 1 );
const int = 3; /* or anything you like */
weighted_moving_average = moving_total + ( youngest_val >> nshift ) - ( oldest_val >> nshift ) ;
This weighted moving average, when unweighted (ie nshift = 0 ) is called
convolution in mathematics; it comes up in Fourier transforms when you consider a time domain response of a filter that is bandwidth-limited in the frequency domain, ie. limited to pass frequencies between some f1 ≤ f ≤ f2 the Fourier transform of a square function, one whose whose frequency response is = ( =1 ; f1 ≤ f ≤ f2; = 0 otherwise).
But there isn’t much mathematics in it as you can see; add up the last n entries sum [0..n-1] (where n = circular_buf_size ) and take that sum over entries 0..n-1, then take sum over entries 1..n, then 2..n+1 etc and this sequence of sums is the moving average. The size (width) ie no. of entries in the circular buffer is the crucial parameter that determines the amount of smoothing. The more entries in the buffer, then the more the smoothing effect. Here I’ve just incorporated your idea as an additional tuning technique to also help tweak the behaviour as you please. There are many variations on this that you could try, but this is one, which I don’t particularly like because of the computational cost unless you use shifts instead of division and multiplication and also there could be problems with overflow which are nasty, so I prefer the earlier expression, although it’s not as flexible.
/* An alternative technique; the weighting factor probably really ought to be a power of 2 to reduce the cost of the division */
const uint weighting_factor = 2; /* for example */
weighted_moving_average = (moving_total * weighting_factor + (youngest_val - oldest_val >> nshift )) / weighting_factor;
Need to watch out for overflow in the multiplications and in the additions particularly where the buffer complete sum is calculated.
Remember if you have to go this way the old trick that divisions can always be done as follows by suitable (( x * ((1.0/div) << n ) ) >> n ) where ((1.0/div) << n ) is an integer constant and the multiplication needs to be done in enhanced width. You might want to add on an offset value after the multiplication to adjust for the rounding error when doing the division represented by the >> n, worth doing all this with a spreadsheet, integer maths and lots of tweaking to get the best accuracy. Probably best avoided though where accuracy matters.