0
\$\begingroup\$

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: enter image description here

  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.

\$\endgroup\$
2
  • \$\begingroup\$ does the ADC have registers? what does the MCU datasheet describe in its register maps? \$\endgroup\$ Commented Sep 28, 2019 at 12:09
  • \$\begingroup\$ quite some! Page 482 - 484 of link has the memory map of the ADC registers. \$\endgroup\$ Commented Sep 28, 2019 at 12:39

3 Answers 3

3
\$\begingroup\$

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 adc_readings[100]={0};
uint16_t moving_average;

void ADC1_IRQHandler(void) {
    uint16_t oldest_value = adc_readings[idx]; //grab the value we're about to overwrite
    uint16_t new_value = ADC1->DR; //read whatever register you need to get the ADC reading
    moving_average = ((100 * moving_average) - oldest_value + new_value) / 100; //this is the shortcut trick!
    adc_readings[idx] = new_value;
    if (idx++ > 100) idx = 0;
}

The advantage of this method is that the moving average is always kept current and is available any time with no extra CPU load when you need to read it. Also, calculating the average each time requires less operations because of the "trick" where you can just pop off the oldest value and pop on the newest one, and update the average by an amount proportional to their difference. The disadvantage is that the CPU is involved with every ADC reading, each time the ADC IRQ is called. So if you go several hundred readings without needing to know the moving average, you'll have some unnecessary CPU load. But if you need to know the moving average quickly, especially if you need to know it with every new ADC reading, then this method wins.

Tip: If your moving average size is a power of 2, such as 128 or 64, then the moving average will be much faster to compute since the divide can be a simple bit shift.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you very much! When posting the question I did not know the concept of circular DMA buffers, only interrupts. So this really, really helps. Question is: what is less CPU cycles? 100 samples = 100ms. I expect to reques the value once per sec. But I then have the latency of calculating the average (can we estimate clock cycles for avg of 100 half words?). I see you calculation using the interrupt. That has low latency. Like it. But my guess is that if I need that value less than per 100ms (or even 200 or 400ms) the total nr of clock cycles used for the whole system is higher? I am wrong? \$\endgroup\$ Commented Sep 29, 2019 at 10:31
  • \$\begingroup\$ If you just count the number of operations, doing an average of 100 values involves 100 adds, 1 divide, 100 increments+compare+branch in the for loop, plus 8-14 to enter/exit the function. Keeping the moving average updated with the ISR involves 1 multiply, 2 add/subtracts, 1 divide, and 8-14 cycles to enter and exit the interrupt, + some to increment/wrap the counter, write the new value. I'd guess once the code is optimized, if you only need the average value every 1 second, the DMA approach will be more efficient. But the fastest way to know for sure would be to code it both ways and test. \$\endgroup\$
    – Dan Green
    Commented Sep 29, 2019 at 16:38
2
\$\begingroup\$

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.

enter image description here

Figure 1. Simulation results.

You can try this out quite quickly in a spreadsheet.

\$\endgroup\$
4
  • \$\begingroup\$ 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 \$\endgroup\$ Commented Sep 28, 2019 at 12:41
  • \$\begingroup\$ 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? \$\endgroup\$
    – Transistor
    Commented Sep 28, 2019 at 12:54
  • \$\begingroup\$ 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 \$\endgroup\$ Commented Sep 28, 2019 at 13:10
  • \$\begingroup\$ @JeromeBu1982 there is no other way than the CPU. \$\endgroup\$ Commented Sep 28, 2019 at 18:39
0
\$\begingroup\$

If you do not want to use CPU - which is not possible if you do not use external averaging hardware. Simple lowpass filter will do

enter image description here enter image description here

or another duty

\$\endgroup\$
3
  • \$\begingroup\$ I am trying to average only the onPeriod of the PWM \$\endgroup\$ Commented Sep 29, 2019 at 16:14
  • \$\begingroup\$ And it does it. Zero is the same all the time \$\endgroup\$ Commented Sep 29, 2019 at 16:20
  • \$\begingroup\$ you're right! I just need to multiply by 1/dutycycle \$\endgroup\$ Commented Oct 1, 2019 at 18:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.