快速且高效存储的移动平均值计算


33

我正在寻找一种节省时间和内存的解决方案来计算C中的移动平均值。我需要避免除法,因为我使用的是没有专用除法单元的PIC 16。

此刻,我只是将所有值存储在环形缓冲区中,并在每次新值到达时简单地存储和更新总和。这确实很有效,但不幸的是占用了我的大部分可用内存...


3
我认为没有其他节省空间的方法可以做到这一点。
Rocketmagnet 2012年

4
@JobyTaffey很好,它是控制系统上使用非常广泛的算法,它需要处理有限的硬件资源。因此,我认为他会比在SO上找到更多帮助。
clabacchio

3
@Joby:关于这个问题的一些皱纹与资源有限的小型系统有关。看我的答案。您在像SO人员习惯的大型系统上执行此操作的方式将非常不同。在我设计电子产品的经验中,这涉及到很多方面。
Olin Lathrop'4

1
我同意。这对于此论坛非常适用,因为它与嵌入式系统有关。
Rocketmagnet 2012年

我撤回异议
Toby Jaffey 2012年

Answers:


55

正如其他人提到的那样,您应该考虑使用IIR(无限脉冲响应)滤波器,而不是现在使用的FIR(有限脉冲响应)滤波器。还有更多功能,但乍一看,FIR滤波器实现为显式卷积和带有方程式的IIR滤波器。

我在微控制器中经常使用的特殊IIR滤波器是单极点低通滤波器。这是简单的RC模拟滤波器的数字等效。对于大多数应用程序,它们将具有比您使用的盒式过滤器更好的特性。我遇到的盒式滤波器的大多数用途是某人在数字信号处理课中没有注意的结果,而不是由于需要它们的特殊特性而导致的。如果您只想衰减已知为噪声的高频,则单极点低通滤波器会更好。在微控制器中以数字方式实现的最佳方法通常是:

FILT <-FILT + FF(NEW-FILT)

FILT是一种持久状态。这是计算此过滤器所需的唯一持久变量。NEW是此迭代正在更新过滤器的新值。FF是过滤器分数,用于调整过滤器的“重量”。查看此算法,可以看到对于FF = 0而言,滤波器无穷大,因为输出永远不会改变。对于FF = 1,因为输出仅跟随输入,所以实际上根本没有任何滤波器。有用的值介于两者之间。在小型系统上,您选择FF为1/2 N因此乘以FF可以实现向右移N位。例如,FF可能是1/16,乘以FF因此是4位的右移。否则,此过滤器只需要一个减法和一个加法,尽管数字通常需要比输入值宽(有关数字精度的更多信息,请参见下面的单独部分)。

通常,我获取A / D读数的速度明显快于所需的速度,并应用其中两个级联的滤波器。这是串联的两个RC滤波器的数字等效值,在衰减频率之上衰减12 dB /倍频程。但是,对于A / D读数,通常通过考虑其阶跃响应来查看时域中的滤波器更为相关。这告诉您在测量事物变化时系统看到变化的速度。

为了方便设计这些过滤器(这仅意味着选择FF并确定要级联的数量),我使用程序FILTBITS。您可以在级联的滤波器系列中为每个FF指定移位位数,并计算阶跃响应和其他值。实际上,我通常通过包装脚本PLOTFILT运行此代码。这将运行FILTBITS,该文件将生成CSV文件,然后绘制CSV文件。例如,这是“ PLOTFILT 4 4”的结果:

PLOTFILT的两个参数表示将有两个上述类型的过滤器级联。值4表示实现与FF相乘的移位位数。因此,在这种情况下,两个FF值为1/16。

红色迹线是单位阶跃响应,是主要要看的东西。例如,这告诉您,如果输入瞬时更改,则组合滤波器的输出将在60次迭代中稳定为新值的90%。如果您关心的是95%的建立时间,则必须等待约73次迭代,而对于50%的建立时间,则只需等待26次迭代。

