一阶数字低通滤波器参数选择:α系数推导与工程实践
相信每个写过数字信号处理的工程师都和“一阶数字低通滤波器”交过手——尤其是做传感器采集、运动控制、音频去噪这类项目的人几乎都绕不开这个只有一行递推公式的“老朋友”。公式很简单y[n] y[n-1] α·(x[n] - y[n-1])参数只有一个α系数。但真到项目里落地很多人会在采样频率、截止频率、α系数这三者之间反复试探噪声压不下去就加滤波信号迟滞太严重又松滤波好不容易凑出一组参数换了采样周期整个系统又得从头调。这篇文章我把一阶数字低通滤波器的参数选择讲透——从α的两种推导路径、采样频率与截止频率的工程权衡到Python仿真验证、嵌入式C语言落地最后用实战中的几个反面案例复盘。适合正在调嵌入式滤波参数的工程师也适合刚接触数字滤波、想系统理解这套算路的学生。1. 一阶低通的本质一个“惯性环节”在数字域的翻译1.1 从模拟RC电路看懂它到底在做什么先说清楚一阶低通在物理上是什么。模拟电路里的RC低通滤波器本质是一个电容对电压变化“跟不上”的惯性系统。输入电压突变时电容两端电压按指数曲线慢慢逼近目标值逼近快慢由时间常数τ RC决定对应的截止频率 fc 1 / (2πRC)。到了数字域我们当然不能真的用电容电阻但可以用差分方程模拟同一个惯性过程。最通用的递推式就是上面那个y[n] y[n-1] α * (x[n] - y[n-1])把它拆开看当前输出 上一拍输出 α × (当前输入 - 上一拍输出)。换句话说输出值每次都向输入值“挪”一小步α就是迈步幅度。α大输出跟手快α小输出平滑、迟钝。这和模拟RC电路的充电曲线在数学上是同构的所以很多资料会直接叫它“数字RC低通滤波器”。1.2 为什么它永远有一席之地一阶低通在工程里用了几十年还没被淘汰核心原因是它在“性价比”上太突出计算量极小每个采样点只需要一次减法和一次乘加在STM32这类单片机上跑几路滤波器毫无压力。内存占用极低只需要保存上一拍的输出值连状态数组都不用开。参数直观只有一个α理解了之后调参非常快。这些特性让它在嵌入式领域成为“默认的平滑工具”。哪怕后来有巴特沃斯、切比雪夫、移动平均等各种滤波器一阶低通依然出现在绝大多数项目的起始版本里。它解决的核心问题就是把高频抖动、量化噪声、工频干扰从信号里压掉保留趋势变化。但也正因为“看起来太简单”很多人忽略了它背后的参数依赖关系——α并不是随便拍的它与采样频率、期望截止频率存在确定的数学关系。下面这部分是我认为整篇文章里最该被记住的内容。2. α系数的推导同是“截止频率”公式不同结果差不少2.1 基于时间常数的精确推导α 1 - exp(-2πfc/fs)一阶模拟低通滤波器的时间常数 τ 1 / (2πfc)。把这个惯性过程离散化可以推导出差分方程。具体过程涉及对模拟系统单位冲激响应的采样或对一阶微分方程做后向差分离散。从连续域的一阶惯性系统方程τ * dy(t)/dt y(t) x(t)把导数替换为前向差分 (y[n] - y[n-1]) / T其中 T 1/fs 是采样周期代入整理后得到递推系数α 1 - exp(-T / τ) 1 - exp(-2π * fc / fs)这正是EMA指数移动平均的标准写法。这个公式可以理解为在采样周期T内输出向输入指数逼近了多少。fc定得越高一个采样周期内逼近的比例越大α越大fc越低每次逼近的比例越小α越小。举个例子fs 1000Hzfc 10Hzα 1 - exp(-2π * 10 / 1000) 1 - exp(-0.06283) ≈ 0.0609也就是说每个采样周期输出向输入移动约6.09%。这个值远小于很多第一感觉里的“0.5”这也是为什么新手往往会选偏大的α。2.2 另一种常见公式α (2πfc/fs) / (1 2πfc/fs)网上还能看到另一种公式α (2π * fc / fs) / (1 2π * fc / fs)这个公式来自后向欧拉Backward Euler离散化或者说是对上面指数公式在“fc远小于fs”条件下的近似展开。它同样能工作而且实现简单因为只需要一次除法和一次乘法。但两个公式不是完全等价的。当 fc / fs 很小时两者几乎一样但当 fc / fs 偏大比如超过0.05差异就变得不可忽略。我算了一组对比数据方便看清楚fc / fsα(指数精确式)α(欧拉近似式)相对误差0.0010.0062610.0062440.27%0.010.06090.05912.95%0.050.26960.239011.35%0.10.46650.385717.32%当 fc / fs 达到0.1时两个公式算出来的α差了约17%以上最终滤波后的实际截止频率偏离也会很明显。所以说如果项目对截止频率有明确要求优先用指数精确式如果条件就是fc远小于fs近似公式也够用但心里要有数。2.3 应该选哪条公式我的建议很直接默认用指数式 α 1 - exp(-2πfc/fs)因为它对应“截止频率点增益为-3dB”的连续域定义工程语义最清晰。在C语言里算一次exp并不复杂而且α一般是在初始化阶段算一次后面滤波循环里根本不会增加实时开销。只有当你明确知道自己只需要一个粗糙平滑且fs远大于fc的时候再用欧拉近似式降低推导复杂度也不迟。还要强调一点不管你用哪条公式这个“截止频率”指的是模拟域的等效截止频率。数字滤波器在离散系统里实际达到的-3dB点会略有偏移尤其在fc/fs比值较大的时候。后面讲验证环节我会给出用freqz扫描真实频响的方法。3. 工程中采样频率与截止频率怎么定先做频谱分析再选参数3.1 截止频率不是猜的先把信号和噪声画出来很多工程师拿到滤波需求第一反应是“先取个0.1试试”。这其实是把因果关系搞反了。α不是你需要拍板的最终变量真正需要决定的是信号的最高有效频率是多少噪声主要集中在什么频段这两个问题明确了截止频率就自然落在两者之间。正确流程是用示波器或采集设备记录一段原始数据尽可能包含噪声最严重的工况。对这段数据做FFT观察频谱分布。找出信号能量所在的频段上限记为 f_signal_max。找出噪声开始明显抬升的频段记为 f_noise_min。把截止频率定在 f_signal_max 和 f_noise_min 之间最好留出至少3倍间隔。举一个我实际做过的例子一个气体压力传感器输出信号真实压力变化周期大概1~2秒对应频率不到1Hz但传感器输出上叠了约50Hz的纹波和20kHz的开关噪声。这里 f_signal_max 约1Hzf_noise_min 至少有50Hz频谱上间隔巨大截止频率取10Hz就非常安全。如果采样频率是500Hz那么α 1 - exp(-2π * 10 / 500) ≈ 1 - exp(-0.12566) ≈ 0.118这个α取出来就是0.118而不是拍脑袋的0.01或0.5。3.2 采样频率的约束不是越高越好但要保证隔离带采样频率的选取首先要满足奈奎斯特采样定理即 fs 2 × 信号最高频率。但在设计数字低通滤波器时更实际的约束是让 fc / fs 保持一个合理范围。fc / fs 太小比如低于0.001α会变得非常小约0.006。这时候两个问题会冒出来滤波器的极点位置 1-α 非常接近单位圆对量化误差和系数误差极敏感。输出值的“爬行”速度很慢系统响应可能要几秒甚至几十秒才能跟上输入变化。fc / fs 太大比如接近0.5α接近0.63以上滤波作用迅速减弱甚至失去低通意义。一般工程经验是让 fc / fs 落在0.001到0.1之间。如果你算出来不在这个区间优先调整采样频率而不是硬凑α。比如单片机ADC可以配置不同采样率传感器有不同输出率把采样率合理抬高或降低往往能让滤波参数更从容。3.3 相位滞后和响应时间的账一阶低通永远不会“零滞后”。对于频率为 f0 的正弦信号经过截止频率为 fc 的一阶低通后相位滞后约为φ ≈ -arctan(f0 / fc)看两组数字就明白了信号1Hz截止频率10Hz滞后约-5.7°。信号10Hz截止频率10Hz滞后约-45°。如果你的系统是开环的数据平滑45°滞后一般还能接受但如果滤波器在闭环反馈回路里比如PID控制器的反馈通道5°以上的滞后都可能让系统稳定性变差。这时要么把截止频率往高推要么接受更多的噪声要么换成零相位滤波或更复杂的滤波器结构。另一个直观指标是“阶跃响应时间”。一阶低通对阶跃信号的到达时间大约是t_63% ≈ 1 / (2π * fc) t_90% ≈ 2.3 / (2π * fc)以fc10Hz为例达到阶跃63%需要约15.9ms90%约36.6ms。如果你需要更快的响应就得提高截止频率如果你能容忍更慢的响应就可以降低截止频率换取更强抑噪。这个公式比反复试α要可靠得多。4. 完整实现Python仿真验证与嵌入式C语言落地4.1 先用Python把频响和参数算明白在写嵌入式代码之前我强烈建议先用Python做一次仿真验证。下面这个脚本可以直接跑它把滤波前后的信号波形和频响都画出来。import numpy as np import matplotlib.pyplot as plt from scipy import signal fs 1000.0 # 采样频率 Hz fc 10.0 # 设计截止频率 Hz alpha 1 - np.exp(-2 * np.pi * fc / fs) print(falpha {alpha:.6f}) # 生成测试信号1Hz有效信号 50Hz噪声 t np.arange(0, 2, 1/fs) clean np.sin(2 * np.pi * 1 * t) noise 0.3 * np.sin(2 * np.pi * 50 * t 0.5) x clean noise # 一阶低通滤波 y np.zeros_like(x) for i in range(1, len(x)): y[i] y[i-1] alpha * (x[i] - y[i-1]) # 频率响应验证 w, h signal.freqz([alpha], [1, -(1 - alpha)], worN8192, fsfs) freq w gain_db 20 * np.log10(np.maximum(np.abs(h), 1e-12)) # 查看1Hz和50Hz处的增益 for target in [1.0, 10.0, 50.0]: idx np.argmin(np.abs(freq - target)) print(f{target}Hz 增益: {gain_db[idx]:.2f} dB) # 画图 fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].plot(t[:2000], x[:2000], labelraw, alpha0.6) axes[0].plot(t[:2000], y[:2000], labelfiltered, linewidth2) axes[0].legend() axes[1].semilogx(freq, gain_db) axes[1].axhline(-3, linestyle--, colorgray) axes[1].axvline(fc, linestyle--, colorred, labelfc) axes[1].legend() plt.show()这个脚本跑完你应该能看到50Hz噪声被明显压制幅值衰减通常超过35dB。1Hz有效信号基本不衰减幅值损失小于0.1dB。频率响应曲线在fc位置附近正好越过-3dB线。这两个结果非常重要——它告诉你“图纸上的截止频率”和“实际滤波器行为”是否一致。如果实际-3dB点和设计值偏差明显多半是fc/fs比值太大需要回头检查参数。4.2 嵌入式C语言实现浮点和定点两种做法单片机端实现非常直接。浮点MCU直接用float结构体就可以typedef struct { float alpha; float y_prev; uint8_t initialized; } lpf_t; float lpf_process(lpf_t *f, float x) { if (!f-initialized) { f-y_prev x; f-initialized 1; return x; } float y f-y_prev f-alpha * (x - f-y_prev); f-y_prev y; return y; }初始化函数里算alpha#include math.h void lpf_init(lpf_t *f, float fs, float fc) { f-alpha 1.0f - expf(-2.0f * (float)M_PI * fc / fs); f-y_prev 0.0f; f-initialized 0; }如果你用的是Cortex-M0这类不带硬件浮点单元的低成本MCU可以考虑定点化。一阶低通的定点实现要格外小心α太小导致乘出来的增量被舍入掉建议用32位累加器保存中间结果typedef struct { uint32_t alpha_q15; // alpha 映射到 Q15 int32_t y_prev_q16; // 输出保存在 Q16留余量 } lpf_q15_t; #define Q15_SCALE 32768.0f void lpf_init_q15(lpf_q15_t *f, float fs, float fc) { float alpha 1.0f - expf(-2.0f * (float)M_PI * fc / fs); f-alpha_q15 (uint32_t)(alpha * Q15_SCALE); f-y_prev_q16 0; } int16_t lpf_process_q15(lpf_q15_t *f, int16_t x) { int32_t x_q16 (int32_t)x 16; int32_t diff x_q16 - f-y_prev_q16; // alpha * diffalpha为Q15 int32_t step (int32_t)(((int64_t)f-alpha_q15 * diff) 15); int32_t y_q16 f-y_prev_q16 step; f-y_prev_q16 y_q16; return (int16_t)(y_q16 16); }这里用Q16来保存输出累加值的原因很直接如果输出也放在Q15里α非常小时每次step微乎其微中间结果因为舍入误差可能长期不变滤波就变成“不滤波”了。Q16多出的一位余量配合64位中间乘法能有效缓解这个问题。4.3 用阶跃响应快速验证整套链路参数写进固件后我习惯先做一个阶跃验证把滤波器的输入从0直接跳到满量程记录输出曲线。对一阶低通来说输出应该是一条指数逼近曲线t_63%的时间应该与设计一致。比如fs1000Hz、fc10Hz时理论t_63%约15.9ms。实测如果差太多优先检查滤波循环是否真的按采样周期执行有没有丢中断。是否在滤波之前用了另一个平滑或过采样。初始化状态是否被反复清零。这一步只要做了滤波器参数是否正常基本就一目了然了。5. 实战复盘几个看着正常却翻车的坑5.1 采样时间不均匀α顶着截止频率也是白搭一阶低通的递推公式隐藏了一个前提每次调用间隔必须等于初始化的采样周期T。很多项目把滤波放在主循环里跑但主循环不是固定节拍这次5ms、下次13ms滤波器的实际截止频率就会不断漂移。明明设计的是10Hz噪声大时可能实际只有6Hz响应慢时又是15Hz。解决办法是在定时器中断或ADC完成中断里调用滤波函数确保调用间隔恒定。如果确实没法保证固定节拍那就让α根据实际间隔实时更新float alpha_for_interval(float fs_design, float fc, float actual_interval) { // actual_interval 是上次调用到现在的实际秒数 return 1.0f - expf(-2.0f * (float)M_PI * fc * actual_interval); }不过这样引入expf在每次调用时都有开销能避开就避开优先把采样节拍校正好。5.2 α过小导致“温水煮青蛙”式输出有次做温湿度传感器数据平滑为了彻底压住波动我把截止频率设得非常低算出来α只有0.003。结果传感器开机后输出值接近目标用了十几秒生产线上根本等不起。复盘时发现问题不只是响应慢还有另一个隐患在定点MCU上α0.003用Q15表示只有约98再减去初始化误差每步修正量小得可怜。输出很容易被量化噪声“锁死”在一个偏离真值的地方。后面我把方案改成开机前两秒用较大的α快速逼近稳态后再切到小α精确平滑。也就是大家常说的“变系数滤波”或“动态α”。这种方法实现成本极低但效果立竿见影float alpha (elapsed_time 2.0f) ? 0.2f : 0.01f; y y_prev alpha * (x - y_prev);5.3 复位时输出状态不清零导致的“跳变”一阶低通滤波器必须保存上一拍输出值。如果系统运行中重启了算法模块而状态变量没有被正确初始化输出会从0开始爬向真实值造成一段明显的跳变。很多控制系统的“上电过冲”就来自这个细节。正确做法是初始化时把y_prev直接设成当前输入值并让第一拍输出直接等于输入。这就是我在C代码里加initialized标志的原因。不过要小心冷启动时ADC还没稳定第一拍输入可能本身就不准因此更稳妥的做法是等ADC前几次采样稳定后再启动滤波器并把y_prev设为其平均值。5.4 两级一阶滤波叠加后的截止频率漂移有些工程师觉得“一级不够就串两级”。思路没错但两级串联后的总截止频率并不等于单级的截止频率。设两级都用fc10Hz的系数串联后总频响在10Hz处已经不是-3dB而是约-6dB。实际总截止频率会下移到约6.4Hz附近。这个问题在精密测量里很致命。我的经验是如果确定要用两级滤波先明确目标总截止频率再反推每一级的参数。同级截止频率可以稍高于目标值具体取值可以借助Python的freqz扫一遍几秒钟就能确认。6. 最后再分享一个调参顺序小心得按我做了多年现场调参的经验一阶数字低通滤波器的正确打开方式一定是“先分析频谱再定截止频率同时锁死采样频率最后算α”顺序不能反。只要采样频率固定、截止频率合理α就是一个确定的、可复算的数值不需要靠手感试来试去。如果项目赶时间我也会用一个简化经验值希望滤波后的滞后不明显的场景把截止频率设为信号最高频率的5到10倍希望抑噪更狠的场景设为3到5倍。算完α放进仿真里看一眼前后频谱十有八九能一次通过。最后还有一个小技巧把设计好的fs、fc、α、t_63这些参数写进代码注释里。良好的使用经验和方法沉淀下来下次做类似项目时就不用了再从头推导一遍公式也能少踩很多坑。