简介:面向嵌入式与单片机开发者的实用滤波算法资源,系统整理了10种常用数字滤波方法的C代码实现,包括限幅滤波、中位值滤波、算术平均滤波、递推平均滤波、防脉冲干扰平均滤波、限幅平均滤波、一阶滞后滤波、加权递推平均滤波、消抖滤波及限幅消抖滤波。每种算法均附带方法原理、适用场景和优缺点对比,并针对流量、压力、液位、温度等参数给出N值选取参考,便于读者结合具体信号特征进行选型与移植。资源包共3个文件,包含可直接编译使用的C源文件、详细说明10种软件滤波方法的Word文档,以及一篇关于单片机数字滤波算法研究的PDF论文,整体仅198KB,轻量且结构清晰,便于快速查阅与移植。已有3185人学习/下载,适合正在处理传感器数据采集、需抑制脉冲干扰或周期性噪声的开发者参考借鉴。 做嵌入式开发最烦人的事之一,就是传感器读回来的数据不干净。ADC采个电压,波形图跟过山车似的;MPU6050的加速度,静止放着还在乱跳;温湿度传感器明明环境没变,数值却在25度和27度之间来回蹦。那些毛刺、尖峰和随机抖动,让后面的阈值判断、PID控制、状态识别全都跟着遭殃。这时候,滤波算法就派上用场了。
这篇文章整理的是我这些年实际项目中反复用到的10种常用滤波算法,全部给出C代码实现。它们不花哨,不依赖什么数学库,在STM32、51、Arduino这类单片机上都能直接用。无论你是做嵌入式开发、智能硬件,还是习惯先用Python验证再移植到C的朋友,这份清单都值得收藏。
1. 为什么说软件滤波是嵌入式的基本功
1.1 硬件滤波不能解决所有问题
很多人第一反应是在传感器输出端加一个RC低通滤波电路。这确实能滤掉一部分高频噪声,但问题在于:硬件参数一旦定死就不好改了。你算出截止频率10Hz,结果换一个现场环境,干扰变成了5Hz,这时候只能重新算电阻电容、重新焊板子。而且硬件滤波对脉冲型干扰几乎无能为力,一个尖峰直接穿过RC网络,后端该误判还是误判。
软件滤波就没有这些麻烦。它不需要额外的元器件,改一个常量、换一行代码就能调整响应特性。有些算法还能根据工况实时切换,这在硬件上很难实现。我做过的几个工业采集项目里,最后稳定下来的方案基本都是“简单硬件防高频 + 软件滤波处理残余噪声”,软硬结合远比单靠一边靠谱。
1.2 十种算法到底在过滤什么
很多人一听到“滤波”就想到低通、高通、带阻这类频率概念,其实软件滤波处理的是更直观的东西:异常跳变、随机噪声、周期性纹波、信号抖动。这10种算法本质上是三类思路的组合。
第一类是限幅,专门干掉那些明显不合理的突变和尖峰;第二类是平滑,通过平均或者加权的方式把随机噪声摊薄;第三类是状态保持,也就是消抖,让输出不会因为一点小波动就来回切换。理解了这三类思路,后面看代码就会很轻松,很多组合算法其实就是把这几类思路拼在一起。
2. 10种常用滤波算法的C代码实现
下面每个算法我都会给出可以独立使用的C函数。为了保持代码简洁,有的地方用了static变量保存状态,你在工程里用的时候要注意,一个算法实例对应一组static变量,如果多个通道共用同一个函数,需要改成结构体或者加参数区分。
2.1 限幅滤波:先干掉最离谱的毛刺
原理很简单:把当前采样值和上一次的合法值比较,如果差值超过设定阈值,就认为这一次是干扰,直接丢弃,返回上一次的值。这是所有滤波算法里成本最低的一个。
#define LIMIT_MAX 50 int limit_filter(int new_value, int last_value) { if ((new_value - last_value > LIMIT_MAX) || (last_value - new_value > LIMIT_MAX)) { return last_value; } return new_value; }阈值怎么定?不要拍脑袋。先估算信号物理上的最大变化速度。比如温度传感器采样周期是100ms,系统温度最快每秒变化2度,那么相邻两次采样最多差0.2度,阈值设成0.5度就足够了。设得太大,脉冲干扰滤不干净;设得太小,正常变化都被当成异常,输出会变成一条僵硬的直线。
2.2 中位值滤波:排序取中间,脉冲干扰的克星
中位值滤波的思路是把最近N次采样放进数组,排序后取中间那个值。因为脉冲干扰通常只持续一拍,排在中间位置的概率极低,所以它能很好地剔除这类噪声。代价是每次滤波都要做一次排序,N越大越费时间。
#define N 5 int middle_filter(int new_value) { static int buf[N]; static int idx = 0; int tmp[N], i, j; buf[idx] = new_value; idx = (idx + 1) % N; for (i = 0; i < N; i++) tmp[i] = buf[i]; for (i = 0; i < N - 1; i++) { for (j = i + 1; j < N; j++) { if (tmp[j] < tmp[i]) { int t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } return tmp[N / 2]; }注意:中位值滤波对真实快速变化的信号有很强的滞后,如果你的信号本身就是快速上升沿,比如电机的电流反馈,慎用。它更适合温度、液位、光照这类变化缓慢的模拟量。
2.3 算术平均滤波:最简单直接的做法
算术平均滤波就是连续采样N次,取平均值作为输出。它对高斯白噪声的抑制效果不错,尤其适合采样周期内信号基本稳定的场景。缺点是每输出一个值就要采N次,实时性差,采样期间的信号波动会被平均掉。
#define N 12 int average_filter(void) { int sum = 0; int i; for (i = 0; i < N; i++) { sum += adc_read(); // 每次调用采集一次 } return sum / N; }N的选取有个经验:如果信号上叠加了工频干扰,N尽量取工频周期内的整数倍采样数。比如50Hz工频,采样率1kHz,N取20,这样在一个完整工频周期内平均,纹波能压掉很大一截。
2.4 递推平均滤波(滑动平均):平滑与响应的中庸之道
递推平均也叫滑动平均,它维护一个固定长度的滑动窗口,每来一个新采样值,就丢掉窗口里最旧的那个,然后对窗口内所有值求平均。相比算术平均,它不用每输出一次就重新采样N次,实时性更好,是工程里用得最频繁的平滑算法之一。
#define N 12 int sliding_average_filter(int new_value) { static int buf[N]; static int sum = 0; static int idx = 0; sum -= buf[idx]; // 移除最旧的值 buf[idx] = new_value; // 放入新值 sum += new_value; idx = (idx + 1) % N; // 更新环形索引 return sum / N; }这段代码有个容易踩的坑:首次调用时,buf数组里的元素都是0,sum - buf[idx]减掉的是0,没问题。但如果buf没有被清零,或者你在init里手动清零不完全,会导致前N次输出偏低。建议在初始化函数里显式把buf清零。
2.5 中位值平均滤波:先排序再均值,防脉冲又平滑
中位值平均滤波,也叫防脉冲干扰平均滤波。它在递推平均的基础上加了排序:先把窗口里的N个值排序,去掉最小和最大的各M个,然后对中间部分求平均。这样既滤掉了脉冲干扰,又保留了平滑能力,对付“随机噪声+偶发尖峰”的混合干扰特别好用。
#define N 12 #define M 2 int middle_average_filter(int new_value) { static int buf[N]; static int idx = 0; int tmp[N], i, j, sum = 0; buf[idx] = new_value; idx = (idx + 1) % N; for (i = 0; i < N; i++) tmp[i] = buf[i]; for (i = 0; i < N - 1; i++) { for (j = i + 1; j < N; j++) { if (tmp[j] < tmp[i]) { int t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } for (i = M; i < N - M; i++) sum += tmp[i]; return sum / (N - 2 * M); }M的取值上限是N/2 - 1,别设成N的一半或更大,否则中间什么都没剩下。一般情况下N取10到20,M取1到3就够用了。这个算法的缺点是排序有一定开销,但在单片机处理温和信号时完全没问题。
2.6 限幅平均滤波:有舍有得,先清理再平滑
限幅平均滤波是把限幅和平均组合起来:先对采样值做限幅判断,抛弃异常突变,再把通过判断的值送入滑动平均窗口。它特别适合现场电磁干扰比较强、但又需要平滑输出的场景。
#define RADAR_MAX 50 #define WINDOW_N 8 int clamp_average_filter(int new_value) { static int buf[WINDOW_N]; static int sum = 0; static int idx = 0; static int last_valid = 0; int val = new_value; if ((val - last_valid > RADAR_MAX) || (last_valid - val > RADAR_MAX)) { val = last_valid; } sum -= buf[idx]; buf[idx] = val; sum += val; idx = (idx + 1) % WINDOW_N; last_valid = val; return sum / WINDOW_N; }组合算法的最大优势是各干各的活:限幅负责剔除尖峰,平均负责压制随机噪声。缺陷也很明显,两步都会引入滞后,信号响应会比单一算法更迟钝。
2.7 一阶滞后滤波:软件版RC低通
一阶滞后滤波是RC低通滤波器的离散实现,本质是加权平均:新值的权重为a,旧值的权重为1-a。它有一个特别好的性质:无论a怎么选,都能保证输出不会突变,非常平滑。
#define FACTOR 50 // 0~100之间,整数定点 int first_order_filter(int new_value) { static int last = 0; int result; result = (FACTOR * new_value + (100 - FACTOR) * last) / 100; last = result; return result; }FACTOR实际是数字滤波的权重:越大代表新值比重越大,跟踪越快,滤波作用越弱;越小代表输出越平滑,但滞后越明显。我做温度采集时喜欢把FACTOR设在20到40之间,做按键防抖则不太用这个算法,滞后太难受。
用这种整数写法要注意溢出。FACTOR * new_value在16位int下很容易超界,建议把中间变量设成long,最后再截断。
2.8 加权递推平均滤波:给新数据更大的话语权
递推平均对所有窗口数据一视同仁,而加权递推平均会按数据新旧分配权重:越新的数据权重越大。这样做的目的是在平滑的同时减少滞后感,让输出能更快跟上信号的真实变化。
#define N 5 static const int weight[N] = {1, 2, 3, 4, 5}; int weighted_average_filter(int new_value) { static int buf[N]; int i, sum = 0, total = 0; for (i = 0; i < N - 1; i++) { buf[i] = buf[i + 1]; sum += buf[i] * weight[i]; total += weight[i]; } buf[N - 1] = new_value; sum += new_value * weight[N - 1]; total += weight[N - 1]; return sum / total; }权重的设计是关键。常见思路是让权重线性递增,比如{1,2,3,4,5}。也可以按指数递增,让新数据占比更大,但这也意味着滤波强度下降。实际使用时我建议把总和算成固定值,比如total = 15,这样连除法都能优化成移位或乘法,在MCU上更省。
2.9 消抖滤波:把按键防抖的逻辑搬到数值上
消抖滤波的思路很简单:只有连续多次采样值都落在合理范围内,才更新输出,否则保持上一次的值。它非常适合开关量、阈值判断、按键输入这类场景,本质上是防抖动。
#define COUNT_MAX 5 #define THRESHOLD 2 int debounce_filter(int new_value) { static int last = 0; static int count = 0; if ((new_value - last <= THRESHOLD) && (last - new_value <= THRESHOLD)) { count++; if (count >= COUNT_MAX) { count = 0; last = new_value; } } else { count = 0; } return last; }注意,这个函数返回的是滤波后的输出值,在不满足条件时返回的是上一次的有效值。因此刚上电的时候,last会一直是0,如果你判断的阈值本身就在0附近,需要先把last初始化成第一帧采样值。
2.10 限幅消抖滤波:防抖和限幅双重保险
限幅消抖滤波把限幅和消抖组合在一起,先判断本次采样相对上次是否超限,超限直接丢弃;没超限再进入消抖计数,连续多次稳定后更新输出。它比单纯消抖更能抵抗瞬时大干扰。
#define LIMIT 30 #define DB_COUNT 3 int clamp_debounce_filter(int new_value) { static int last = 0; static int count = 0; int val; if ((new_value - last > LIMIT) || (last - new_value > LIMIT)) { count = 0; return last; } val = new_value; count++; if (count >= DB_COUNT) { count = 0; last = val; } return last; }组合算法最大的坑是两级判断的参数容易互相打架。限幅设得太小,消抖根本没机会发挥作用;消抖次数设得太多,信号真实变化又被压住。我的习惯是先定限幅,把物理上绝对不可能出现的值档掉,再根据实际抖动周期定消抖次数。
3. 选型与参数调优:5分钟找到最合适的算法
3.1 按信号特征快速定方向
很多刚接触滤波的人会问,到底选哪个好?我的建议是先看信号特征,再看你对响应速度的要求。
如果信号是温度、湿度、液位这类变化缓慢的量,中位值、递推平均、一阶滞后随便选,滞后一点也无所谓。如果信号变化很快,比如电流采样、编码器读数,优先选限幅和加权平均,能保住响应速度。如果信号上有明显的尖峰脉冲,中位值平均和限幅平均是首选。如果做的是按键、开关、继电器状态判断,消抖滤波就够了。
3.2 参数到底怎么调
调参数是最容易让人烦躁的一步。先说N值的选取。平滑类算法的N越大,输出越平稳,但滞后也越严重。一般从N=4到N=12之间试起,别一上来就N=100。中位值相关的算法建议N取奇数,这样中间位置明确,不容易出现取整歧义。
限幅阈值要用物理意义去算。以ADC采集电压为例,如果系统最大电压变化率为每秒100mV,采样率是1kHz,那么相邻两次采样最大真实差值是0.1mV。阈值设成这个值的3到5倍,既能容错又能挡掉异常跳变。
一阶滞后的系数a是表现力最强的参数。a从0到100,对应滤波从“完全输出旧值”到“完全输出新值”。我通常从30起步,观察输出波形的毛刺幅度,再逐步减小,直到毛刺可以接受、响应滞后也在容忍范围内。
3.3 10种算法优缺点速查表
| 算法 | 核心思路 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|
| 限幅滤波 | 丢弃超限突变 | 简单、快 | 无法平滑随机噪声 | 脉冲干扰 |
| 中位值滤波 | 排序取中值 | 抗脉冲强 | 滞后大、有排序开销 | 缓变信号 |
| 算术平均 | N次采样平均 | 简单、抑制白噪声 | 实时性差 | 周期性干扰 |
| 递推平均 | 滑动窗口平均 | 实时性好、平滑 | 滞后随N增大 | 大多数模拟量 |
| 中位值平均 | 排序后去头尾平均 | 抗脉冲又平滑 | 计算量大 | 混合干扰 |
| 限幅平均 | 限幅+平均 | 抗尖峰且平滑 | 两级滞后 | 强干扰环境 |
| 一阶滞后 | 加权合成 | 超平滑、无跳变 | 滞后不可控 | 低频缓变量 |
| 加权递推平均 | 按新旧加权 | 平滑且响应快 | 权重设计麻烦 | 需要快速响应 |
| 消抖滤波 | 连续确认才更新 | 防状态抖动 | 不适合快速信号 | 按键、开关量 |
| 限幅消抖 | 限幅+消抖 | 防抖且抗大干扰 | 参数联动复杂 | 状态判断 |
4. 从代码到产品:C语言实现中的常见坑与排查技巧
4.1 C语言实现的几个暗坑
第一个坑是数据类型溢出。ADC采集结果一般是12位,最大值4095,累加20个就是81900,已经超过16位int的范围了。很多人一滤波发现结果变成负数,十有八九是这个原因。处理办法很简单:求和变量用long,或者采样次数控制在16位范围内。
第二个坑是静态数组的初始化。虽然static变量默认清零,但不同编译器的初始化行为并不完全一致,尤其是在某些低端MCU的启动代码里。我在项目里吃过亏之后,养成了写init函数的习惯,把内部buf和状态变量显式清零。
第三个坑是中位值排序的复制开销。很多参考代码直接对static数组排序,这会把缓存里的顺序打乱,导致窗口数据错乱。正确的做法是先拷贝到临时数组,再对临时数组排序。示例代码里我全部用了拷贝方式,避免这个问题。
4.2 滤波后依然抖动的排查流程
如果滤波后数据还在抖,先别急着加大N或者调小a,按这个顺序排查。
第一步,关掉滤波,直接打印原始数据,确认干扰从哪里来。如果原始数据本身就不稳定,先查硬件:接地、电源纹波、传感器驱动时序都有可能。第二步,确认滤波函数被调用的频率和采样率一致。很多人在中断里采样,在主循环里滤波,频率不匹配会导致滤波效果完全不对。第三步,用串口或者上位机同时打印原始值和滤波值,观察一段时间。如果滤波值只是滞后但不抖,说明算法没问题,是参数太保守;如果滤波值还在剧烈跳动,说明干扰超出了当前算法的处理能力,考虑换组合算法。
我在调试时会用一个小技巧:把滤波前后数据用两个通道输出,一个接原始信号,一个接滤波后信号,同时显示在波形图上。这样一眼就能看出滞后量、残余噪声和响应速度,比自己盯着串口数字猜效率高太多。
4.3 串级滤波与工程化封装
单一算法很难做到面面俱到,实际项目里我更推荐串级组合。比如ADC采集电压数据,我常用“限幅 -> 递推平均 -> 一阶滞后”这样的链路:限幅干掉尖峰,递推平均压住随机噪声,一阶滞后保证最终输出平滑。效果往往比单独调任何一个算法都好。
但串级不是越多越好。每一级都在引入滞后,串三级以后,系统响应慢得像蜗牛。我的原则是能两级解决的事,绝不用三级。
从工程化角度,建议把每个滤波器封装成独立模块,定义成结构体形式,包含状态变量和参数项。这样就能很方便地为多个通道各创建一个实例,互相不干扰,也方便在调试时动态切换算法。等代码稳定以后,再根据MCU的ROM和RAM预算裁剪成静态实现,这样既能享受模块化的清晰,又能保证最终资源的可控。
我在实际使用中发现,滤波算法的选择其实是个性化很强的工程决策。同一个温度数据,做恒温控制的人要陡响应,做数据记录的人要平滑,没有绝对正确的答案,只有相对合适的组合与参数。多试、多对比波形,比记住任何结论都管用。
本文还有配套的精品资源,点击获取