绿色迹线显示单个完整振幅尖峰的输出。这使您对随机噪声抑制有所了解。看起来没有一个样本会导致输出变化超过2.5%。

蓝色迹线使人对该滤波器对白噪声的处理产生主观感觉。这不是严格的测试,因为不能保证这次运行的PLOTFILT被选作白噪声输入的随机数的确切内容。只是让您大致感觉到它将被压榨多少以及光滑程度如何。

在我的软件下载页面上的PIC开发工具软件版本中,可以找到PLOTFILT(可能是FILTBITS)以及许多其他有用的东西,尤其是用于PIC固件开发的东西。

增加了数值精度

从评论和新答案中可以看出,有兴趣讨论实现此过滤器所需的位数。请注意,乘以FF将在二进制点下方创建Log 2(FF)新位。在小型系统上,FF通常选择为1/2 N,这样实际上通过向右移位N位来实现该乘法。

因此,FILT通常是一个定点整数。请注意,从处理器的角度来看,这不会改变任何数学运算。例如,如果要过滤10位A / D读数并且N = 4(FF = 1/16),则需要在10位整数A / D读数之下的4个小数位。大多数处理器中,由于10位A / D读数,您将要进行16位整数运算。在这种情况下,您仍然可以执行完全相同的16位整数运算,但首先将A / D读数左移4位。处理器不知道差异,也不需要。无论您认为它们是12.4个固定点还是真正的16位整数(16.0个固定点),对整个16位整数进行数学运算都有效。

通常,如果您不想由于数字表示而增加噪声,则需要在每个滤波器极点上添加N位。在上面的示例中,两个第二个过滤器必须具有10 + 4 + 4 = 18位才能不丢失信息。实际上,在8位计算机上,这意味着您将使用24位值。从技术上讲,只有两个的第二极需要更大的值,但是为了简化固件,我通常对滤波器的所有极使用相同的表示形式,从而使用相同的代码。

通常,我编写一个子例程或宏来执行一个滤波器极点操作,然后将其应用于每个极点。在特定项目中,子程序还是宏取决于循环或程序存储器是否更重要。无论哪种方式,我都使用一些暂存状态将NEW传递到子例程/宏中,该子例程/宏将更新FILT,但也将其加载到NEW所在的相同暂存状态中。由于更新了一个极点的FILT,因此可以轻松地应用多个极点。下一个的新功能。当有子例程时,在进入过程中有一个指向FILT的指针很有用,在离开过程中它会更新为紧随FILT之后。这样,如果多次调用子例程,它将自动在内存中的连续过滤器上运行。使用宏,您不需要指针,因为您传入了地址以进行每次迭代。

程式码范例

这是上面针对PIC 18所描述的宏示例:

///////////////////////////////////////////////////// //////////////////////////////
//
//宏过滤器过滤
//
//用NEWVAL中的新值更新一个滤波器极点。NEWVAL已更新为
//包含新的过滤值。
//
// FILT是过滤器状态变量的名称。假定为24位
//并在本地银行中。
//
//更新过滤器的公式为:
//
// FILT <-FILT + FF(NEWVAL-FILT)
//
// FF乘以FILTBITS位的右移来完成。
//
/宏过滤器
  /写
         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

  /写
  / loop n个位;每个位一次,将NEWVAL右移
         rlcf newval + 2,w;将NEWVAL右移一位
         rrcf newval + 2
         rrcf newval + 1
         rrcf newval + 0
    /结束循环

  /写
         movf newval + 0,w;将移位后的值添加到过滤器中并保存在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

这是PIC 24或dsPIC 30或33的类似宏:

