1. 为什么一个看似简单的滤波器值得单独写一篇
指数移动平均(Exponential Moving Average,EMA),在数字信号处理语境下几乎就是“一阶低通滤波”的离散孪生兄弟。我最早接触它是做传感器去噪,当时拿着一个IMU的加速度计数据,噪声大得没法直接用,同事甩了一句“套个低通就行”,结果我翻了半天资料,陷入了“均值滤波窗口该开多大”“要不要上卡尔曼”这种选择困难。后来才明白,一个指数移动平均就能解决七八成问题,而且它不仅是滤波,还广泛出现在强化学习的奖励平滑、深度学习优化器里的动量项、技术分析里的MACD指标、控制环路里的参考值斜坡处理。
这类滤波器最容易被低估,恰恰因为它太简单了:一阶、一个参数、几十行代码就能实现。可实际操作中,参数选不对、边界条件没处理好、离散化理解偏差,都会导致“滤波了但没完全滤掉”或者“信号被拉得面目全非”。这篇文章就把指数移动平均和一阶低通滤波的事说透:从连续域到离散域的推导,系数和时间常数的换算,到工程里怎么整定,再配上常见坑位清单,争取看完就能直接上手用。
适合谁看:写嵌入式单片机做传感器处理的工程师、做数据分析和量化策略的Python用户、做控制算法需要平滑参考轨迹的同学,以及所有被“噪声毛刺”困扰却不想一上来就上重型算法的朋友。
2. 指数移动平均与一阶低通滤波的本质关系
2.1 从RC低通电路说起
经典的模拟一阶低通滤波器就是一个电阻串联一个电容到地,输入信号经过电阻给电容充电,从电容两端取输出电压。它的频域特性用传递函数表达:
G(s) = 1 / (1 + s·RC)
其中RC就是时间常数τ。这个式子的物理含义很直观:频率越高,电容的容抗越小,高频成分越容易被短路到地,所以输出里高频分量被衰减;直流(频率为0)时容抗无穷大,信号全部落在电容上,输出等于输入。所以这是一个低通滤波器,转折频率(截止频率)为:
f_c = 1 / (2πRC)
工程上常说的“截至频率”,就是指在这个频率点,信号幅度衰减到原来的0.707倍(即-3dB)。低于这个频率的能通过,高于的逐步衰减。
2.2 模拟域到离散域的离散化
到了数字系统里,我们处理的是一串采样序列,没法直接塞一个电容进去,所以要把微分方程变成差分方程。RC电路的时域微分方程是:
RC · (dy/dt) + y = x
其中x是输入信号,y是输出信号。用一阶向后差分近似导数:
dy/dt ≈ (y[n] - y[n-1]) / Δt
代入原方程:
RC · (y[n] - y[n-1]) / Δt + y[n] = x[n]
整理得到:
(RC/Δt + 1) · y[n] = x[n] + (RC/Δt) · y[n-1]
y[n] = [1 / (RC/Δt + 1)] · x[n] + [(RC/Δt) / (RC/Δt + 1)] · y[n-1]
令 α = Δt / (RC + Δt),则:
y[n] = α · x[n] + (1 - α) · y[n-1]
这就是教科书里的指数移动平均递推式。它恰好是当前输入和上一次输出的加权和,权重只由α决定。可以看到,所谓的“指数移动平均”,本质上就是把连续的一阶RC低通滤波离散化得到的递推形式。
2.3 为什么它叫“指数”移动平均
如果把递推式反复展开,会得到:
y[n] = α · x[n] + α·(1-α) · x[n-1] + α·(1-α)^2 · x[n-2] + …
每一项对历史输入的贡献以(1-α)为公比呈指数衰减。时间越久远的数据,影响力越小,而且衰减是指数级的——这就是名字里“指数”二字的来源。
对比普通滑动平均(SMA),它给每个历史数据相同的权重1/N,EMA则给近期数据更高权重。这个特性带来的实际好处是:它对信号的响应更快,同时不需要维护一个N长的数据窗口,内存占用是恒定的。对单片机来说,EMA就是一个全局变量的事,而滑动平均最低也要一个环形缓冲区。
2.4 两套叫法的使用场景倾向
虽然数学上二者是同一件事,但工程里叫法有偏向:做信号处理、电路的人一般说“一阶低通滤波”,关注的是截止频率、衰减斜率(-20dB/十倍频程)这些频域指标;做数据分析、量化交易的人习惯说“指数移动平均”,关注的是平滑程度、滞后大小这些时域指标。我在实际写代码时,两者就是同一个递推式,只是参数整定的思路不同。下一节就聊怎么把α和频域的截止频率对应起来。
3. 衰减与截止频率:用频域视角理解参数
3.1 频域响应与噪声衰减倍数
EMA的离散传递函数可以写成:
H(z) = α / [1 - (1-α)·z^{-1}]
令z = e^{jωΔt},可以画出幅频特性曲线。重要的是几个关键结论:
- 直流增益是1,也就是说信号中的直流分量(平均值)完全不衰减。
- 频率越高,增益越小,整体呈现低通特性。
- 截止频率处增益为0.707。
实际工程里更常用一个近似公式来求等效截止频率:
f_c ≈ 1 / (2π · Ts) · α / (1 - α)
其中Ts是采样周期。这个公式的推导思路是:将模拟时间常数τ近似为 (1-α)·Ts / α,然后代入f_c = 1/(2πτ)。在α远小于1时,这个近似误差很小;α较大时会有偏差,但也能给整定一个大致标尺。
另一个有用的工程指标是高频噪声衰减倍数:当输入频率远高于截止频率时,每倍频程衰减约6dB,相当于幅度以2倍频程减半。举个例子,假设采样率1kHz,α取0.1,对应的截止频率大约是1/(2π·0.001)·0.1/0.9 ≈ 17.7Hz。如果信号本身以1Hz变化,噪声是50Hz工频干扰,50Hz相对17.7Hz大约高了2.8倍频程,幅度大约衰减到原来的1/3.5。这个衰减幅度并不算大,很多场景不够用,可能需要级联二阶/三阶。
3.2 时间常数τ的直观理解
时间常数τ = RC,它决定这个滤波器对阶跃输入的响应速度。给一个从0突然跳到1的阶跃信号,一阶系统的输出经过一个τ的时间能够到达稳态值的63.2%,经过3τ到达95%,经过5τ到达99.3%。
在离散EMA里,τ = (1-α)/α · Ts。所以如果α越小,τ越大,滤波越平滑,但信号滞后越明显。滞后和噪声抑制是一对不可调和的矛盾,这一点在任何滤波器里都一样,EMA也不例外。
我在做IMU姿态解算时特别有感触:加速度计数据带高频振动噪声,想把噪声压下去就得减小α,但α一减小,姿态角响应就变慢,无人机稍微一晃,融合出来的角度滞后得让我一度怀疑传感器坏了。后来换成互补滤波结构,加速度计过EMA、陀螺仪做积分,再按权重融合,才解决响应和噪声的矛盾。核心思想是:不要指望一个低通滤波器既平滑又不滞后,结构设计才是关键。
3.3 离散化误差问题
严格来说,前面用一阶向后差分做的离散化是一种近似,它会在高频端引入一定相位误差。对于大多数传感器滤波和数据分析场景,这个误差是可以接受的。但如果你的系统对相位敏感(比如音频信号处理、闭环反馈控制),推荐用双线性变换(Tustin变换)来离散化,精度更高,但推导会复杂一些。EMA这个形式本身就是向后差分离散的结果,所以它适用的场景也是以“信号趋势提取、噪声平滑”为主,不适合对相位一致性要求极高的场合。
4. 参数整定实战:α、窗口长度还是时间常数
4.1 用一个无量纲系数还是时间常数
EMA实现起来只需要一个递推式,但参数管理上有两种思路:
- 直接调α:0到1之间的小数,1时完全不滤波,越接近0越平滑。
- 调时间常数τ + 采样周期Ts:α = Ts / (τ + Ts),或在α << 1时近似α ≈ Ts / τ。
我强烈推荐在工程代码里用时间常数τ作为配置参数,而不是直接暴露α。原因很直接:α本身没有物理意义,换一个采样率就得重新调,而τ的单位是秒,代表着滤波器的响应时间常数,换平台、换采样率时只需要重新计算α,而且可以从“信号上需要滤掉多高频的噪声”反推出合理的τ。
举个例子:单片机以100Hz采样,想要把截止频率放在5Hz附近。用近似公式:
α ≈ 2π · f_c · Ts = 2π · 5 · 0.01 = 0.314
如果更严谨一点,用精确关系反推:τ = 1/(2π·f_c) = 0.0318s,α = Ts/(τ+Ts) = 0.01/(0.0318+0.01) ≈ 0.239。近似和精确之间差了约1.3倍,对一般工程够用,但严格对照截止频率时应以精确公式为准。
4.2 不同场景的经验参数区间
根据我做过的几个项目,给一些参考区间(前提是信号采样率已知):
| 应用场景 | 典型采样率 | 建议τ区间 | 实际效果参考 |
|---|---|---|---|
| 加速度计静态倾角测量 | 50-200Hz | 0.2-1s | 1s基本能把微小的手抖滤掉,但倾斜响应也慢 |
| IMU角速度去噪 | 100-1000Hz | 0.02-0.1s | 兼顾动态响应,避免相位滞后过大 |
| 压力传感器/称重 | 10-50Hz | 0.5-2s | 称重场景平稳优先,可容忍较大滞后 |
| 电流/电压DC采样 | 1-10kHz | 0.1-1ms | 主要抑制开关噪声,τ过大反而看不清暂态 |
| 金融时间序列 | 日线/分钟线 | 指数加权半衰期对应3-20根K线 | 量价因子里的EMA参数含义就是半衰周期 |
这些参数不是拍脑袋定的,核心逻辑是:先明确信号中有效成分的频率范围和噪声频率范围,再用截止频率换算。举个例子,如果信号的有效变化周期大约是1s(0.5Hz),噪声在10Hz以上,截止频率放在2-3Hz比较合理,对应τ约为0.05-0.08s,α随采样率换算即可。
4.3 关于有效窗口长度
经常有人问“这个EMA相当于多少窗口的滑动平均”。把EMA的权重指数衰减曲线和滑动平均的矩形窗口对比,一个经验法则是:EMA的等效窗口长度大约为1/α(以采样点为单位),或者等效时间3τ。
这是个很有用的换算关系:如果你原来用10点滑动平均,那么一个α=0.1的EMA大概能产生类似的效果,但响应更快。不过EMA的尾部拖得长,对异常值(比如传感器偶发脉冲干扰)的响应是缓慢衰减的,不像滑动平均那样在一个窗口后彻底丢弃。所以如果你的噪声里混有偶尔的粗大误差,建议先用中值滤波剔除,再用EMA平滑。
5. 实操实现与边界问题处理
5.1 一个干净的C语言实现
嵌入式场景里,我习惯这样写一个EMA滤波器:
typedef struct { float alpha; float y_prev; uint8_t initialized; } EMA_Filter; float ema_filter_process(EMA_Filter *f, float x) { if (!f->initialized) { f->y_prev = x; f->initialized = 1; return x; } f->y_prev = f->alpha * x + (1.0f - f->alpha) * f->y_prev; return f->y_prev; }初始化时把输出直接赋成第一次输入值,这是一个关键的“冷启动”策略。如果不这么做,滤波器初始输出为0,给一个非零信号后需要好几个τ才能收敛到真实值附近。想象一下数据从5V的传感器过来,第一帧就跳到0.3,信号本身被严重扭曲,这在很多系统里是不可接受的。
5.2 初始化策略的三种选择
初始化策略可以按场景选:
- 首值初始化:最简单,第一次输入直接赋值,适合大多数在线滤波场景。
- 零值初始化:如果信号本身就接近0,可以不做处理,但要注意开始阶段输出会出现明显的上升过程。
- 预填充初始化:用之前记录的一段平均值作为y_prev初始值,适合系统重启后需要快速恢复到稳态的场景。
我自己在开发一个目标跟踪项目时遇到过这个问题:每次物体出现时,距离传感器数据从“无目标”变成“有目标”会跳变,如果滤波器的y_prev还停在0,那么前几帧的滤波输出完全失真。最后是把“无目标”状态下的历史均值保存下来,重新进入时预填进去,效果立竿见影。
5.3 Python快速验证与可视化
做数据分析时我通常先用Python验证参数再搬到嵌入式代码里,这样可以快速调整参数。核心实现只有几行:
import numpy as np def ema_filter(x, alpha, init=None): n = len(x) y = np.zeros(n) if init is None: y[0] = x[0] else: y[0] = init for i in range(1, n): y[i] = alpha * x[i] + (1 - alpha) * y[i-1] return y配合matplotlib画一下原始数据和滤波后的对比,参数合不合适一眼就能看出来。测试时可以构造一个带噪声的正弦信号,噪声幅度调到信号幅度的20%左右,来回调α看时域效果和频谱响应,比空想参数靠谱得多。
5.4 整型平台上的定点实现
很多MCU没有浮点单元,直接用float在中断里做运算很浪费CPU。此时可以把α量化为Q15格式,参考实现如下:
#define ALPHA_Q15 3277 // 相当于0.1 int16_t ema_process_q15(int16_t x, int16_t y_prev) { int32_t tmp = (int32_t)ALPHA_Q15 * x + (int32_t)(32768 - ALPHA_Q15) * y_prev; return (int16_t)(tmp >> 15); }这个实现需要注意几点:x和y_prev都要按实际物理量缩放成同一量纲的整数;中间乘积用int32_t累加避免溢出;右移15位相当于除32768,缩放比例和Q格式要统一。α用0.1对应32768×0.1≈3277。好处是只做乘法、加法和移位,在Cortex-M0这类低端内核上也能在几个微秒内跑完。
6. 与其他平滑算法对比:边界在哪里
6.1 和滑动平均(SMA)的对比
滑动平均的输出等于最近N个点的算术平均,窗口内每个点权重一致。它的优点是:对白噪声的衰减是线性叠加,N越大越平滑;但缺点是:需要维护缓冲区,对阶跃信号的响应延迟固定为N/2个采样点,且容易出现振铃式的过冲。
EMA更偏向近期数据,历史数据权重指数衰减。响应速度比SMA快,尤其在信号突变的场景下,EMA能更快地跟上趋势。但EMA对单个异常点的“记忆”更久,异常值的残留痕迹会在后面几帧慢慢消失而不是一次性丢掉。
从频域看两者差异更清楚:SMA的幅频响应有周期性零点,若干扰频率恰好落在零点上时能被完全滤除,而EMA没有这种特性。如果干扰是固定频率(比如50Hz工频),SMA配合窗口长度,理论上可以做到零增益,EMA只能做到一定程度衰减。
6.2 和卡尔曼滤波的对比
卡尔曼滤波是“滤波”里的集大成者,能处理带噪声的线性动态系统,输出状态估计和协方差。很多初学者一上来就奔着卡尔曼去,实则对多数“单变量、近似稳态信号”来说,卡尔曼退化成固定增益的形态,和EMA是等价的。
在实际项目中我的选择逻辑是:非线性、多传感器融合、噪声统计特性变化明显的场景,才值得上卡尔曼;单纯去个毛刺、平滑个趋势线,EMA就够了。维护卡尔曼需要调Q矩阵、R矩阵,谁用谁知道,那是不亚于调PID的痛苦。
6.3 一阶滤波、二阶滤波和更高阶滤波的取舍
一阶EMA对高频噪声的衰减斜率只有-20dB/十倍频程,想获得同样的高频抑制能力,代价是截止频率压得很低,进而带来巨大的信号滞后。如果对滞后敏感(比如电机电流环、飞行器姿态环),可以考虑二阶巴特沃斯低通或级联两个EMA。
级联两个EMA的递推就是连续做两次滤波。其等效的幅频特性在高频段衰减更陡,但相位滞后也更大。我实际用过这个方案处理编码器速度信号:一次EMA的噪声残余还是太大,级联后平滑多了,代价是速度环带宽打了折扣。最后把截止频率从原来的10Hz提到20Hz才平衡过来,噪声稍微多一点但动态响应能接受。所以说,滤波阶数不是越高越好,而是要在噪声抑制和动态响应之间找平衡点。
7. 常见问题与排查技巧实录
7.1 为什么不收敛到真实值
有人反馈“滤波器输出跟真实值之间始终有个固定偏差”,这种现象通常是输入信号自身有直流偏置,或者初始化值没对。如果初始化的y_prev是0而信号均值是10,那么输出会从10开始指数逼近真实均值,但经过几个τ后其实会收敛。如果一直有稳定偏差,检查一下是不是把滤波输出当成了真实值使用,而滤波本身就有相位滞后——换句话说输出的不是“当前时刻的真实值”,而是“一段时间前的平滑值”。
7.2 参数α换算错误
一个特别隐蔽的坑是把“平滑常数α”和“截止频率”混用。比如采样率从100Hz换成1000Hz后,α照搬过去,实际滤波效果完全不同。同样的α在100Hz采样下对应τ=0.1s(假设α近似0.1),在1000Hz下对应τ=0.001s,截止频率直接提高10倍,硬件没变,参数表现却天差地别。正确做法是固定τ,按新采样率重新计算α。
还有一个相关性很强的坑:EMA递推式里α和(1-α)是实数,如果单片机用整型运算时忘了把α量化到对应的Q格式,容易发生权重相加不为1的bug。这会导致输出缓慢漂移,排查起来很费劲。
7.3 输出震荡或出现超调
EMA是一阶系统,理论上对阶跃输入不会超调。如果看到明显的输出超调,大概率是输入源本身就是震荡的(比如脉宽调制输出带着纹波),或者滤波器已经被级联出二阶特性。还有一种情况:α太大,接近0.8以上,滤波器几乎没有平滑作用,高频噪声和信号自身波动都会直接透传出来,这时候与其说是震荡,不如说是没滤波。
7.4 参数在线调整要谨慎
有的系统需要在运行中切换滤波系数,比如根据运动状态切换大α和小α。直接改α的瞬间,如果y_prev是按旧α算出来的历史状态,切换后第一帧输出会出现跳变。稳妥做法是切换时用当前输出值重新初始化,或者做渐变过渡,分几十次逐步把α从旧值滑到新值,避免输出突变。
7.5 嵌入式中断里调用的注意事项
如果滤波器在定时器中断里跑,要注意它可能会被更高优先级的中断打断。EMA只是一个乘加运算,几个微秒就结束,中断嵌套问题不大。但如果用的是定点实现且中间变量是int32_t,要注意乘法的溢出问题,尤其是在信号量程本身就很大的情况下。建议先在PC上把整个量程峰值算一遍,确认中间乘积不会超出32位范围。
8. 写在最后:几个个人经验
做了这么多年信号采集和嵌入式控制,我越来越觉得“滤波器选择”不是越高级越好,而是越匹配越好。EMA和一阶低通滤波能覆盖绝大多数“单变量、稳态信号平滑”的需求,而且实现成本低、参数直观、调试容易。它不完美:高频抑制能力有限、相位滞后不可消除、对粗大误差无免疫力——但这些限制换个角度看,也恰恰是它的优点:可预测、稳定、不会给你塞出数据异常。
个人操作习惯上,有两件事几乎成固定流程:第一,任何滤波参数整定前,先花十分钟记录信号的原始波形和频谱,而不是凭感觉调α;第二,新平台适配滤波器时,一律先用Python离线仿真一遍参数,确认时域响应和频域目标都对得上,再搬进嵌入式代码。
如果你在项目中已经用了滑动平均,想平滑一些但又不希望滞后太大,试着换用EMA:先根据有效信号频率算截止频率,再换算出α,多数情况下效果会有明显改善。如果你正在用的EMA老是“滤不干净”,先别急着换二阶或者上卡尔曼,重新审视一下自己的截止频率是不是选得太大。工程问题很多时候不是算法不够好,而是参数和信号模型没对齐。