1. 藏在“平滑”背后的同一件事
很多人第一次接触指数移动平均(EMA),是在量化交易或者数据可视化里,为了把一条毛刺丛生的曲线抹得顺滑一些。第一次接触一阶低通滤波(First-Order Low-Pass Filter),则多半是在单片机读取传感器数据、处理ADC采样值的时候。乍看之下,一个属于统计分析领域,一个属于信号处理领域,八竿子打不着。但只要你把两者的公式并排放在一起,就会发现:这根本就是同一件事的两种写法。
EMA的递推公式长这样:
[ y[n] = \alpha \cdot x[n] + (1 - \alpha) \cdot y[n-1] ]
一阶低通滤波器的离散形式长这样:
[ y[n] = \frac{T_s}{T_s + RC} \cdot x[n] + \frac{RC}{T_s + RC} \cdot y[n-1] ]
把两个公式对照一下,(\alpha) 对应的就是 (\frac{T_s}{T_s + RC})。也就是说,指数移动平均在离散域天然就是一个一阶低通滤波器,两者的系数只是用不同语言描述了同一个物理过程。这个等价关系一旦看穿,很多选型问题就变得非常简单——你在某个场景积累的调参经验,完全可以直接迁移到另一个场景里去用。
这篇文章我会从两者等价的原理讲起,拆解系数换算关系,再给出工程实践里常踩的坑和不同场景下的选型建议。适合正在做嵌入式信号处理、数据分析、或者量化策略特征工程的读者,尤其是那种“明明知道EMA是滤波,却说不出滤波器参数怎么算”的中间状态的人。
2. 从差分方程到物理直觉:为什么这两个公式是同一回事
2.1 从模拟滤波器到数字滤波器的推导路径
要彻底理解两者的等价性,得先从模拟域的一阶RC低通滤波器说起。一个电阻(R)和一个电容(C)串联,输入电压(x(t))加在两端,从电容两端取输出电压(y(t)),电路的微分方程是:
[ RC \cdot \frac{dy(t)}{dt} + y(t) = x(t) ]
这是最经典的一阶系统。截止角频率(\omega_c = 1/(RC)),对应截止频率(f_c = 1/(2\pi RC))。低频信号能顺利通过,高频信号被衰减,这就是“低通”二字的来源。
现在要把这个模拟滤波器数字化。用一阶后向差分法近似微分项:
[ \frac{dy(t)}{dt} \approx \frac{y[n] - y[n-1]}{T_s} ]
其中(T_s)是采样周期。代入微分方程:
[ RC \cdot \frac{y[n] - y[n-1]}{T_s} + y[n] = x[n] ]
整理一下:
[ (RC + T_s) \cdot y[n] = T_s \cdot x[n] + RC \cdot y[n-1] ]
两边同除以((RC + T_s)):
[ y[n] = \frac{T_s}{RC + T_s} \cdot x[n] + \frac{RC}{RC + T_s} \cdot y[n-1] ]
如果你把(T_s/(RC + T_s))记作(\alpha),那么(\frac{RC}{RC + T_s} = 1 - \alpha)。公式就变成了EMA的标准形式:
[ y[n] = \alpha \cdot x[n] + (1 - \alpha) \cdot y[n-1] ]
这个推导是我认为理解两个概念等价性的最短路径。不需要什么高深的数学工具,一个差分近似加一次代数变形就够了。
2.2 把时间常数和半衰期讲明白
现在关键问题来了:EMA只有一个参数(\alpha),一阶低通滤波器有两个参数(R)和(C),但这两个参数组合起来也只有一个自由度——时间常数(\tau = RC)。所以两者本质上都是一个参数的算法族。
在模拟域里,时间常数(\tau)的物理意义非常直观:当输入是一个阶跃信号时,输出从0上升到最终值的63.2%需要的时间就是(\tau)。这个“63.2%”源于(1 - e^{-1} \approx 0.632)。
在EMA的语境里,(\alpha)和时间常数之间的关系是:
[ \tau = -\frac{T_s}{\ln(1 - \alpha)} ]
这个公式是怎么来的?EMA的冲激响应是:
[ h[n] = \alpha \cdot (1 - \alpha)^n ]
把离散时间(n)换成连续时间(t = n \cdot T_s),当(T_s)远小于(\tau)时,((1 - \alpha)^n \approx e^{-n \cdot T_s / \tau})。于是:
[ 1 - \alpha = e^{-T_s / \tau} ]
取自然对数:
[ \ln(1 - \alpha) = -\frac{T_s}{\tau} ]
[ \tau = -\frac{T_s}{\ln(1 - \alpha)} ]
不过这个公式在工程上用得不多,因为反直觉。工程上更常用的直觉是“半衰期”——也就是一个阶跃输入进来,输出爬到幅度一半所需要的时间。指数衰减的半衰期公式是:
[ t_{1/2} = \tau \cdot \ln 2 \approx 0.693 \cdot \tau ]
换算成以(\alpha)表达:
[ N_{1/2} = \frac{\ln 0.5}{\ln(1 - \alpha)} ]
这个(N_{1/2})就是“需要多少个采样周期,输出才能到达最终值的一半”。举个例子,(\alpha = 0.1)时:
[ N_{1/2} = \frac{-0.693}{\ln(0.9)} \approx \frac{-0.693}{-0.105} \approx 6.6 ]
也就是说,每隔大约6.6个采样周期,输出向着输入方向走完剩余距离的一半。这和“时间常数(\tau)”表达的是同一种性质的指标,只是用了不同的刻度尺。
我个人在实践中发现,和产品经理或者不了解信号的同事沟通时,说“这个滤波器的响应需要大约40个采样周期才能跟上一半的变化”比说“时间常数是XX毫秒”要直观得多。很多人一听时间常数就发怵,但半衰期这个概念谁都能理解。
2.3 截止频率的精确换算
如果你的团队里有人问“这个EMA对应的截止频率是多少”,你需要给出精确答案。把(\tau = RC)代入截止频率公式:
[ f_c = \frac{1}{2\pi \tau} = \frac{1}{2\pi \cdot RC} ]
再把(\tau = -T_s / \ln(1 - \alpha))代进去:
[ f_c = -\frac{\ln(1 - \alpha)}{2\pi \cdot T_s} ]
又因为采样频率(f_s = 1/T_s),可以写成:
[ f_c = -\frac{f_s \cdot \ln(1 - \alpha)}{2\pi} ]
这个公式的实用性非常高。比如你有一个以100Hz采样的传感器信号,想把50Hz以上的噪声滤掉,先算:
[ \ln(1 - \alpha) = -\frac{2\pi \cdot f_c}{f_s} = -\frac{2\pi \cdot 50}{100} \approx -3.1416 ]
[ 1 - \alpha = e^{-3.1416} \approx 0.0432 ]
[ \alpha = 0.9568 ]
这个(\alpha)值非常大,意味着每个新采样点几乎完全覆盖旧值。这是对的,因为50Hz截止频率在一个100Hz采样率的系统里已经接近奈奎斯特频率了,此时滤波器几乎没有平滑效果——它已经把“有用信号”和“噪声”的界限推到了采样极限附近。
反过来,如果你在一个1kHz采样的系统里想要20Hz的截止频率,工程上更自然的做法是先取时间常数(\tau = 1/(2\pi \times 20) \approx 7.96)毫秒,然后算(\alpha = 1 - e^{-T_s/\tau} = 1 - e^{-0.1254} \approx 0.1178)。这时候(\alpha = 0.1178)的含义就很直观:每个新采样点只带来约12%的权重,平滑效果比较明显。
这里顺便纠正一个网上经常出现的错误说法。有些资料会把截止频率写成(f_c = \alpha / (2\pi T_s)),这是不准确的,除非(\alpha)非常小。更严格的说法是(f_c = -\ln(1-\alpha) / (2\pi T_s))。两者在(\alpha)趋于0时趋近一致,但(\alpha)稍大一点偏差就开始显现。
| (\alpha) | 近似公式(f_c/f_s) | 精确公式(f_c/f_s) | 相对误差 |
|---|---|---|---|
| 0.01 | 0.00159 | 0.00160 | 0.6% |
| 0.05 | 0.00796 | 0.00817 | 2.6% |
| 0.1 | 0.01592 | 0.01675 | 5.3% |
| 0.3 | 0.04775 | 0.05679 | 19% |
| 0.5 | 0.07958 | 0.11032 | 38.6% |
从表里可以直观看出,(\alpha)超过0.1以后,近似公式的误差开始快速膨胀,在(\alpha = 0.5)时误差接近40%。所以在做正经设计时,建议用精确公式,除非你只是想要一个粗糙的量级估计。
3. 调参这件事,为什么那么多人在“灵敏度”上翻车
3.1 灵敏度陷阱:(\alpha)的微小变化在两端区间的影响天差地别
调参这件事看起来毫无技术含量:改一个数字,看效果,不行再改。但实际上,由于EMA的参数(\alpha)对输出特性的影响不是线性的,很多人调到怀疑人生。
来看(\alpha)和半衰期(N_{1/2})的关系。当(\alpha)接近0时,(1-\alpha)接近1,(\ln(1-\alpha))的变化非常缓慢。(\alpha)从0.01改到0.02,(N_{1/2})大约从69降到34.5——减半。当(\alpha)接近1时,(\ln(1-\alpha))的变化又变得非常剧烈。(\alpha)从0.9改到0.95,(N_{1/2})从6.6变成了13.5——翻倍还多。而在中间区间,比如(\alpha)从0.1改到0.2,(N_{1/2})从6.6变成3.1——也是缩减一半以上。
这带来一个实践结论:用(\alpha)作为调参旋钮,除非你很熟悉目标响应时间,否则很难一眼看出该往哪个方向拧、拧多少。相比之下,用时间常数(\tau)或者半衰期的采样周期数去设计,再反推(\alpha),可控性会好很多。
我见过不少人在传感器滤波上遇到“信号太毛糙”的问题,操作方法就是把(\alpha)从0.2往0.1方向一路砍,结果发现信号平滑了,但跟踪延迟也变得难以接受。然后又往回抬,反复横跳。根因就是他根本没有先明确“我能接受的延迟是多少个采样周期”,而是凭感觉在试。
正确的做法是先定延迟预算。比如你的系统要求:输入发生一个单位阶跃变化后,输出必须在0.5秒内到达目标值的90%。采样周期为10ms,那么就是50个采样周期内到达90%。指数衰减到10%剩余需要的时间是(\tau \cdot \ln(10) \approx 2.3026 \cdot \tau),所以:
[ 2.3026 \cdot \tau \leq 0.5\text{s} \Rightarrow \tau \leq 0.217\text{s} ]
采样周期10ms,即(T_s = 0.01)s,所以:
[ \alpha = 1 - e^{-T_s/\tau} = 1 - e^{-0.01/0.217} = 1 - e^{-0.0461} \approx 0.045 ]
这时候你得到的是一个有明确物理意义的(\alpha)值,而不是拍脑袋拍的。整个过程只需要一张草稿纸加一台计算器。
3.2 初值问题:很多人忽略的第一帧
EMA的递推式里有一个初始项(y[-1])或者(y[0]),很多人直接取0,结果前几十帧数据看起来总有一段诡异的爬升。这在传感器滤波场景里尤其明显——上电瞬间,如果初始值设为0,而传感器实际读数是800,那输出就要花一段时间从0慢慢爬到800附近,这段时间里控制系统可能已经因为“看到”的数值异常而做出错误动作。
解决初值问题有几种方案,按推荐程度排序:
- 直接把第一帧原始采样值作为初始值,也就是(y[0] = x[0])。最简单,绝大多数场景下效果最好。
- 用开机后前N帧的平均值作为初始值。适合传感器有稳定偏置且噪声较大的场景,但会增加几十毫秒的启动延迟。
- 用预设的经验值。适合已知工作点比较稳定的系统,比如电机电流为空载电流附近,可以直接预设一个空载值。
我实际写代码时几乎总是用第一种方案,就一行:
static float y = 0.0f; static uint8_t first_frame = 1; float ema_filter(float x, float alpha) { if (first_frame) { y = x; first_frame = 0; } else { y = alpha * x + (1.0f - alpha) * y; } return y; }这个做法基于一个简单的观察:在大多数嵌入式系统里,传感器刚上电时的第一帧数据和真实值之间的误差,要远小于“从0开始爬升”造成的收敛误差。两害相权取其轻。
3.3 截止频率中隐含的采样稳定性要求
还有一个细节经常被忽略:EMA作为一阶低通滤波器的离散实现,有一个隐性的稳定性先决条件——采样间隔必须相对稳定。这个条件不是(\alpha)决定的,而是离散化过程本身就假定了等间隔采样。
后向差分法推导出的离散一阶低通滤波器,其极点为(z = 1 - \alpha)。这个极点始终在单位圆内(只要(0 < \alpha < 2)),所以系统本身是稳定的。但如果你的采样周期不稳定,实际起作用的(\alpha)是在波动的。比如你设计(\alpha = 0.1)、(f_s = 100\text{Hz}),但系统的RTOS调度偶尔抖动,某次采样的实际间隔变成了30ms而不是10ms,那么这次实际等效的(\alpha)就偏大了。累积下来,输出的平滑度和延迟都会偏离设计值。
在采样间隔波动较大的场景(比如某些事件驱动的传感器读取),建议不要直接用固定(\alpha),而是根据实际时间间隔动态计算(\alpha):
float compute_alpha(float tau, float dt) { return 1.0f - expf(-dt / tau); }这样虽然每次都要调一次(\exp),但换来的是在任何采样间隔下都有一致的时间常数响应。对于运行在MCU上的场景,(\exp)的运算成本可能偏高,可以改用近似公式或者查表。但如果只是几百Hz的采样率,大部分MCU跑一次浮点(\exp)也就是几个微秒的事,完全在预算之内。
3.4 参数在两个领域里的“翻译表”
由于EMA和低通滤波器太常出现在不同知识背景的讨论里,我整理了一张换算表,方便跨领域协作时快速对齐语言。这张表我自己用了很久,每次和做量化的朋友或做嵌入式的同行交流,都能省不少解释成本。
| 统计/量化语境 | 信号处理语境 | 换算关系 |
|---|---|---|
| (\alpha)(平滑系数) | 滤波器增益系数 | (\alpha = 1 - e^{-T_s/\tau}) |
| 半衰期 (N_{1/2})(个采样周期) | 时间常数 (\tau) | (\tau = N_{1/2} / \ln 2) |
| 衰减速度 | 截止频率 (f_c) | (f_c = -\ln(1-\alpha)/(2\pi T_s)) |
| 更小的(\alpha) | 更低的截止频率 | 两者都意味着更强的平滑/更大的延迟 |
| 对异常值的鲁棒性 | 脉冲响应衰减速度 | 越接近指数衰减越平滑,非指数衰减会引入振铃 |
这张表最重要的是第一行和第三行。只要记住了(\alpha = 1 - e^{-T_s/\tau}),你就可以在“时间常数”和“平滑系数”两套话语体系之间自由切换。
4. 一阶滤波的边界:什么时候它不够用,以及备选方案
4.1 一阶系统的相位延迟与阶跃响应问题
EMA/一阶低通滤波器在设计上有一个不可回避的代价:相位滞后。输入信号里的所有频率分量,经过滤波器之后都会在相位上被延后,而且频率越高延后越多。在截止频率(f_c)处,相位滞后恰好是45度。数学上对应的就是相频响应(\phi(\omega) = -\arctan(\omega / \omega_c))。
这意味着什么?如果你的系统对实时性要求很高——比如运动控制中的速度环、医疗设备中的脉搏波监测——一阶滤波的相位滞后可能会直接破坏系统的稳定性或测量的时序精度。控制理论里,相位裕度不足会导致系统震荡甚至发散;医疗信号里,波形整体右移一段固定延迟,可能让特征点检测发生系统性偏移。
另一个让人意外的点是一阶系统对阶跃输入的响应虽然单调无超调,但对高频噪声的衰减速率是固定的——每倍频程衰减6dB。这在很多场景下不够用。比如一个信号里有1kHz的高频开关噪声,你的滤波器截止频率是10Hz,理论上1kHz处的衰减是(-20\log_{10}(1000/10) = -40\text{dB}),也就是噪声被衰减到原来的1%。听起来不错,但如果原始噪声幅度极大,1%的残留依然可能超出ADC的精度指标。这时候就需要更高阶的滤波器:二阶低通能做到每倍频程衰减12dB,四阶就是24dB。
但高阶滤波器不是免费午餐。阶数越高,相位滞后越严重,而且滤波器系数的量化误差对性能的影响也越大。在固定点MCU上实现高阶IIR滤波器,还需要额外关注数值稳定性。所以“要不要升阶”永远是一个权衡,不是无脑升级。
4.2 噪声类型与滤波器类型的匹配
“噪声”这个笼统的词背后是完全不同的概率分布和频谱特性。白噪声功率谱平坦,高频和低频分量均匀分布;粉红噪声功率随频率升高而下降,常见于电子元器件热噪声之外的自然现象;脉冲噪声则是稀疏的大幅度尖峰,常见于电机换向时的电磁干扰、蓝牙发射瞬间的串扰。
一阶低通滤波器对白噪声的抑制效果不错,对脉冲噪声的抑制效果却很有限。原因很简单:一个巨大的脉冲进入滤波器,会被平滑成一段幅度较高、宽度较宽的隆起。这个隆起在敏感的检测算法里,可能被误判为一个真实的信号事件。
数学上看,脉冲响应宽度大约是(\tau)量级的几倍。如果脉冲的幅度是正常信号幅度的20倍,即使被系数(\alpha = 0.1)摊薄,脉冲发生后那一个输出点依然会有一个约2倍于正常信号的瞬时突起。这在阈值检测里足够触发一次误报了。
对付脉冲噪声,更好的选择是中值滤波或者滑动中值,它对单点异常值有天然的免疫能力。代价是计算量更大,且本身也是一个非线性操作,会改变信号的相位特性,使用时需要评估。流程上可以先用中值滤波剔除脉冲,再用一阶低通做平滑,两级串起来的效果在电机电流检测中会很明显。
如果噪声主要是高斯白噪声而你又希望信号延迟尽可能小,可以用卡尔曼滤波器。它对白噪声的最优滤波特性是理论保证的,而且天然以状态空间形式存在,方便直接和多传感器融合结合。但卡尔曼滤波需要模型参数,使用门槛比一阶滤波高一个台阶,很多场景杀鸡用牛刀。
4.3 整数运算环境下的实现注意事项
很多MCU没有FPU,或者浮点运算开销太大。这时候就面临在定点环境下实现EMA的问题。
一个常见的做法是把(\alpha)按等比缩放转成整数。比如(\alpha = 0.1)可以放大到(256 \times 0.1 \approx 26),然后用移位和整数运算完成:
#define ALPHA_Q8 26 // 0.1 * 256 uint16_t ema_filter_q8(uint16_t x, uint16_t y_prev) { return ((uint32_t)((256 - ALPHA_Q8) * y_prev) + (uint32_t)(ALPHA_Q8 * x)) >> 8; }这个写法把浮点乘法换成了整数乘法和一次移位,运行速度能提升不少。但要注意几个坑:
- 累加时要转成32位中间变量,否则(256 \times y)在16位系统上会溢出。
- (\alpha)量化到8位时的量化误差会在时间常数上造成大约(\pm 2%)~(5%)的偏差。如果对截止频率有严格需求,建议用更高的Q值(比如Q16)来降低量化误差。
- 如果(\alpha)小于0.0039(即小于(1/256)),在8位量化下会直接变成0,滤波器退化为直通。这是最容易踩的隐形坑。
另外一个不太常见但很实用的技巧:如果信号范围和(\alpha)都是固定的,可以预计算系数表,用查表替代乘法。这在资源极度受限的场景——比如4位或者8位MCU、几KB内存——是可行的优化路径。不过以我现在的开发环境来看,大部分场景用Q16定点和一次32位整数乘法就已经足够。
5. 跨领域实战:三个场景的选型与调参思路
5.1 嵌入式传感器数据平滑:先定义延迟预算再算(\alpha)
嵌入式领域的传感器滤波,核心矛盾永远是“平滑程度”和“响应速度”之间的取舍。很多人不懂这个博弈背后的量化关系,全靠调参试错。
我在一个无人机气压计定高项目里用过EMA做气压高度平滑。无人机飞行时气压计输出非常毛糙,尤其是旋翼气流对气压传感器的干扰非常明显。初始用原始的5Hz输出直接做控制,飞行高度在定高模式下起伏上下约1米,完全不可用。
我先明确需求:希望输出的高度估计延迟不超过150ms,在这个前提下尽量平滑。采样输出为20Hz,也就是(T_s = 50)ms,目标延迟对应约3个采样周期,取半衰期大约在1.5周期到2周期之间。如果用时间常数来算,150ms对应(\tau = 150ms / 2.3026 \approx 65)ms(这个系数来自10%剩余时间),所以:
[ \alpha = 1 - e^{-50/65} \approx 1 - e^{-0.769} \approx 0.536 ]
用(\alpha = 0.5)实现,硬件上用整数移位即可,一个周期都不用浮点运算。实测下来高度控制稳定在了±15cm以内。如果把(\alpha)调到0.3,平滑能再好一点,但150ms延迟预算就超标了,飞机会明显感觉到定高控制的主观“迟钝”。所以说,先定延迟预算再反推(\alpha),这个顺序不能颠倒。
5.2 量化交易特征值:小心滤波对信号相位的影响
量化交易里,EMA是均线系统中最基础的构件。双均线金叉死叉是很多人入门的第一个策略。但从信号处理的角度看,短周期均线和长周期均线的差值(也就是MACD柱子的核心),本质上是一个带通滤波器。
这个视角有什么用?MACD通用的参数(12, 26, 9)对应的是12周期EMA和26周期EMA的差值。从频域看,这个差值本质上是两个低通滤波器输出的相减,会形成一个带通特性。而MACD信号线(9周期EMA)再加一次平滑之后,相位延迟进一步增加。这带来的实际影响是——金叉和死叉信号天然是滞后的,行情走得越快,滞后越明显。
如果你试图用更短的EMA参数(比如5和10)来减少滞后,需要注意一个副作用:短EMA对高频噪声更敏感,可能频繁产生假交叉。这就是为什么有些做日内策略的人会把EMA参数从12/26改成8/21而不是5/10——8/21在滞后和噪声敏感度之间取得的平衡比5/10更实用。
我的经验是,在量化策略里使用EMA时,先把数据丢进去做一次离线分析,算出你用的参数对应的截止频率,然后对照一下行情的主要周期成分。如果你的策略是捕捉分钟级波段,却用了截止频率在小时级的EMA参数,那不可避免会错过很多启动点。这种错误在构思策略时就能用公式算出来,不需要真金白银去试错。
5.3 音频与语音处理的包络提取:用时间常数而不是系数
音频领域里,经常需要提取信号的包络——比如动态范围压缩器、噪声门、响度归一化,都需要一段短时间内的RMS或峰值包络。这种场景下的标准做法就是取信号的绝对值或平方,然后过一个一阶低通滤波器。
这里有个常见错误:直接在固定采样率下用一个拍脑袋的(\alpha),然后在导出时发现包络的跟随速度不对。正确做法是用时间常数来设定。比如动态范围压缩器里常见的attack time和release time——attack time决定了包络跟踪启动有多快,release time决定了信号减弱时包络有多快回落。它们对应到滤波器上就是两个不同的时间常数。
举个例子,设定attack time为1ms、release time为100ms、采样率44.1kHz,则:
[ \alpha_{\text{attack}} = 1 - e^{-1/44100 \times 1000} \approx 0.0224 ]
[ \alpha_{\text{release}} = 1 - e^{-100/44100 \times 1000} \approx 0.097 ]
注意这里attack和release不是同一个(\alpha),你需要根据信号方向动态切换系数:
float envelope(float x, float current_env) { float alpha = (x > current_env) ? ALPHA_ATTACK : ALPHA_RELEASE; return alpha * x + (1.0f - alpha) * current_env; }这种系数切换方式在音频处理里非常常见。如果没有把(\alpha)换算成时间常数再去定义attack/release,你就得纯靠耳朵和示波器一点一点碰,浪费大量时间。而这个场景下,直接用采样率和时间常数换算一次就能拿到准确的系数。
5.4 三个场景放在一起看出的规律
如果把这几个案例摆在一起,会发现选型和调参的合理步骤几乎是固定的:
- 先明确应用对“信号延迟”的容忍上限。
- 用这个上限确定系统可接受的时间常数。
- 再根据实际采样周期换算出对应的(\alpha)。
- 最后才考虑用什么精度和实现形式在目标硬件上落地。
这个顺序反过来几乎必然出问题。很多人先拍一个(\alpha)再去试效果,发现延迟太大或太平滑,再回头改,浪费的时间完全可以用一张草稿纸和几次乘法省下来。
6. 实操中容易忽略但值得认真对待的细节
6.1 浮点累积误差在高精度场景中的影响
EMA递推式在浮点实现里看起来精度很高,但在长时间运行的高精度场景中,浮点累积误差是有实际影响的。特别是当数据长期处于一个缓慢变化的大平台上,(\alpha)又比较小时,每次迭代都是(\alpha \times x + (1-\alpha) \times y)。如果机器的浮点精度是单精度(float),每次运算会有约(10^{-7})量级的舍入误差,日积月累,输出的最低位可能漂移出你关心的精度范围。
在实时控制系统里这个误差通常在可接受范围内,但在精密计量仪器、长时间运行的遥测设备里,就需要引起重视。解决办法也很直接:定期对滤波器做“归零校准”——在系统空闲时把当前输入作为基准重新初始化。或者改用双精度运算,虽然MCU上的计算成本会增加,但高精度场景下这个成本是值得的。
6.2 滤波器的“预热”与动态切换
很多系统在长时间运行中需要切换滤波器的截止频率。比如一个可调节档位的设备,用户切换档位后,希望传感器的响应速度变化。这时如果只改(\alpha),输出值会突然发生一次跳变,因为新系数的递推式叠加在旧系数已经积累的历史状态上。
解决这个问题的一种方案是:切换(\alpha)时,同时把(y)重置为当前输入值。但这会瞬间丢掉之前的平滑信息,可能让输出发生一个小台阶。另一种更平滑的做法是把当前(y)作为新的初始状态,然后用新的(\alpha)继续递推。这样输出不会跳变,但会有一小段过渡期。具体选哪种要看应用需求:控制类系统通常在切换后更看重响应速度而非连续性,所以选择重置;信号分析类系统则大多选择保留状态,避免产生伪事件。
这里没有统一的正确答案,但知道“切换(\alpha)时输出会受旧状态影响”这件事本身,能帮你避免很多看起来莫名其妙的波形异常。
6.3 并行或级联滤波时的性能退化
有时候一个通道的信号会经过多级滤波,比如磁力计数据先经过一次低通,再经过一次均值,又经过一个Kalman。这时候要注意,两级一阶低通级联之后,等效的阶跃响应就不再是单调的一阶响应了。它变得类似二阶系统,可能出现超调,只是超调量通常不大(级联两个一阶系统在阻尼比1时临界,超调和振荡都很小)。
如果你的需求是纯粹的“一阶无超调特性”,那级联滤波器会破坏这个特性。替代方案是保持单级低通,但把截止频率设置得更低,或者直接用等效时间常数为两级之和重新设计一个一阶滤波器。数学上,两个同为截止频率(f_c)的一阶滤波器级联后,等效-3dB点会向左移动约((\sqrt{2} - 1)f_c),这一点在设计链路时需要提前考虑。
7. 用一阶滤波思路辨析常见误区
7.1 误区一:EMA一定比滑动平均更“高级”
很多人潜意识里觉得指数移动平均比简单滑动平均(SMA)更“高级”或者更“正确”。其实两者各有利弊,关键在于适用场景。
SMA的窗口长度N内每个样本的权重相同,它的频率响应有一个显著的旁瓣——在窗口长度相关的频率上有较深的凹陷,但也伴随着欠阻尼的旁瓣,某些频段反而比EMA更“透声”。EMA的脉冲响应是单调指数衰减,频响曲线是一阶低通,没有旁瓣,但高频衰减速率只有6dB/oct,不如长窗口SMA陡峭。
在需要“近期数据权重大于远期数据”的场景——比如实时控制、趋势跟踪——EMA更合适。但在需要精确截断某个固定时间窗口的场景——比如一段时间内的平均值计算、波动率估计——SMA或加权的滑动平均更能保持窗口语义。
7.2 误区二:(\alpha)越小越好
(\alpha)越小,平滑效果越明显,但延迟也越大。如果延迟超出了系统的稳定裕度,系统就可能震荡甚至发散。很多PID控制回路串联传感器滤波后出现震荡,调了半天PID参数无果,最后发现问题出在传感器滤波的相位滞后上。
一个可行的经验法则是:滤波器的时间常数不要超过控制系统目标闭环时间常数的1/5到1/10。超出这个范围,滤波对控制系统的相位裕度侵蚀会非常明显。在控制环路设计里,从来都是“滤波粗暴,控制难调;滤波克制,控制好调”。
7.3 误区三:零相位滤波总是更好
很多人看到“零相位失真后移”这类描述,就以为零相位滤波在所有场景都是优越的。但在线实时系统里无法严格实现零相位,因为零相位需要知道未来的数据。离线场景下可以用filtfilt之类的函数实现零相位,但那是事后处理。
如果对实时性没有要求,当然可以用离线零相位滤波获得更好的视觉效果。但如果是流式处理,接受一定的相位滞后并把滞后纳入系统设计,才是务实的做法。
8. 结尾:一套值得长期留存的调参清单
写到这里,EMA和一阶低通的等价关系、数学推导、实操参数、边界状况和跨领域经验都梳理了一遍。最后把我自己实际做项目时固定用的一套流程整理出来,当成一份可复用的调参清单。
- 第一步,确认采样率(f_s),这一步决定了一切换算的基准。
- 第二步,定义延迟预算。用“从阶跃输入到输出跟到稳定值的90%,我能接受多慢?”来定时间常数(\tau)。如果答案是500ms,(\tau)就取约217ms。
- 第三步,算(\alpha = 1 - e^{-T_s/\tau})。不要拍脑袋拍(\alpha),要由(\tau)推出来。
- 第四步,检查截止频率(f_c = -\ln(1-\alpha)/(2\pi T_s)),确认它对目标信号的频率成分没有过度伤害。
- 第五步,确认初值处理、采样间隔稳定性、定点量化误差三个实现细节。
- 第六步,在真实数据上验证延迟和噪声抑制效果,必要时升阶或换非线性滤波器。
这个清单在我过去三四年做过的嵌入式、工业检测和数据分析项目里反复使用,至今仍然觉得够用。EMA和一阶低通滤波器看起来是入门级的内容,但它的价值恰恰在于——一旦你把这两个概念当成一个东西来理解,你对“平滑”这件事的认知就从“调参数碰运气”上升到了“按规格算响应”的层面。这种差别的具体体现,就是项目调试时间从几天压缩到几小时。