///////////////////////////////////////////////////// //////////////////////////////
//
//宏过滤器ffbits
//
//更新一个低通滤波器的状态。新的输入值在W1:W0中
//,并且要更新的过滤器状态由W2指向。
//
//更新后的过滤器值也将在W1:W0中返回,W2将指向
//至过滤器状态之后的第一个内存。因此,此宏可以是
//依次调用以更新一系列级联的低通滤波器。
//
//过滤器公式为:
//
// FILT <-FILT + FF(NEW-FILT)
//
//乘以FF是通过算术右移
// FFBITS。
//
//警告:W3已废弃。
//
/宏过滤器
  / var新的ffbits整数= [arg 1];获取要转换的位数

  /写
  / write“;执行一极低通滤波,移位位=” ffbits
  /写“;”

         sub w0,[w2 ++],w0; NEW-FILT-> W1:W0
         子w1,[w2-],w1

         lsr w0,#[v ffbits],w0;将结果右移W1:W0
         sl w1,#[-16 ffbits],w3
         或w0,w3,w0
         asr w1,#[v ffbits],w1

         添加w0,[w2 ++],w0;添加FILT以在W1:W0中获得最终结果
         addc w1,[w2-],w1

         mov w0,[w2 ++];将结果写入过滤器状态,前进指针
         mov w1,[w2 ++]

  /写
  / endmac

这两个示例都使用我的PIC汇编器预处理器实现为宏,该预处理器比任何一个内置宏功能都更强大。


1
+1-就靠钱。我唯一要补充的是,移动平均滤波器在与某些任务(例如产生驱动超声波发生器的驱动波形)同步执行时确实有其位置,以便它们滤除1 / T的谐波,其中T是移动的平均时间。
杰森S

2
好的答案,但有两件事。第一:不一定是缺乏关注导致选择了错误的过滤器;就我而言,我从未听说过这种差异,对于未毕业的人也是如此。所以有时候这只是无知。但是第二点:为什么要级联两个一阶数字滤波器而不是使用一个高阶数字滤波器?(只是为了理解,我没有批评)
clabacchio

3
与单个二阶IIR滤波器相比,两个级联的单极IIR滤波器对数值问题更鲁棒,更易于设计。折衷方案是,通过2个级联级,您将获得低Q(= 1/2?)滤波器,但是在大多数情况下,这并不是什么大问题。
杰森S

1
@clabacchio:我应该提到的另一个问题是固件实现。您可以编写一次单极点低通滤波器子例程,然后多次应用。实际上,我通常会编写这样的子例程,以使内存中的指针进入过滤器状态,然后使该指针前进,以便可以轻松地连续调用它以实现多极点过滤器。
Olin Lathrop'4

1
1.非常感谢您的回答-所有这些。我决定使用此IIR过滤器,但由于我需要对计数器值求平均并进行比较以检测特定范围内的变化,因此该过滤器不用作标准低通滤波器。由于根据硬件的不同,这些值的尺寸可能会非常不同,因此我希望取一个平均值,以便能够对这些特定于硬件的更改自动做出反应。
2012年

18

如果您可以将两个项目的幂限制为平均值(即2,4、8、16、32等),那么可以在没有专用除法的低性能微型计算机上轻松高效地完成除法,因为可以做一点位移。每个右移是两个的幂,例如:

avg = sum >> 2; //divide by 2^2 (4)

要么

avg = sum >> 3; //divide by 2^3 (8)

等等


有什么帮助?OP说,主要问题是将过去的样本保留在内存中。
杰森S

这根本没有解决OP的问题。
Rocketmagnet 2012年

12
OP认为他有两个问题,将PIC16和内存分配给环形缓冲区。这个答案表明划分并不困难。诚然,它不能解决内存问题,但是SE系统允许部分答案,用户可以从每个答案中自己取一些东西,甚至可以编辑并组合其他人的答案。由于其他一些答案需要除法运算,因此它们同样不完整,因为它们没有显示如何在PIC​​16上有效地实现此目的。
马丁

8

如果您不介意降低采样率,那么对于真正的移动平均滤波器(又名“棚车滤波器”)有一个答案,那就是内存需求更少。它称为级联积分梳状滤波器(CIC)。这个想法是您拥有一个积分器,该积分器在一段时间内会产生差异,而关键的内存节省设备是通过降低采样率,您不必存储积分器的每个值。可以使用以下伪代码来实现:

