简介:这份资源面向嵌入式开发、单片机应用及信号采集方向的工程师与学生,系统整理了10种常用软件滤波算法的C语言实现,帮助解决传感器采样中随机干扰、周期性波动与脉冲噪声等实际问题。压缩包共3个文件,包含1个C源码文件、1个Word说明文档和1个PDF研究资料,整体约198KB,体积轻巧便于随工程携带与查阅。内容覆盖限幅滤波、中位值滤波、算术平均、递推平均、中位值平均、限幅平均、一阶滞后、加权递推平均、消抖及限幅消抖等算法,每种均给出方法原理、优缺点与适用参数取值,如流量取N=12、压力取N=4等经验值,可直接对照移植到实际项目中。目前已有3190人学习下载,适合需要快速选型、对比滤波效果并落地C代码的开发者参考。
1. 从一段“数据抖动”说起:这套滤波算法C代码到底能干什么
如果你做过单片机采集、传感器数据处理或者嵌入式信号调理,大概率遇到过这种情况:ADC采回来的数值像心电图一样上下乱跳,明明传感器手册标称精度够用,实际读出来就是不稳。很多人第一反应是加电容、改PCB布局,硬件折腾一圈之后发现改善有限,最后才意识到问题出在软件端——没有做数字滤波。这份“10种常用的滤波算法(C代码)”就是冲着这个场景来的:它把嵌入式开发中最常用的十种滤波算法用C语言实现了一遍,每个算法独立成文件,接口清晰,可以直接移植到STM32、GD32、ESP32或者任何带C编译器的平台上。适合谁?刚入行的嵌入式软件工程师、做毕业设计需要处理传感器数据的学生、以及想给现有采集系统加一层软件滤波但不想从零推导公式的开发者。它不涉及复杂的自适应滤波理论,也不依赖任何第三方库,纯C实现,拿到手改改宏定义就能用。
2. 项目结构拆解:十个算法文件怎么组织、怎么选
2.1 目录布局与文件职责
这份代码的组织方式很“嵌入式”——没有花哨的构建系统,就是一组.c和.h文件平铺在src目录下,外加一个main.c做演示。常见做法是每个算法一对文件,比如filter_moving_average.c配filter_moving_average.h,头文件里放函数声明和必要的宏,源文件里放具体实现。这样做的目的是让使用者按需取用,不需要把十个算法全编译进去。我一般会在项目里建一个filters文件夹,把这十个文件丢进去,然后在IDE里只添加实际用到的那几个。
从功能维度看,这十个算法大致可以分成三类:第一类是时域平均类,包括算术平均滤波、滑动平均滤波、加权平均滤波、中位值滤波;第二类是动态响应类,包括一阶滞后滤波、限幅滤波、限速滤波;第三类是组合与统计类,包括防脉冲干扰平均滤波、中位值平均滤波、以及一个简单的卡尔曼滤波实现。分类的意义在于选型:采样慢、要求平滑的用平均类;采样快、要求跟随的用滞后或限幅类;有脉冲噪声的用中位值或防脉冲组合。
2.2 统一接口设计:为什么每个算法都长一个样
这份代码最值得借鉴的地方是接口统一。每个滤波算法都暴露一个初始化函数和一个滤波函数,命名遵循xxx_filter_init和xxx_filter_update的格式。初始化函数负责设置窗口大小、初始值等参数,滤波函数接收一个新采样值,返回滤波后的值。下面是一个典型的滑动平均滤波实现:
// filter_moving_average.h #ifndef FILTER_MOVING_AVERAGE_H #define FILTER_MOVING_AVERAGE_H #define MOVING_AVG_WINDOW_SIZE 10 typedef struct { float buffer[MOVING_AVG_WINDOW_SIZE]; float sum; unsigned char index; unsigned char count; } moving_avg_t; void moving_avg_init(moving_avg_t *filter); float moving_avg_update(moving_avg_t *filter, float new_value); #endif// filter_moving_average.c #include "filter_moving_average.h" void moving_avg_init(moving_avg_t *filter) { unsigned char i; for (i = 0; i < MOVING_AVG_WINDOW_SIZE; i++) { filter->buffer[i] = 0.0f; } filter->sum = 0.0f; filter->index = 0; filter->count = 0; } float moving_avg_update(moving_avg_t *filter, float new_value) { // 减去最旧的值,加上最新的值,保持窗口内总和 filter->sum -= filter->buffer[filter->index]; filter->buffer[filter->index] = new_value; filter->sum += new_value; filter->index++; if (filter->index >= MOVING_AVG_WINDOW_SIZE) { filter->index = 0; } // 窗口未填满时按实际数量平均,避免初始阶段被零值拉低 if (filter->count < MOVING_AVG_WINDOW_SIZE) { filter->count++; } return filter->sum / (float)filter->count; }这段代码的逻辑说明:buffer是一个环形缓冲区,index指向下一个要覆盖的位置,sum维护窗口内所有值的累加和。每次更新时,先减去被覆盖的旧值,再写入新值并累加,这样避免了每次遍历窗口求和,把时间复杂度从O(n)降到O(1)。count变量用来处理启动阶段窗口未填满的情况,否则前几个采样值会被缓冲区里的初始零值稀释,导致输出偏小。参数说明:MOVING_AVG_WINDOW_SIZE决定平滑程度,值越大输出越稳但滞后越明显,一般取8到20之间;float类型可以根据平台换成double或定点数。
2.3 中位值与防脉冲:对付尖峰干扰的两种思路
中位值滤波的核心是排序后取中间值,对偶发的脉冲干扰特别有效。这份代码里用了一个简单的冒泡排序,窗口大小通常取5或7。防脉冲干扰平均滤波则是“先去掉一个最大值和一个最小值,再对剩下的求平均”,相当于中位值和平均值的折中。下面看中位值滤波的关键片段:
// filter_median.c 核心部分 float median_filter_update(median_filter_t *filter, float new_value) { float temp[MEDIAN_WINDOW_SIZE]; unsigned char i, j; float swap; // 滑动窗口:搬移旧数据,存入新值 for (i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) { filter->buffer[i] = filter->buffer[i + 1]; } filter->buffer[MEDIAN_WINDOW_SIZE - 1] = new_value; // 复制到临时数组进行排序,不破坏原始缓冲区顺序 for (i = 0; i < MEDIAN_WINDOW_SIZE; i++) { temp[i] = filter->buffer[i]; } // 冒泡排序,窗口小,开销可接受 for (i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) { for (j = 0; j < MEDIAN_WINDOW_SIZE - 1 - i; j++) { if (temp[j] > temp[j + 1]) { swap = temp[j]; temp[j] = temp[j + 1]; temp[j + 1] = swap; } } } // 取中间值,窗口为偶数时取中间两个的平均 if (MEDIAN_WINDOW_SIZE % 2 == 1) { return temp[MEDIAN_WINDOW_SIZE / 2]; } else { return (temp[MEDIAN_WINDOW_SIZE / 2 - 1] + temp[MEDIAN_WINDOW_SIZE / 2]) / 2.0f; } }逻辑说明:每次更新时,先把缓冲区整体左移,腾出最后一个位置放新值,这样缓冲区里始终保存最近N个采样。排序在临时数组上进行,不影响缓冲区的时序。参数说明:MEDIAN_WINDOW_SIZE建议取奇数,5或7在多数场景下够用;如果采样率很高而MCU主频较低,冒泡排序可能成为瓶颈,常见做法是改用插入排序或直接维护一个有序链表。注意,中位值滤波对缓变信号会引入额外滞后,不适合快速跟随的场景。
3. 编译与运行:从裸机到PC验证的完整步骤
3.1 在PC上先跑通:用gcc验证算法行为
拿到一份嵌入式C代码,我习惯先在PC上编译运行,用打印的方式观察滤波效果,确认算法逻辑没问题再往MCU上搬。这份代码的main.c里通常包含一个演示函数,生成一段带噪声的正弦波或阶跃信号,分别喂给各个滤波器,打印原始值和滤波值。编译命令很简单:
# 在项目根目录下执行,假设源文件在src目录 gcc -o filter_demo src/main.c src/filter_moving_average.c src/filter_median.c src/filter_limiter.c -lm ./filter_demo参数说明:-lm是链接数学库,如果代码里用了sinf、fabsf等函数就需要加;如果只编译部分算法,按实际用到的.c文件替换即可。运行后会看到类似raw: 1023.4, filtered: 1018.7的输出,观察滤波后的曲线是否更平滑、滞后是否可接受。这一步能帮你快速排除数组越界、初始值未清零之类的低级错误。
3.2 移植到MCU:需要改哪些地方
移植到STM32或其他MCU时,主要改三个地方。第一,把main.c里的演示代码替换成你的ADC读取和串口打印逻辑;第二,如果MCU不支持硬件浮点,把float换成double或定点int32_t,同时调整除法运算;第三,根据实际采样率调整窗口大小和滤波系数。以STM32 HAL库为例,在ADC转换完成回调里调用滤波函数:
// 在ADC中断回调中调用 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { uint16_t raw = HAL_ADC_GetValue(hadc); float filtered = moving_avg_update(&g_moving_avg, (float)raw); // 将filtered用于后续控制或显示 g_sensor_value = filtered; }注意:滤波函数本身不是线程安全的,如果同时在中断和主循环里调用同一个滤波器实例,需要加临界区保护或者用双缓冲。常见做法是只在中断里做原始值采集和滤波,主循环读取滤波结果。
3.3 参数整定:窗口大小和滤波系数的经验值
窗口大小和系数没有万能公式,但有一些经验范围。滑动平均窗口取8到20,对应截止频率约为采样率的1/10到1/20;一阶滞后滤波的系数α取0.1到0.3,α越小越平滑但响应越慢;限幅滤波的阈值取传感器最大允许跳变量的1.5到2倍。我一般会先用一个可调参数跑一遍实际数据,把原始值和滤波值同时打到串口,用上位机画曲线对比,调到自己满意为止。别指望一次调好,这是个反复试的过程。
4. 避坑与排查:五个实际调试中踩过的坑
4.1 现象:滤波后数值一直偏小,稳定后也不对
原因:滑动平均或算术平均在启动阶段,缓冲区里全是初始零值,前几个输出被零值拉低。如果初始化时没有把count置零或者没有做窗口未满时的特殊处理,就会一直偏小。解决:像2.2节代码那样,用count变量记录实际有效样本数,窗口未满时除以count而不是窗口大小。或者初始化时用第一个采样值填满整个缓冲区。
4.2 现象:中位值滤波输出偶尔跳变,和原始值一样
原因:窗口大小取了偶数,中间两个值相差较大时取平均,反而引入了一个“人造”的中间值。解决:把窗口大小改成奇数,5或7最常用。如果必须用偶数窗口,考虑取中间两个中更接近上一次输出的那个,而不是简单平均。
4.3 现象:一阶滞后滤波在阶跃响应时爬升太慢,控制环路振荡
原因:α取得太小,滤波器的相位滞后超过了控制环路的相位裕度。解决:先确定控制环路的带宽要求,再反推α的下限。经验公式是α ≥ 2π × 控制带宽 / 采样率。如果还是不够快,换用限幅滤波或卡尔曼滤波。
4.4 现象:移植到MCU后输出全是零或乱码
原因:结构体没有对齐,或者栈空间不足导致局部数组溢出。解决:检查结构体定义是否用了__attribute__((packed))导致非对齐访问;把大数组从栈上移到全局或静态区;确认编译器的浮点ABI设置和库文件匹配。
4.5 现象:多个滤波器串联后输出振荡
原因:两个滤波器各自引入相位滞后,串联后总滞后叠加,在闭环系统中可能引发振荡。解决:不要盲目串联多个滤波器。如果一级滤波不够,优先调整参数或换算法,而不是级联。确实需要级联时,把响应快、滞后小的放在前面。
5. 二次开发与改造:从能用到好用
5.1 把浮点改成定点,省下硬件浮点单元
很多低成本MCU没有FPU,用float会拖慢速度、增大代码体积。改造思路是把所有浮点运算转成Q格式定点数。以滑动平均为例,把float换成int32_t,窗口大小取2的幂次,除法用右移代替:
// 定点滑动平均,窗口大小16,Q12格式 #define FIXED_SHIFT 12 #define FIXED_ONE (1 << FIXED_SHIFT) int32_t fixed_moving_avg_update(fixed_moving_avg_t *f, int32_t new_value) { f->sum -= f->buffer[f->index]; f->buffer[f->index] = new_value; f->sum += new_value; f->index = (f->index + 1) & (FIXED_WINDOW_SIZE - 1); // 窗口大小为16,右移4位完成除法 return f->sum >> 4; }注意:定点数加法可能溢出,sum要用int64_t或者确保窗口大小和采样位宽不会导致溢出。改造后运算速度通常能提升3到5倍,代码体积减少约30%。
5.2 加一层参数配置接口,方便上位机在线调整
实际项目中,滤波参数经常需要根据工况调整。我一般会给每个滤波器加一个set_param函数,通过串口协议接收上位机下发的参数。比如滑动平均的窗口大小、一阶滞后的α值。这样不用重新烧录就能调参,省下大量时间。实现时注意参数范围校验,窗口大小不能超过缓冲区数组长度,α必须在0到1之间。
5.3 用表格对比选型,别凭感觉挑算法
下面这张表是我在实际项目中总结的选型参考,列了五种最常用算法的适用场景和代价:
| 算法 | 适用场景 | 滞后程度 | 计算开销 | 抗脉冲能力 |
|---|---|---|---|---|
| 滑动平均 | 缓变信号、ADC去噪 | 中等 | 低 | 弱 |
| 中位值 | 有偶发尖峰干扰 | 较大 | 中 | 强 |
| 一阶滞后 | 快速跟随、闭环控制 | 小 | 极低 | 弱 |
| 限幅滤波 | 剔除明显异常值 | 无 | 极低 | 中 |
| 防脉冲平均 | 既要平滑又要抗脉冲 | 中等 | 中 | 较强 |
选型时先问自己三个问题:信号变化快不快?干扰是持续噪声还是偶发脉冲?MCU还剩多少算力?答案清楚了,算法基本就定了。
5.4 验证方法:用阶跃响应和噪声注入测一遍
改完代码别急着上系统,先做两个测试。第一个是阶跃响应:给滤波器输入一个从0跳到100的阶跃信号,观察输出爬到63.2%和90%需要多少个采样周期,这能直观反映滞后。第二个是噪声注入:在原始信号上叠加高斯白噪声和随机脉冲,看滤波后的信噪比改善和脉冲抑制效果。我习惯把这两组数据打到串口,用Python画出来对比,比盯着数字看靠谱得多。
从那以后我每次改滤波参数,都强制走一遍阶跃响应和噪声注入测试,不再凭感觉调。希望帮到你。
本文还有配套的精品资源,点击获取