输入滤波算法工程实践:从滑动平均到FIR/IIR的完整选型指南 1. 输入滤波到底在解决什么问题1.1 为什么输入端的滤波比后端更重要做数字信号处理的人经常忽略一件事系统性能的天花板往往在信号进入ADC之前就已经决定了。我见过不少项目算法层写得花团锦簇自适应、卡尔曼、小波分析全上了一遍结果现场数据一上来效果还不如实验室里的仿真。排查到最后十有八九是输入端那一坨噪声把信噪比毁掉了。输入滤波算法本质上就是在这道“入口”做文章。它做三件事把带外噪声和干扰压下去、把信号调理到ADC能接受的范围、把混叠风险提前掐死在采样之前。很多人觉得滤波是“软件里跑个平均”的事但真正工程化之后你会发现输入滤波的算法选型、参数设计、定点实现每一个环节都直接决定最终数据的可用度。这篇文章面向的读者是那些手里有ADC、有传感器、有实时数据流正在被噪声、毛刺、漂移折磨的工程师。我会把输入滤波算法从原理到工程实现讲透包括哪些场景该用均值、哪些场景该用中值、什么时候必须上FIR/IIR以及我在实际项目中踩过的那些坑。1.2 从信号链的视角看滤波器选型滤波器不是孤立存在的。它的上游是传感器或信号源下游是ADC和处理器。选什么算法取决于中间隔着多少干扰、信号带宽是多少、处理器余量有多大。举个例子。热电偶测温信号变化极其缓慢带宽可能不到1Hz但工频干扰和随机噪声很突出。这种情况下用一阶低通或者滑动平均就非常合适因为你根本不需要保留高频分量。而如果是电机电流采样信号本身就包含丰富的谐波信息用于做FOC磁场定向控制那滤波就得小心翼翼既要把PWM开关噪声滤掉又不能把电流谐波砍得面目全非否则转子位置估算会出问题。所以我的习惯是接到一个滤波需求先问自己四个问题。信号带宽是多少噪声频谱大概长什么样实时性要求有多高能接受多少延迟处理器的算力余量有多少这四个问题回答完算法选型基本就锁定了一半。这也是本文贯穿始终的思考方式。2. 核心滤波算法的原理与适用场景2.1 滑动平均滤波最朴素但最常用的方法滑动平均的原理简单到不能再简单维护一个长度为N的窗口输出窗口内所有采样值的算术平均。y[n] (x[n] x[n-1] ... x[n-N1]) / N它的频率响应是一个sinc函数形状零点出现在 k·fs/Nk1,2,...的位置。这意味着它对整数倍于 fs/N 的频率分量有非常深的陷波效果。如果你知道干扰的频率比如50Hz工频把N选成 fs/50 的整数倍就能显著压制这个干扰。但滑动平均有个先天短板它对脉冲噪声的抑制能力很弱。一个幅值极大的毛刺即便只有1个采样点也会按照1/N的比例泄漏到输出中N越大泄漏越小但代价是响应变慢。还有一个容易被忽略的问题窗口长度N决定了截止频率——N越大截止频率越低但同时延迟也越大本质上是拿时间换平滑度。在实际使用中我更喜欢用“去极值滑动平均”先剔除窗口内的最大值和最小值再对剩余值求平均。这个改进几乎不增加计算量却能同时兼顾随机噪声抑制和脉冲干扰剔除在工业传感器采集场景非常好用。2.2 中值滤波对付脉冲噪声的利器中值滤波的核心思想是窗口内的N个采样值排序取中间那个作为输出。它对付脉冲噪声的效果堪称立竿见影。想象一个传感器信号被偶尔的接触不良打出尖锐毛刺滑动平均会把毛刺摊平成一个持续N个周期的隆起而中值滤波直接把这个毛刺当作异常值丢弃完全不影响周围的正常波形。这一点在压力传感器、接近开关信号、光电编码器信号处理中特别有价值。中值滤波的代价是计算量。窗口越大排序开销越高。不过工程上有个优化技巧窗口长度为奇数3、5、7时中值索引明确且可以用“插入排序只维护有序数组”的方式把每次处理的时间复杂度降到接近O(N)。对我用过的多数MCU来说窗口取5已经是性价比很高的配置。需要注意中值滤波对高斯白噪声的抑制效果远不如滑动平均因为它在平滑连续随机起伏时效率偏低。所以我的经验是先用中值滤波去毛刺再级联滑动平均或一阶低通去随机噪声两者配合而不是二选一。2.3 一阶低通RC等效滤波资源最省的方案一阶低通滤波的离散形式长这样y[n] y[n-1] α · (x[n] - y[n-1])其中α是滤波系数取值0到1之间。α越小滤波越强但响应越慢。这个算法在模拟域对应一个RC低通电路在数字域就是最常见的指数加权移动平均EWMA。它的优势非常明显只需要两个变量上一次输出、当前输入一次乘法和一次加法任何单片机都能在几个时钟周期内完成。而且它不耗内存不需要维护窗口适合做高频采样场景下的逐点滤波。但要注意α的选择是有讲究的。α与等效时间常数的关系可以近似为τ ≈ (1 - α) / α · Ts其中Ts是采样周期。如果你想滤除100Hz以上的噪声采样率是1kHz那么等效时间常数需要约1.6ms算出来α大约是0.16。这个推导在工程上很有用——很多人拍脑袋填α结果要么滤不干净要么信号被拖得迟钝。一阶低通最大的不足是相位延迟在所有低通滤波器中偏大且阻带衰减只有-20dB/十倍频。如果干扰频率离信号频率很近单靠一阶可能压不住需要升级到二阶或更高阶滤波器。2.4 FIR与IIR真正意义上的频率域滤波当滑动平均和中值滤波这些时域手段不够用时就该上真正的频率域滤波器了。FIR滤波器有限脉冲响应的核心性质是线性相位。它在通带内不会让不同频率分量的相对时延错开这对处理含有陡峭边沿的信号至关重要。比如方波、PWM占空比测量、过零检测这类场景用IIR会明显看到边沿变歪、过零点偏移而FIR能保持波形形状。FIR设计的重点有二一是阶数二是窗函数。阶数越高过渡带越窄但计算量线性增长窗函数的选择影响阻带衰减。以我对工程实践的观察Hamming窗搭配过渡带宽度δf阶数估算可以用这个近似公式N ≈ 3.3 / (δf · Ts)IIR滤波器则胜在效率。同样性能指标下IIR的阶数可能只有FIR的1/5到1/10。经典的Butterworth巴特沃斯滤波器在通带内最平坦适合对信号幅度保真要求高的场景Chebyshev切比雪夫I型过渡带更陡但通带有纹波。实际项目中我会优先用Butterworth因为它的幅频特性最容易理解和调参而且数字实现稳定。IIR最大的坑是相位非线性。它会让不同频率成分经历不同的时延导致时域波形畸变。如果应用对相位敏感就需要用零相位滤波——但这要求离线处理无法用于实时流。实时场景下要想获得类线性相位还是老老实实用FIR。3. 实操过程滤波器设计与参数整定3.1 设计一个抗混叠输入滤波器的完整流程我做电机控制器项目时需要采集相电流做FOC控制采样率定在10kHz。PWM频率是10kHz意味着逆变器开关噪声会直接落在采样频点上如果不在模拟前端或数字端处理混叠会把高频噪声折回低频控制性能立刻劣化。第一步明确信号带宽需求。FOC电流环的带宽通常做到1kHz以内所以信号本身只需要保留0~1kHz。第二步确定需要压制的干扰频段。开关噪声集中在10kHz及其谐波模拟前端已经有RC硬件滤波但衰减不够软件端还需要补一刀。第三步就是算法选型。需求是线性相位、通带平坦、阻带在10kHz处尽量深。滑动平均窗口取到10时陷波零点正好在1kHz的整数倍上但10kHz处衰减不够一阶低通在10kHz处只有约-40dB勉强但会给1kHz信号带来明显相移。最后我采用了2阶Butterworth低通截止频率2kHz配合模拟RC滤波10kHz处总衰减超过-60dB效果很干净。这道流程的重点在于先定义“保留什么、滤掉什么”再选算法最后调参数。顺序反了一定会在现场翻车。3.2 关键参数计算截止频率、阶数与采样率的关系设计数字滤波器最核心的参数是归一化频率。模拟频率f换算成数字角频率ωω 2π · f / fs这个换算容易被忽略但它决定了后面所有设计公式的输入。比如fs10kHz想要2kHz的截止频率ωc 2π · 2000 / 10000 1.2566 rad/sample对应归一化频率0.2即f/fs。二阶Butterworth低通的双线性变换法设计S域传输函数H(s) ωc² / (s² 1.4142·ωc·s ωc²)做双线性变换时需要用预畸变修正截止频率偏移。预畸变公式ωp 2/fs · tan(ωc/2)然后再代入变换公式得到数字滤波器的系数。很多现成库比如scipy.signal.butter能直接算但如果你在嵌入式平台手写理解这条链路还是很有必要的——否则遇到系数不对、截止频率偏移你根本不知道从哪里开始排查。FIR滤波器的窗口法设计相对直接。指定过渡带δf用前面提到的阶数估算公式算N然后生成理想低通脉冲响应并乘以窗函数。我建议先用Python的scipy调好系数导出成头文件数组再烧进MCU。直接在嵌入式环境下设计滤波器调试效率太低。3.3 定点实现中的量化问题工程实现比算法设计更容易翻车的地方就是定点量化。MCU和DSP多数没有硬件浮点单元用浮点模拟会吃掉大部分算力。所以滤波器系数和中间变量都要定点化。我常用的做法是Q格式。比如Q15格式表示范围-1到0.9999分辨率1/32768。滤波器系数通常在-1到1之间很适合Q15。但中间累加和很容易溢出——二阶IIR的中间变量可能达到输入幅度的几十倍尤其是系数接近1时。处理方法是把内部状态变量用Q31保存只在最终输出时缩回Q15。另一个常见问题是直流分量。IIR滤波器的极点非常接近单位圆时系数量化会导致极点偏移严重时滤波器自激振荡。我实测过一个6阶IIR的Q15实现某些系数量化后阻带特性已经完全变样。解决办法有两个一是用SOS二阶节级联而不是直接的高阶结构每个二阶节的量化误差相互独立二是给靠近单位圆的极点分配更多位宽必要时对这几个系数用Q31单独存储。4. 常见问题与排查技巧实录4.1 滤波后信号失真、相移过大的处理遇到的第一个典型问题是滤波后信号的过零点和相位滞后导致控制误差。我用一阶低通处理电流信号时100Hz的基波相位滞后有大约15度。对FOC来说相位滞后意味着dq轴解算出现偏差转矩脉动变大。后来我把这个问题拆开分析相位滞后与截止频率的比值强相关——截止频率离信号频率越近滞后越严重。改进方案有两条路一是把截止频率推高到信号频率的10倍以上让滞后降到几度以内但这意味着滤波效果变差需要配合更高阶的阻带衰减来补偿。二是改用FIR滤波器线性相位让所有频率的滞后相同校准掉这个常数延迟后波形形状不变。实践中我通常结合两条路模拟前端做主要衰减数字端用FIR做精细整形这样既保证了阻带深度又确保了通带相位可控。4.2 实时性与滤波效果的权衡现场总有人说“滤波开大点”但滤波开太大系统反应就迟钝了。这是一对天然矛盾必须量化。实时性可以用“阶跃响应时间”衡量。滑动平均的阶跃响应时间是N个采样周期一阶低通是约3τFIR则要看群延迟。设计时应该先定一个可接受的延迟预算。比如伺服控制环路的电流环延迟预算通常是50~100μs在10kHz采样率下就是0.5到1个采样周期。这种情况你能选的滤波器非常有限基本只有一阶低通且α要接近1或者干脆用预测补偿来抵消滤波引入的延迟。我的建议是永远不要为了滤波效果牺牲超过系统预算10%的实时性。一旦超出宁可回头去改善硬件布线和屏蔽。4.3 一组实测避坑清单这几条是我多年调试经验的浓缩每条背后都对应一个真实翻车场景。第一数字滤波不能弥补采样抖动。如果ADC触发时刻不稳定滤波器只会把“时间轴畸变”平均成“幅度畸变”越滤越乱。先解决时钟和触发源再谈滤波。第二IIR滤波器初始化一定要用信号的历史稳态值。如果初始状态全是0启动瞬间会有一个巨大的瞬态冲击这在电机启动、电源上电时特别明显。我通常在初始化时先跑一段“预热”采样让滤波器状态收敛后再启用输出。第三滤波器输出要加饱和保护。定点实现中即使设计了防溢出仍可能出现极端输入下的饱和失真。在输出端做一个限幅能把这种风险隔离在可控范围。第四级联滤波器一定要算总增益。多个滤波器级联时每级的直流增益可能不是精确的1尤其是量化后最终输出会有直流偏移。用一个标准直流信号标定一下软件里补偿掉即可。第五做FFT看频谱时直接用滤波前的原始数据别拿滤波后数据做频谱分析。滤波后做FFT会掩盖混叠问题而你真正需要知道的是进入ADC之前到底有没有不该有的频率成分。5. 工具选型与开发环境建议5.1 桌面端设计工具我强烈建议把滤波器的设计工作放到桌面端完成而不是在嵌入式环境里反复试错。Python的scipy.signal是我最常用的工具一行代码就能算出滤波器系数from scipy.signal import butter, lfilter, freqz import numpy as np fs 10000 # 采样率 10kHz fc 2000 # 截止频率 2kHz b, a butter(2, fc / (fs / 2), btypelow) w, h freqz(b, a, worN8000)用freqz验证幅频响应确认阻带衰减和通带纹波满足要求后再把b、a数组转换成定点系数。对于FIR设计可以用firwinfrom scipy.signal import firwin N 21 # 滤波器阶数 h firwin(N, fc / (fs / 2), windowhamming)拿到系数后我习惯先做一步仿真用真实采样数据跑一遍lfilter对比滤波前后波形确认延迟、幅度、边沿都符合预期再进入嵌入式移植。这个流程能省掉大量上板调试时间。5.2 嵌入式实现的验证方法嵌入式端移植完成后验证手段也要跟上。我的标准做法是构造三个测试信号直流高斯噪声、方波、50Hz正弦脉冲毛刺。分别测低频噪声抑制、边沿保持能力和脉冲剔除能力。只要这三个测试通过现场出问题的概率就大大降低了。另外建议在代码里加一个旁路开关调试阶段可以随时对比滤波前和滤波后的数据。我踩过一次大坑滤波器移植后以为没事结果有根信号线虚焊噪声根本不是滤波能解决的。旁路开关让我很快发现“滤了跟没滤一样”——那就不该再调滤波器该查硬件了。6. 关于输入滤波算法的一些个人体会做了这么多年信号处理回头看输入滤波这件事我觉得最大的体会是滤波器的价值不在算法本身多聪明而在于设计者对信号和噪声的理解有多深。同一个滑动平均有人调出来一团糟有人调出来干净利落差距就在于是否想清楚了信号频率、噪声频谱、采样率和延迟预算之间的定量关系。还有一点感触颇深滤波永远是在信息损失和噪声抑制之间找平衡。不要追求“滤到完美”那是做不到的。你要做的是在满足系统需求的前提下保留足够的信息、压下足够的噪声然后留足裕量。现场的环境比实验室恶劣得多温度漂移、器件离散、电磁干扰都会让滤波器性能打折设计时务必留出20%以上的裕量。最后分享一个小技巧每次调完滤波器把设计参数、测试波形、最终系数存档标注清楚当时的信号特性和决策理由。下次遇到类似项目直接翻档案就能快速切入不用再从零试错。这也是我踩过足够多的坑之后养成的第一个好习惯。