function out = filterInput(in)
{
   const int decimationFactor = /* 2 or 4 or 8 or whatever */;
   const int statesize = /* whatever */
   static int integrator = 0;
   static int downsample_count = 0;
   static int ringbuffer[statesize];
   // don't forget to initialize the ringbuffer somehow
   static int ringbuffer_ptr = 0;
   static int outstate = 0;

   integrator += in;
   if (++downsample_count >= decimationFactor)
   {
     int oldintegrator = ringbuffer[ringbuffer_ptr];
     ringbuffer[ringbuffer_ptr] = integrator;
     ringbuffer_ptr = (ringbuffer_ptr + 1) % statesize;
     outstate = (integrator - oldintegrator) / (statesize * decimationFactor);
   }
   return outstate;
}

您的有效移动平均长度为,decimationFactor*statesize但您只需要保持statesize样本不变即可。显然,如果您的statesizedecimationFactor为2的幂,则可以得到更好的性能,因此除法和余数运算符将被移位和掩码和所代替。


后记:我同意奥林的观点,您应该始终在移动平均滤波器之前考虑简单的IIR滤波器。如果您不需要棚车滤波器的零频,那么1极或2极低通滤波器可能会很好地工作。

另一方面,如果您出于抽取目的进行过滤(获取高采样率输入并将其平均以供低速率处理使用),那么CIC过滤器可能正是您想要的。(尤其是如果您可以使用statesize = 1并仅使用单个先前的积分器值就完全避免使用环形缓冲区)


8

Olin Lathrop在数字信号处理堆栈交换中已经描述了使用一阶IIR滤波器进行的数学运算的深入分析(包括许多漂亮的图片。)此IIR滤波器的公式为:

y [n] =αx[n] +(1-α)y [n-1]

可以仅使用整数而不使用以下代码进行除法来实现(在我从内存键入内容时可能需要进行一些调试)。

/**
*  @details    Implement a first order IIR filter to approximate a K sample 
*              moving average.  This function implements the equation:
*
*                  y[n] = alpha * x[n] + (1 - alpha) * y[n-1]
*
*  @param      *filter - a Signed 15.16 fixed-point value.
*  @param      sample - the 16-bit value of the current sample.
*/

#define BITS 2      ///< This is roughly = log2( 1 / alpha )

short IIR_Filter(long *filter, short sample)
{
    long local_sample = sample << 16;

    *filter += (local_sample - *filter) >> BITS;

    return (short)((*filter+0x8000) >> 16);     ///< Round by adding .5 and truncating.
}

该滤波器通过将alpha的值设置为1 / K来近似最后K个样本的移动平均值。在前面的代码中,#define通过BITS选择LOG2(K)来执行此操作,即,对于K = 16设置BITS为4,对于K = 4设置BITS为2,依此类推。

(更改后,我将立即验证此处列出的代码,并在需要时编辑此答案。)


6

这是一个单极点低通滤波器(移动平均值,截止频率= CutoffFrequency)。非常简单,非常快,效果很好,几乎没有内存开销。

注意:除传递给newInput的变量外,所有变量的作用域都超出过滤器功能

// One-time calculations (can be pre-calculated at compile-time and loaded with constants)
DecayFactor = exp(-2.0 * PI * CutoffFrequency / SampleRate);
AmplitudeFactor = (1.0 - DecayFactor);

// Filter Loop Function ----- THIS IS IT -----
double Filter(double newInput)
{
   MovingAverage *= DecayFactor;
   MovingAverage += AmplitudeFactor * newInput;

   return (MovingAverage);
}

注意:这是一个单级滤波器。多个级可以级联在一起以增加滤镜的清晰度。如果您使用多个阶段,则必须调整DecayFactor(与截止频率有关)以进行补偿。

