# STM32 ADC and timer(s): Measuring moving average peak voltage of PWM signal with varying amplitude

I want to measure a moving average of the amplitude of a fix frequency, fixed dutycyle PWM signal with slowly varying amplitude but in a noisy environment (DC motors).

I want to use as little as possible CPU power to do this and have the ADC do this for me. I have an internal signal coming from a timer that is in sync with the signal to measure. I can use this as a trigger.

Below is what I try to do:

1. use the internal trigger from the timer on the chip that actually generates the PWM signal

2. wait 1/4th of the onPeriod before taking any samples to filter out rising time or overshoot

3. then take N samples and filter out noise by taking the arithmetic average
4. keep a constant moving average of the M past cycles (3 in the drawing, 100 in reality)

5. Store this result in an ADC register such that the application can always retrieve the last 100ms average

So far, I figured out 1 (interconnect of timer output channel and ADC external trigger) and 3 (oversampling) by reading the reference manual and think I can do 2 by using a basic timer inbetween first timer output and ADC. But I completely struggle with 4 and 5.

Can this be done? I am using STM32L4 and F4/7.

• does the ADC have registers? what does the MCU datasheet describe in its register maps? – analogsystemsrf Sep 28 '19 at 12:09
• quite some! Page 482 - 484 of link has the memory map of the ADC registers. – JeromeBu1982 Sep 28 '19 at 12:39

If it were only a small number of averages then I would create a shift register.

Pseudo code for a small number of averages.

function movingAverage(pulseAverage) {
static register[2];          // 3 registers
register[2] = register[1];   // data shift
register[1] = register[0];   // data shift
register[0] = pulseAverage;  // move the input into the array
return (register[2] + register[1] + register[0]) / 3;
}


For a larger number such as the 100 in your question this may use up too much precious RAM in a microprocessor. Instead, I would consider running an "eternal average", averaging all the values since the unit powered up. For example, in your 100 cycle average you would calculate the new average as $$avg = \frac {avg \times 99 + newRead}{100}$$.

This is very simple and requires only one variable. The downside is that it is a little more sluggish in its response. A quick spreadsheet simulation shows the following with averages of the last three cycles and "eternal average" with a weight of 1/3 for the new reading.

Figure 1. Simulation results.

You can try this out quite quickly in a spreadsheet.

• Agree that exponential smoothing is the way to go with 100 samples. No use in storing individual samples. BUT: this is a CPU approach. Was looking to get as much as possible done by the ADC – JeromeBu1982 Sep 28 '19 at 12:41
• I have never heard of an ADC with a CPU. On the other hand there are thousands of CPUs with ADCs. Surely you have a processor after the ADC? – Transistor Sep 28 '19 at 12:54
• STM32L4: MCU running my application, timers and ADC's doing repetitive stuff at hardware level. I am currently looking into Direct Memory Access by the ADC to store the oversampled cycle samples – JeromeBu1982 Sep 28 '19 at 13:10
• @JeromeBu1982 there is no other way than the CPU. – P__J__ Sep 28 '19 at 18:39

The F4/F7 have plenty of memory to handle this. You'll need just over 200 bytes (100 samples * 2 bytes each), which is a fraction of even the smallest F4 chips. A moving average is a type of FIR filter and has some advantages to an IIR filter (such as an "infinite average"), such as that it responds more quickly to changes and will settle more quickly on a value if your ADC readings settle on a value. Often, this means it'll be more accurate at any given time than an IIR filter.

There are a few ways to do a moving average:

1) Use the DMA to write the block of 100 samples. Setup the DMA to write to the address of some variable like uint16_t adc_readings[100], and set the number of transfers to 100, and the memory increment size the halfword. Also setup the DMA to be in circular mode, so it will automatically wrap around to the beginning of the adc_readings array. Start the ADC and DMA and everything will happen in the background, no CPU needed.

Whenever you need to get the moving average value, you'll need to use the CPU, however. Write a function that takes the sum of all 100 values in the array, and divide by 100.

This method is great if you only need to read the average value every once in a while, since there's no CPU load otherwise.

2) Create an interrupt routine for when the ADC has a value for you. In the interrupt, append the value to your array. Something like this:

uint16_t idx=0;
uint16_t moving_average;

moving_average = ((100 * moving_average) - oldest_value + new_value) / 100; //this is the shortcut trick!
if (idx++ > 100) idx = 0;
}