我正在寻找一种节省时间和内存的解决方案来计算C中的移动平均值。我需要避免除法,因为我使用的是没有专用除法单元的PIC 16。
此刻,我只是将所有值存储在环形缓冲区中,并在每次新值到达时简单地存储和更新总和。这确实很有效,但不幸的是占用了我的大部分可用内存...
我正在寻找一种节省时间和内存的解决方案来计算C中的移动平均值。我需要避免除法,因为我使用的是没有专用除法单元的PIC 16。
此刻,我只是将所有值存储在环形缓冲区中,并在每次新值到达时简单地存储和更新总和。这确实很有效,但不幸的是占用了我的大部分可用内存...
Answers:
正如其他人提到的那样,您应该考虑使用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汇编器预处理器实现为宏,该预处理器比任何一个内置宏功能都更强大。
如果您可以将两个项目的幂限制为平均值(即2,4、8、16、32等),那么可以在没有专用除法的低性能微型计算机上轻松高效地完成除法,因为可以做一点位移。每个右移是两个的幂,例如:
avg = sum >> 2; //divide by 2^2 (4)
要么
avg = sum >> 3; //divide by 2^3 (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
样本不变即可。显然,如果您的statesize
和decimationFactor
为2的幂,则可以得到更好的性能,因此除法和余数运算符将被移位和掩码和所代替。
后记:我同意奥林的观点,您应该始终在移动平均滤波器之前考虑简单的IIR滤波器。如果您不需要棚车滤波器的零频,那么1极或2极低通滤波器可能会很好地工作。
另一方面,如果您出于抽取目的进行过滤(获取高采样率输入并将其平均以供低速率处理使用),那么CIC过滤器可能正是您想要的。(尤其是如果您可以使用statesize = 1并仅使用单个先前的积分器值就完全避免使用环形缓冲区)
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,依此类推。
(更改后,我将立即验证此处列出的代码,并在需要时编辑此答案。)
这是一个单极点低通滤波器(移动平均值,截止频率= 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)。
您可以使用简单的IIR滤波器来近似某些应用的移动平均值。
权重是0..255的值,较高的值=较短的平均时间标度
值=(newvalue * weight + value *(256-weight))/ 256
为了避免舍入错误,value通常会很长,您只能使用高阶字节作为“实际”值。
其他所有人都对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;
}
正如mikeselectricstuff所说,如果您确实需要减少内存需求,并且您不介意脉冲响应是指数(而不是矩形脉冲),那么我会选择指数移动平均滤波器。我广泛使用它们。使用这种类型的过滤器,您不需要任何缓冲区。您不必存储N个过去的样本。只有一个。因此,您的内存需求减少了N倍。
另外,您不需要为此做任何划分。仅乘法。如果可以使用浮点运算,请使用浮点乘法。否则,请进行整数乘法并向右移动。但是,现在是2012年,我建议您使用允许使用浮点数的编译器(和MCU)。
除了可以提高内存效率和更快的速度(您不必更新任何循环缓冲区中的项)之外,我还说这也是更自然的,因为在大多数情况下,指数冲激响应可以更好地匹配自然行为。
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,您的两阶段级联也需要一些解释。您是否要保留两个平均值,并且将每个迭代的第一个结果存入第二个平均值中?这有什么好处?