显然,您所需要做的就是将这两行放置在任何地方,它们不需要自己的功能。在移动平均值代表输入信号的平均值之前,此滤波器确实具有斜坡上升时间。如果需要绕过启动时间,则可以将MovingAverage初始化为newInput的第一个值而不是0,并希望第一个newInput不是异常值。

(CutoffFrequency / SampleRate)的范围在0到0.5之间。DecayFactor是介于0和1之间的值,通常接近1。

单精度浮点数足以应付大多数事情,我只是更喜欢双精度。如果需要使用整数,可以将DecayFactor和Amplitude Factor转换为小数整数,其中分子存储为整数,分母为2的整数次幂(因此,可以将右移至整数)。分母,而不必在滤波器循环中进行除法)。例如,如果DecayFactor = 0.99,而您想使用整数,则可以设置DecayFactor = 0.99 * 65536 =64881。然后,只要在过滤器循环中乘以DecayFactor,就只需将结果>> 16即可。

有关此问题的更多信息,请在线阅读一本非常好的书,有关递归过滤器的第19章:http : //www.dspguide.com/ch19.htm

PS对于移动平均范式,一种不同的设置DecayFactor和AmplitudeFactor的方法可能与您的需求更相关,假设您希望将之前的大约6个项目进行平均,并单独进行,您需要添加6个项目并除以6,因此您可以将AmplitudeFactor设置为1/6,将DecayFactor设置为(1.0-AmplitudeFactor)。


4

您可以使用简单的IIR滤波器来近似某些应用的移动平均值。

权重是0..255的值,较高的值=较短的平均时间标度

值=(newvalue * weight + value *(256-weight))/ 256

为了避免舍入错误,value通常会很长,您只能使用高阶字节作为“实际”值。


3

其他所有人都对IIR与FIR的效用以及二分法进行了详尽的评论。我只想提供一些实现细节。以下内容适用于没有FPU的小型微控制器。没有乘法,并且如果将N保持2的幂,则所有除法都是单周期移位。

基本FIR环形缓冲区:保留最近N个值的运行缓冲区,并在缓冲区中保留所有值的运行SUM。每次有新样本进入时,从SUM中减去缓冲区中最旧的值,将其替换为新样本,将新样本添加到SUM,然后输出SUM / N。

unsigned int Filter(unsigned int sample){
    static unsigned int buffer[N];
    static unsigned char oldest = 0;
    static unsigned long sum;

    sum -= buffer[oldest];
    sum += sample;
    buffer[oldest] = sample;
    oldest += 1;
    if (oldest >= N) oldest = 0;

    return sum/N;
}

修改后的IIR环形缓冲区:保持最近N个值的运行总和。每次有新样本出现时,SUM-= SUM / N,添加新样本,然后输出SUM / N。

unsigned int Filter(unsigned int sample){
    static unsigned long sum;

    sum -= sum/N;
    sum += sample;

    return sum/N;
}

如果我没看错,那是在描述一阶IIR滤波器;您减去的值不是下降的最早值,而是先前值的平均值。一阶IIR滤波器当然很有用,但是当您建议所有周期信号的输出都相同时,我不确定您的意思。在10KHz采样率下,将100Hz方波馈入20级盒式滤波器将产生一个信号,该信号对于20个采样均匀上升,对于30个采样均匀上升,对于20个采样均匀下降,对于30个较低下降。一阶IIR过滤器...
2013年

...将产生一波波,该波将开始急剧上升并逐渐接近输入最大值(但不是在输入最大值处),然后急剧开始下降并逐渐趋近于输入最小值(但不是在输入最小值处)。行为截然不同。
2013年

没错,我混淆了两种过滤器。这确实是一阶IIR。我正在更改答案以匹配。谢谢。
斯蒂芬·科林斯

一个问题是,简单的移动平均可能有用也可能没有用。使用IIR过滤器,您可以获得具有相对较少的计算的漂亮过滤器。您描述的FIR只能在时间上给您一个矩形-以频率为单位的正弦-您无法管理旁瓣。如果您可以节省时钟滴答,最好将其乘以几个整数,使其成为一个很好的对称可调FIR。
Scott Seidman

