资讯动态

嵌入式开发必备:10种常用滤波算法C语言实现与选型指南

发布时间:2026/9/9 22:16:28 来源:尧图企业网站定制
简介面向嵌入式与单片机开发者的实用滤波算法资源系统整理了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工频采样率1kHzN取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数组里的元素都是0sum - 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到20M取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越大输出越平稳但滞后也越严重。一般从N4到N12之间试起别一上来就N100。中位值相关的算法建议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预算裁剪成静态实现这样既能享受模块化的清晰又能保证最终资源的可控。我在实际使用中发现滤波算法的选择其实是个性化很强的工程决策。同一个温度数据做恒温控制的人要陡响应做数据记录的人要平滑没有绝对正确的答案只有相对合适的组合与参数。多试、多对比波形比记住任何结论都管用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价