写这篇东西的起因,是我在同时处理传感器数据平滑和一组时序指标的实时计算时,发现团队里几个人对同一段代码给出了完全不同的解释:搞控制系统出身的同事叫它“一阶低通滤波”,做数据分析的同事坚持说这是“指数移动平均(EMA)”。两边都没错,但真正有意思的是,这俩名字底下其实是同一套数学结构。把事情彻底掰开揉碎之后,你会发现很多工程上的模糊地带都会变得特别敞亮。
指数移动平均和一阶低通滤波,几乎出现在你能想到的所有实时数据处理场景里:惯导模块的加速度计读数、热电偶的温度采样、编码器的速度估算、股票价格的均线指标、伺服电机的电流反馈整形。它们的共同任务就一句话:从一堆带噪声的离散采样点里,把趋势留下来,把高频抖动压下去。这篇文章不绕弯子,我直接讲清楚它们的数学本质、工程实现、参数标定,以及我在实际项目里踩过的那些坑。
如果你正在为“滤波参数到底怎么调”发愁,或者想搞明白为什么同一个公式在论文里长成两种模样,这篇内容应该能帮你省下不少查资料的功夫。看完之后你至少能回答三个问题:α 和截止频率怎么互相换算,离散实现时该注意什么,为什么某些场景下这个“最简滤波器”反而是最优解。
1. 核心概念:两个名字,一个爹
先说结论:离散域的指数移动平均和连续域的一阶低通滤波,在数学上是同一个系统,只是从不同的学科视角给了它不同的名字。理解这件事的起点,是先把两套表达式都写出来,对照着看。
1.1 指数移动平均的离散递推式
金融和数据分析里常见的形式是这样:
y[n] = α · x[n] + (1 - α) · y[n-1]
其中 x[n] 是当前采样值,y[n] 是当前输出,y[n-1] 是上一次输出,α 是平滑系数,取值范围是 0 到 1。
另一种等价的写法是增量式:
y[n] = y[n-1] + α · (x[n] - y[n-1])
这两种形式在浮点运算里结果完全一致,但增量式的好处是它告诉你一个直观事实:每次输出只是在旧输出的基础上,朝新采样值的方向挪动一小步。α 越大,挪动的步子越大,对新数据越敏感;α 越小,新数据的影响越微弱,曲线越平滑。
这个递推式的名字里带着“指数”,是因为它的脉冲响应是一个指数衰减序列。如果输入只有一个冲击,那后续每个周期的输出都是上一个周期输出的 (1-α) 倍,形成一个等比衰减的尾巴。这正是指数移动平均名字的由来。
1.2 一阶低通滤波的连续域原型
再来看信号处理和控制系统里的表述。一阶低通滤波器的连续传递函数长这样:
H(s) = 1 / (1 + s·τ)
其中 τ 是时间常数,单位是秒,s 是拉普拉斯算子。
这个式子描述的是一个模拟电路就能搭出来的系统——一个电阻串联一个电容,从电容两端取电压,就是经典的一阶 RC 低通滤波器。它的幅频特性是:低频信号几乎无损通过,高频信号按每十倍频程 20dB 的速率衰减。-3dB 截止角频率 ωc = 1/τ,换算成频率就是 fc = 1/(2πτ)。
我当年第一次把这两个式子放在一起看的时候,心里冒出的想法是:一个讲离散递推,一个讲连续频域,这俩怎么可能是同一个东西?直到我走了一遍从连续域到离散域的离散化推导,才彻底想明白。
1.3 从连续滤波器到离散 EMA 的完整推导
在数字信号处理中,把连续滤波器变成可编程实现的离散滤波器,最常见的做法是双线性变换或后向欧拉法。这里我用后向欧拉来推一遍,因为它的数学过程最直观,而且结果跟工程实践高度吻合。
后向欧拉法把拉普拉斯算子 s 近似为:
s ≈ (1 - z⁻¹) / T
其中 T 是采样周期,z⁻¹ 是单位延迟算子。把这个近似代入 H(s) = 1 / (1 + s·τ):
H(z) = 1 / (1 + τ · (1 - z⁻¹) / T)
分子分母同乘 T:
H(z) = T / (T + τ - τ·z⁻¹)
把分母整理成 1 - (1-α)·z⁻¹ 的形式:
H(z) = α / (1 - (1-α)·z⁻¹)
其中:
α = T / (T + τ)
到这里,结构已经非常清楚了:这个离散传递函数的输入输出关系,正是 y[n] = α·x[n] + (1-α)·y[n-1],跟指数移动平均完全一致。
所以结论是:你写的那行 y = y + alpha * (x - y),本质上就是一个经过后向欧拉离散化的一阶低通滤波器。两边讲的不是“类似”,而是“同一个东西在两种语境下的不同投影”。
2. 参数换算:α、τ、截止频率三者怎么互相转
搞清楚数学同源性之后,下一个实际问题就是:我手里有一个需要滤波的传感器信号,采样率是 1kHz,我想滤掉 5Hz 以上的波动,α 应该设多少?
这道题就是把上面的公式倒过来用。先把目标截止频率换算成时间常数:
τ = 1 / (2π·fc)
然后用采样周期 T 和 τ 计算 α:
α = T / (T + τ)
把假设数字代进去:T = 1/1000 = 0.001 秒,fc = 5Hz,τ = 1/(2π×5) ≈ 0.0318 秒,α = 0.001 / (0.001 + 0.0318) ≈ 0.0305。
也就是说,在 1kHz 采样率下,α 大约取 0.03,就能得到一个 -3dB 截止频率约为 5Hz 的低通滤波器。
这里有个必须提醒的点:用这个公式算出来的 α,对应的截止频率是模拟域意义上的 -3dB 点。由于离散化的映射关系,数字域的实际截止频率会跟这个目标值存在一个微小的偏差,具体大小取决于 α 的绝对值。α 越小(也就是采样率相对截止频率越高),偏差越小。对绝大多数工程场景来说,这个偏差完全在可接受范围内。
我在实际项目里通常不会把数学算到极致,而是用这个公式算出初值,然后通过观测波形微调。因为真实系统的噪声特性和你对“平滑度”的主观感受,很难用单一频率指标完全刻画。算出一个量级正确的初值,再用频谱分析或者直接看时域波形调,是最有效率的做法。
另外还有一个常见的工程近似值得记住:当 α 远小于 1 时,截止频率可以用这个简易公式估算:
fc ≈ α / (2π·T)
这个近似式在 α 小于 0.1 时误差很小,而且特别好心算。比如 1kHz 采样、α = 0.01,那 fc 大约就是 0.01 / (2π×0.001) ≈ 1.59Hz。我经常在调试现场用这个公式快速估一下当前参数的量级,心里先有个数,再动手调。
2.1 α 的可视化直觉
很多初学者对 α 和响应速度之间的关系缺乏直觉。我给一个参照系:α 越大,滤波器的“记忆”越短,输出跟随输入的速率越快,但噪声也保留得越多;α 越小,记忆越长,输出越平滑,但滞后也越严重。
这里有一个常用的量化工具叫时间常数 τ 的离散对应。连续系统里,阶跃响应上升到终值的 63.2% 需要 τ 这么长时间。对应的离散实现里,用掉的时间大约是 τ ≈ T/α(由 α = T/(T+τ) 在 α 很小时反推得到)。所以一个 α = 0.1 的滤波器,在 1kHz 采样下,响应到目标值的大约六成需要 10 毫秒左右。想要更平滑就调小 α,想要更灵敏就调大 α,这是最核心的取舍。
2.2 与滑动平均的差异
说到平滑,很多人会自然想到滑动平均(moving average),也就是取最近 N 个点求均值。既然都是平滑,选谁?
我的经验是分场景:
- 滑动平均没有递归结构,不存在累计误差,实现起来也无脑,但需要缓存 N 个历史值,内存开销随 N 线性增长,而且相位滞后是线性的、比较难调。
- EMA 只存一个状态变量,内存几乎可以忽略,计算量是常数级别,适合嵌入式和高频场景。缺点是它对数据的“记忆”理论上是无限长的,只是越老的数据权重越低。
从频域角度看,滑动平均的频率响应有一个固定的零点梳状结构,在特定频率处会出现完全陷波,而 EMA 的幅频响应是单调下降的。如果你的噪声集中在某个特定频率,滑动平均可能在那个频点附近表现更好;如果噪声是宽频的,EMA 的性能通常更令人满意。
有一个实际体验我记得很清楚:做编码器速度估算时,滑动平均窗口稍大一点,响应就慢得让人难以接受,而 EMA 在同等平滑效果下,滞后要小不少。原因在于 EMA 对近期数据赋予了更高的权重,而滑动平均对窗口内所有数据一视同仁,看起来“平滑”,实际上把最新的有效信息也平均掉了。
3. 实操视角:不同场景下的参数整定方法
理论都知道之后,实操才是真正分高下的地方。这一节我把最常见的几类使用场景单独拎出来,讲一讲参数整定的思路和细节。
3.1 传感器噪声滤除
这是 EMA 最常见的应用。我用 MPU6050 这类 IMU 模块做倾角测量时,原始读数带的高频抖动非常明显。此时的核心目标是让滤波后的曲线“稳而准”。
参数整定思路分三步:
第一步,先确定采样率。IMU 一般可以配置到 100Hz 到 1kHz,我通常建议在功耗和计算资源允许的前提下,采样率越高越好,因为过采样本身就能帮助抑制噪声。
第二步,估算噪声的主要频率范围。把传感器静止放在桌面上,用串口抓一段原始数据,在电脑里做一次 FFT,找出抖动能量聚集的频段。假设抖动主要集中在 10Hz 以上,那我希望截止频率砍到 5Hz 以下,这样不平滑都难。
第三步,用公式算 α 初值,再现场微调。还以 500Hz 采样为例,想砍到 5Hz,τ = 0.0318 秒,T = 0.002 秒,α = 0.002/(0.002+0.0318) ≈ 0.059。取 0.06 起步,看波形,如果还是抖就降到 0.04,如果反应太迟钝就升到 0.08。这样调出来的参数,通常比拍脑袋设一个 0.1 或者 0.01 要靠谱得多。
3.2 控制环路的反馈整形
在控制领域,一阶低通滤波经常被用在反馈通道上。比如电机电流环的采样信号,如果不用滤波器,PWM 开关噪声会直接进入控制环,引起电流波动甚至振荡。
但控制环路的滤波有一个特殊约束:相位滞后会影响系统稳定性。你在反馈通道上加了一个低通滤波器,就等于给整个闭环增加了一个滞后环节,相位裕度会下降。所以这里的参数整定不能只盯着“平滑”,还要盯着“系统稳不稳”。
我一般会用两个指标约束参数选择:一是滤波器引入的相位滞后在穿越频率附近不能超过某个阈值,比如 10 到 15 度;二是滤波后的信号噪声峰峰值要低于控制要求的纹波范围。
具体做法:先测出系统的穿越频率 fc_loop,然后把滤波器的截止频率设得比它高 5 到 10 倍。比如穿越频率是 50Hz,滤波器截止频率可以取 250 到 500Hz。这样做能保证在穿越频率附近,滤波器带来的相位滞后很小,不至于影响稳定性。如果你发现这个约束和噪声抑制的需求冲突了,那就得考虑改用更高阶的滤波器,或者采用状态观测器来避免滞后问题。
这里我踩过一个很典型的坑:某次做直流电机速度闭环,我看速度反馈波形毛刺多,为了画面好看,把 EMA 的 α 调得很小,截止频率压到比系统穿越频率还低。结果电机跑起来开始低频振荡,速度曲线变成了一条上上下下的蛇形。后来用带宽分离的思路重新整定,把滤波器截止频率拉高到穿越频率的三倍以上,波形既平滑,系统也稳定了。
3.3 金融时序数据处理
在量化分析里,EMA 最常见的用法之一是计算趋势类指标,比如 MACD。这里的“滤波”目标不是还原物理量的真实值,而是提取价格走势的趋势成分。
金融数据的特征是非平稳、强噪声、事件驱动。用 EMA 时,通常不追求精确的截止频率,而是按“周期”来定义参数。比如常见的 12 周期 EMA、26 周期 EMA,这个周期数 N 和 α 的换算关系是:
α = 2 / (N + 1)
如果 N=12,α≈0.154;N=26,α≈0.074。这套换算在技术分析领域已经约定俗成,几乎所有的行情软件都是这么算的。
我做回测时还会额外注意一件事:金融数据存在跳空、停牌这类异常事件,普通的 EMA 递推会把这类异常点直接计入状态,导致指标在一段时间内处于失真状态。实践中我会对输入数据做预处理,比如剔除异常值或对跳空做复权处理,避免脏数据污染滤波器状态。
4. 嵌入式环境下的工程实现细节
如果上面的内容是从算法角度讲 EMA,这一节切换到工程实现视角。嵌入式环境跟桌面端有本质差异:可能没有浮点运算单元,数据类型位数有限,实时性要求高。在这种环境下实现 EMA,有很多用模拟器永远遇不到的坑。
4.1 基于定点数的高效实现
在常见的 Cortex-M 系列单片机或者低端 DSP 上,浮点运算不是不能用,但相对于定点运算慢不少。如果你的系统里 EMA 只是偶尔算一次,那无所谓;如果它是某个控制环里每个采样周期都要跑的环节,浮点开销可能就会成为瓶颈。
一个经典的做法是把 α 近似为 1/2^k 的形式。因为右移 k 位的运算在硬件上是一两个时钟周期的事,而浮点乘法少说也要几十个周期。用这个近似之后,EMA 的更新变成一个加法和一个右移:
y = y + (x - y) >> k
这里 k 相当于 α = 1/2^k。查一下之前的换算:α = 0.03 时,最接近的 2 的幂是 1/32 ≈ 0.03125,对应 k=5。如果你不追求极端精度,这个近似完全够用。
定点实现时最需要注意的是整数除法和符号位移的坑。右移负数和正数的行为在 C 语言标准里是有区别的,如果 (x - y) 可能为负,建议先判断符号再位移,或者使用算术右移(大多数编译器在带符号类型上默认做的就是算术右移,但为了可移植性还是建议显式处理)。
4.2 状态变量与初始化的细节
EMA 的核心状态只有一个变量 y,它的初值直接影响启动阶段的输出。我见过不少实现直接把 y 初始化成 0,结果系统一上电,输出从 0 慢慢爬到真实值,这段爬升的暂态过程在某些场景下会导致误动作。
好的做法是:如果系统允许,用第一个采样值初始化 y,也就是 y[0] = x[0]。这样输出从一开始就跟真实值接近,不需要等待收敛。对于传感器滤波,这个做法几乎零成本,强烈推荐。
另一个和初始化相关的问题是状态变量的类型。如果你在定点平台上做滤波,y 的精度决定了稳态时的纹波大小。举个例子:用 8 位整数保存 y,而真实信号本身的幅度范围是 0 到 255,那滤波器输出的分辨率就只有 1 个 LSB,α 再小也无法消掉量化噪声。经验法则:状态变量至少要比输入数据宽 4 到 8 位。输入是 12 位 ADC,状态变量至少用 16 位;输入是 16 位,状态变量建议用 32 位。否则你会遇到一个诡异现象:信号已经足够平滑了,但输出曲线总是有一两个 LSB 的反复跳动,怎么调 α 都没用。
4.3 时间基准变化导致的参数失真
EMA 的 α 是跟采样周期绑定的:α = T/(T+τ)。如果你的采样周期是不稳定的——比如主循环里轮询传感器、偶尔被更高优先级的任务抢占——那 EMA 的实际截止频率就会跟着采样周期漂移。
在时间抖动不能接受、但又需要稳定滤波特性的场合,我有两个建议:
第一个建议是改用基于时间戳的增量式计算。每次更新时不使用固定 α,而是根据实际间隔 dt 重新计算等效 α:
α = 1 - exp(-dt/τ)
这个公式是从连续域阶跃响应的离散化直接导出的,在变采样周期下依然能维持物理含义不变的时间常数 τ。代价是多算一个指数函数,如果 CPU 紧张可以用查表或者多项式逼近。
第二个建议是干脆把滤波移到定时器中断里做。用固定的采样基准去读传感器和执行滤波,主循环只负责消费滤波结果。这样 α 可以保持常数,代码也更简单。我大部分项目中都倾向这种方案,因为它把采样抖动和滤波特性完全解耦了。
4.4 参数突变与状态重置
还有一种场景容易被人忽略:滤波器的输入源突然切换,或者系统从掉线状态恢复过来。比如你有一个冗余传感器系统,A 通道故障后切到 B 通道。此时 B 通道的当前读数和 A 通道的滤波输出可能存在一个很大的差值,如果 EMA 状态没有重置,滤波输出会以一个较大的 α 步长缓慢逼近新值,产生一个长长的过渡斜坡。
处理方式很简单:在信号源切换的事件里,把 EMA 的 y 重置为当前新通道的首个采样值。这个操作看起来不起眼,但在航空航天、医疗器械这类对信号连续性要求很高的场景里,能省掉一大堆时序上的麻烦。
5. 常见问题与排查技巧实录
最后这一部分,把我在多年实践中遇到的高频问题整理成一个速查表。这些坑的特点是不踩不知道,踩了之后回头看都觉得“怎么这么蠢”。
5.1 问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 输出完全不跟随输入 | α 为 0 或状态未更新 | 打印 y 和 x 比对 | 检查递推式是否正确执行,确保 α 非零 |
| 输出跳变剧烈 | α 过大 | 看频谱确认滤波效果 | 调小 α,或用频响公式重新计算 |
| 启动阶段有长斜坡 | 状态 y 初值为 0 | 观察前 100 个点输出 | 用首个采样值初始化 y |
| 固定 α 但滤波效果时好时坏 | 采样周期不稳 | 记录相邻采样的时间戳 | 改定时器采样,或基于 dt 重算 α |
| 输出有打不开的细小纹波 | 状态变量位宽不足 | 把 y 的位宽翻倍测试 | 加宽状态变量至输入位宽 +4 或更多 |
| 定点实现时数值溢出回绕 | 差值 (x-y) 超出类型范围 | 打印中间变量 | 用更宽类型存差值,或限制输入范围 |
| 输出长期偏离真实值 | 输入含有直流偏置或传感器未校准 | 对比原始数据均值 | 先校准传感器,再做滤波 |
| 切换输入源后过渡过长 | 状态 y 未重置 | 观察切换瞬间波形 | 事件触发时重置 y |
5.2 我亲历的两个疑难问题
第一个是 ADC 数据滤波时遇到的“拖尾”。现象是电机急停之后,电流读数虽然主体掉得很快,但尾巴上总有一段长时间的小幅漂移,看起来像是传感器出了问题。排查了很久才发现,罪魁祸首是滤波前的数据里有一个非常低频的偏置漂移。EMA 对这种极低频分量几乎没有任何衰减,所以它被完整地传递到输出端。解决办法不是调滤波器,而是先做一次硬件上的偏置校准或者软件上的漂移补偿。这个案例让我意识到:滤波不是万能的,它只能处理它该处理的那部分问题。
第二个是浮点平台的精度问题。在 PC 上用 Python 做回测时,一切正常;同样的公式移植到嵌入式板子上的单精度浮点环境后,输出出现了一种周期性的微小跳动。原因是单精度浮点只能表示约 7 位十进制有效数字,当 x 和 y 的数值都很大的时候,增量 α·(x-y) 可能会被舍入到不可忽略的误差。解决方法是改用双精度,或者在累加时调整计算顺序。这类问题在仿真是发现不了的,只有真机长时间跑才会暴露。
5.3 调试技巧与观察点
调试滤波算法时,最有用的工具是三条曲线:原始输入、滤波输出、两者之差。输入和输出告诉你“平滑得怎么样”,差值告诉你“滤掉了什么”。如果差值里还有明显的周期性分量,说明截止频率设得还不够低;如果差值几乎为零,说明大部分信号被保留,滤波器形同虚设。
另一个我常用的技巧是给系统一个阶跃输入,观察滤波输出的上升过程。在阶跃响应曲线上,你可以非常直观地看到时间常数:从阶跃开始到输出到达目标值的 63.2%,所需时间就是 τ。我当时用这个办法校准了一批传感器的滤波参数,比对着频谱图瞎猜高效得多。
最后说一个心法:别把参数调得过于“激进”。平滑度提升的边际收益是递减的,而滞后和相位损失的代价却是线性甚至超线性增长的。我见过不少工程师为了把波形调得“像丝一样滑”,把 α 压到特别小,结果系统动态响应一塌糊涂。滤波的本质是信息取舍,你在哪个频段保留信息,必然在时域上付出相应的滞后代价。做一个清醒的取舍者,比做一个激进的平滑者重要得多。
回到开头那个问题:指数移动平均和一阶低通滤波到底是不是同一个东西?我现在可以给一个简洁的答案:它们是同一个物理系统在连续域和离散域的两个化身。理解这层关系之后,你在任何一门课里学到的直觉——关于时间常数、截止频率、阶跃响应的所有经验——都可以无缝移植到另一门课的代码里。这种“底层相通”的洞察,才是比任何具体公式都值钱的东西。