@ScottSeidman:如果只需要将FIR的每一级都输出到该级的输入平均值及其先前存储的值,然后存储输入(如果该输入具有数字范围,则可以使用总和),则无需乘数而不是平均)。是否优于盒式滤波器取决于应用程序(例如,总延迟为1ms的盒式滤波器的阶跃响应在输入变化时会有令人讨厌的d2 / dt尖峰,然后在1ms后再次出现,但是总延迟为1ms的滤波器的最小可能d / dt)。
2013年

2

正如mikeselectricstuff所说,如果您确实需要减少内存需求,并且您不介意脉冲响应是指数(而不是矩形脉冲),那么我会选择指数移动平均滤波器。我广泛使用它们。使用这种类型的过滤器,您不需要任何缓冲区。您不必存储N个过去的样本。只有一个。因此,您的内存需求减少了N倍。

另外,您不需要为此做任何划分。仅乘法。如果可以使用浮点运算,请使用浮点乘法。否则,请进行整数乘法并向右移动。但是,现在是2012年,我建议您使用允许使用浮点数的编译器(和MCU)。

除了可以提高内存效率和更快的速度(您不必更新任何循环缓冲区中的项)之外,我还说这也是更自然的,因为在大多数情况下,指数冲激响应可以更好地匹配自然行为。


5
我不同意您使用浮点数的建议。由于某种原因,OP可能使用8位微控制器。寻找具有硬件浮点支持的8位微控制器可能是一项艰巨的任务(您知道吗?)。在没有硬件支持的情况下使用浮点数将是一项非常耗费资源的任务。
PetPaulsen,2012年

5
说您应该始终使用具有浮点功能的流程是很愚蠢的。此外,任何处理器都可以执行浮点运算,这只是速度问题。在嵌入式世界中,几美分的构建成本可能是有意义的。
Olin Lathrop'4

@Olin Lathrop和PetPaulsen:我从未说过他应该使用带有硬件FPU的MCU。重新阅读我的答案。“(和MCU)”是指功能强大的MCU,足以以流畅的方式与软件浮点算法一起工作,而并非所有MCU都如此。
Telaclavo

4
无需仅将浮点(硬件或软件)用于1极低通滤波器。
杰森S

1
如果他有浮点运算,那么他一开始就不会反对除法。
Federico Russo 2012年

0

Iolin滤波器的一个问题几乎被@olin和@supercat所触及,但显然被其他人忽略了,即四舍五入引入了一些不精确度(以及可能的偏斜/截断):假设N是2的幂,并且只有整数运算是使用右移确实会系统地消除新样本的LSB。这意味着该系列可以持续多长时间,平均数将永远不会考虑这些因素。

例如,假设一个递减的级数(8,8,8,...,8,7,7,7,... 7,6,6,)并假设平均值在开始时确实为8。无论过滤强度如何,拳头“ 7”的样本都会使平均值达到7。仅用于一个样本。同样的故事发生在6位,等等。现在想反了:意甲上升。平均值将永远保持在7,直到样本足够大以至于可以更改。

当然,您可以通过添加1/2 ^ N / 2来校正“偏差”,但这并不能真正解决精度问题:在这种情况下,递减的序列将永远保持在8,直到样本为8-1 / 2 ^(N / 2)。例如,对于N = 4,任何大于零的样本都将保持平均值不变。

我相信解决方案将意味着保留丢失的LSB的累加器。但是我还没有做好足够的准备代码的准备,而且我不确定在其他一些系列情况下它是否不会损害IIR的能力(例如,那么7,9,7,9是否平均为8) 。

@Olin,您的两阶段级联也需要一些解释。您是否要保留两个平均值,并且将每个迭代的第一个结果存入第二个平均值中?这有什么好处?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.