嵌入式ADC采样滤波:10种常用算法C语言实现与选型指南

发布时间:2026/9/9 21:58:43
嵌入式ADC采样滤波:10种常用算法C语言实现与选型指南 简介针对单片机与嵌入式系统中的数字滤波需求这里整理了一套包含10种常用滤波算法的C代码资源适合需要处理传感器噪声、脉冲干扰或周期性波动的开发者使用。压缩包内共3个文件包含可直接参考的C源程序、系统讲解10种软件滤波方法的Word文档以及一篇单片机数字滤波算法研究的PDF论文整体大小约198KB目前已有3185人学习下载。内容覆盖限幅滤波、中位值滤波、算术平均滤波、递推平均滤波、中位值平均滤波、限幅平均滤波、一阶滞后滤波、加权递推平均滤波、消抖滤波和限幅消抖滤波每种算法均列明实现方法、优点与缺点便于在流量、压力、液位、温度等不同测量场景中评估滤波效果并完成选型也能对照C代码直接移植到实际项目快速验证滤波逻辑。 刚接手嵌入式项目那会儿我最怕处理的就是ADC采样回来的数据。传感器明明接得好好的数值却像蹦迪一样上下跳加个滤波吧数据是稳了但真实变化也跟着变迟钝调试时差点怀疑人生。后来才明白滤波这事不该上来就写代码得先搞清楚信号里混的是什么噪声、采样周期是多少、允许的响应延时有多长。这篇就把我用过的10种常用滤波算法从头到尾梳理一遍全部给出C语言实现附上选型思路、参数调优经验和实际对比数据适合正在做传感器采集、单片机开发、仪器仪表的朋友收藏对照。1. 先判断噪声再选算法滤波第一步其实是“诊断”很多朋友觉得滤波就是“低通”拿一个平均或者一阶滞后到处用结果遇到继电器干扰、电机火花时毫无办法数据还是乱跳。问题不在代码而在没有先分析信号里混杂的噪声形态。滤波选型的顺序应该是先看噪声长什么样再决定用哪种策略最后才动手写代码。同样的信号用对算法几行代码就能压住几十条if判断。1.1 毛刺、抖动、漂移先认清你要对付的是谁传感器信号里的噪声大致分三种形态。第一种是脉冲毛刺也就是偶发的尖峰通常来自电机启停、继电器动作、静电放电或者通信干扰。它的特点是幅值大、持续时间短和真实值之间往往差了几百甚至上千个码值。第二种是随机抖动信号围绕真实值上下快速波动幅度不大但频率高是传感器固有噪声的主要形态。第三种是缓慢漂移例如温度变化引起的零点偏移周期很长。打个比方就很好理解毛刺像朋友说话时旁边突然有人插了一嗓子随机抖动像三个人同时小声嘀咕而漂移像是背景音乐音量慢慢变大。三种噪声的处理思路完全不同毛刺要靠限幅、中位值这类“不信它”的算法来剔除随机抖动要靠平均、滞后这类“综合群众意见”的算法来平滑漂移通常用标定、差分或高通思路解决10种经典算法里没有专门对付它的别指望平均或限幅能解决。1.2 十种算法按策略分组抗脉冲、平滑、组合这10种算法按策略可以分成三组这样记起来就轻松多了策略包含算法典型用途抗脉冲干扰限幅滤波、中位值滤波、消抖滤波、限幅消抖电机干扰、继电器火花、静电、开关信号平滑随机噪声算术平均、滑动平均、加权递推平均、一阶滞后传感器固有噪声、信号高频抖动组合策略中位值平均、限幅平均既有脉冲又有随机噪声的复杂工况同一款算法不存在全能的组合策略往往更实用。正因如此我建议把10种算法当成一组工具箱而不是一张排名表。遇到问题先归类再从中挑最合适的零件。2. 抗脉冲干扰三件套限幅、中位值与消抖的C实现先来看抗脉冲干扰这一类。为什么这类噪声不能用平均类滤波因为脉冲的幅值很大平均计算会把一个尖峰“摊”到前后N个点上相当于把毛刺变成了一段持续的凸起数据虽然不跳了但失真更隐蔽。限幅、中位值、消抖这些方法在脉冲尚未影响整体统计之前就直接拒绝它思路完全不同。2.1 限幅滤波给相邻采样值的“信任额度”设门槛限幅滤波的逻辑最简单相邻两次采样值的差值超过设定阈值就认为本次采样不正常输出上一次的值否则正常输出。它等于给每个新采样值设了一个“信任额度”超出额度就不予通过。#define LIMIT_VALUE 50 /* 阈值按信号最大变化率设定 */ int limit_filter(int new_value, int last_output) { if ((new_value - last_output LIMIT_VALUE) || (last_output - new_value LIMIT_VALUE)) { return last_output; } return new_value; }这里有个容易踩的坑上一次的参考值到底用原始采样值还是上一次的滤波输出值我建议用上一次滤波输出值。如果拿上一次原始采样值做基准一次脉冲之后连续几个正常点都会被误杀因为它们的差值是相对脉冲值算的而不是相对正常值。限幅滤波非常适合温度、液位、转速这类物理量不会突然跳变的场景缺点是一旦真实信号快速变化就容易被误伤阈值需要仔细设计后面第5章专门讲。2.2 中位值滤波把排序结果的正中间那个数拿出来中位值滤波的思路是连续采样N次排序后取中间那个数作为输出。N必须取奇数否则取中间偏左偏右都别扭。int median_filter(int *buf, int len) { int temp, i, j; /* 冒泡排序数据量小够用 */ for (i 0; i len - 1; i) { for (j 0; j len - i - 1; j) { if (buf[j] buf[j 1]) { temp buf[j]; buf[j] buf[j 1]; buf[j 1] temp; } } } return buf[len / 2]; }调用的方式通常是连续采集5个点存进数组调用median_filter然后把数组重置继续采。这里有三个注意事项第一排序会直接改动传入的数组如果调用方还要用原始数据就得先拷贝一遍第二N一般取3或5最多取7再大排序开销和输出滞后都会明显增加第三中位值滤波适合温度、液位这类变化缓慢的信号用在振动或音频信号上输出看起来就像被“削掉”了细节。2.3 消抖滤波连续N次一致才更新输出消抖滤波在嵌入式里最常见的名字叫“软件消抖”典型应用是按键、光电开关、霍尔传感器这类逻辑电平信号。它的规则是连续N次采样都得到相同值才更新输出一旦中间出现一次不同计数清零重来。#define DBNCE_CNT 5 /* 连续相同N次才算稳定 */ int debounce_filter(int new_value, int *counter, int *last_valid) { if (new_value ! *last_valid) { (*counter); if (*counter DBNCE_CNT) { *last_valid new_value; *counter 0; } } else { *counter 0; } return *last_valid; }这个函数的返回值是上一次确认有效的值不是本次采样值。中间的计数参数由调用方维护这样同一个函数可以服务多个通道互不干扰。消抖滤波的代价是响应延迟N越大抗干扰越好但输出越迟钝。对于按键这类人类操作5到20次判断都感知不到延迟对于霍尔测速这类高速计数N就得尽量小。2.4 限幅消抖先做物理约束再做状态确认如果把限幅和消抖合起来就得到限幅消抖滤波先判断本次采样值和上次采样值是否超限如果超限就认为这次采样不可信直接清零计数如果没超限再走消抖逻辑。int limit_debounce_filter(int new_value, int last_sample, int *counter, int *last_valid) { if ((new_value - last_sample LIMIT_VALUE) || (last_sample - new_value LIMIT_VALUE)) { *counter 0; return *last_valid; } if (new_value ! *last_valid) { (*counter); if (*counter DBNCE_CNT) { *last_valid new_value; *counter 0; } } else { *counter 0; } return *last_valid; }注意这里的last_sample是上一次的原始采样值不是滤波输出值它做的是物理层面的“初步可信度验证”last_valid才是上次确认有效的状态值。限幅消抖比单独消抖强的地方在于物理上不可能出现的快速跳变会被提前拦截不会给计数器“攒骗局”的机会。比如一个光电传感器正常情况下频率最多100Hz结果一个采样周期内状态翻了好几次这一定是干扰先踢出去再做消抖判断。3. 平滑随机噪声平均类与一阶滞后滤波的C实现平均类和一阶滞后是把随机噪声“磨平”的常用手段。它们对零均值随机噪声非常有效噪声越大效果越明显。跟抗脉冲策略不同这类算法会处理每一个采样值而不是选择性地相信或丢弃。3.1 算术平均滤波攒一批算一次算术平均滤波是很多人最早接触的滤波方式连续采集N个点求和取平均输出一次结果然后重新采集。int average_filter(int *buf, int len) { int sum 0, i; for (i 0; i len; i) { sum buf[i]; } return sum / len; }这个算法的特点是简单可靠输出频率降低为采样率的1/N每次都要等N个点凑齐才能输出实时性较差。它适合温湿度记录仪、电池电压巡检这类周期性上报数据的场景。有个细节如果采样值是12位ADC且N较大int范围内的sum一般不会溢出但如果你用的是16位MCU且N超过64最好把sum声明为long避免累加溢出。3.2 滑动平均滤波环形缓冲里的实时平均值滑动平均滤波也叫递推平均滤波它和算术平均最大的区别是“窗口滑动”。每采到一个新值就覆盖掉窗口里最旧的值立刻输出窗口内平均值不用等人凑齐一批。#define WIN_SIZE 8 static int history_buf[WIN_SIZE]; static int history_index 0; int sliding_average_filter(int new_value) { int sum 0, i; history_buf[history_index] new_value; history_index (history_index 1) % WIN_SIZE; for (i 0; i WIN_SIZE; i) { sum history_buf[i]; } return sum / WIN_SIZE; }这里用环形缓冲区保存最近N个值index永远指向下一次要覆盖的位置。滑动平均的响应延迟大约是(N-1)/2个采样周期实时性比算术平均好很多因此特别适合压力、倾角这类需要连续输出的系统。要注意的是新值进来后被平均的权重只有1/N真实突变会被“稀释”如果信号本身变化快窗口不能设得太大。3.3 加权递推平均给信任度加个权重梯度滑动平均把窗口里所有数据一视同仁但直觉告诉我们越靠近当前时刻的数据越接近真实状态。加权递推平均就是给不同位置的数据分配不同权重通常越新的数据权重越大。#define WIN_SIZE 4 static int history_buf[WIN_SIZE]; static const int weight[WIN_SIZE] {4, 3, 2, 1}; #define WEIGHT_SUM 10 int weighted_sliding_filter(int new_value) { int sum 0, i; /* 数据整体后移最新值放最前面 */ for (i WIN_SIZE - 1; i 0; i--) { history_buf[i] history_buf[i - 1]; } history_buf[0] new_value; for (i 0; i WIN_SIZE; i) { sum history_buf[i] * weight[i]; } return sum / WEIGHT_SUM; }权重设计有个小技巧尽量用线性递增的整数权重比如{1,2,3,4}或{1,2,4,8}权重总和可以整除这样平均时直接用整数除法不牵涉浮点。加权递推平均的滞后比普通滑动平均小但因为让最近的数据“多说话”对脉冲的抗性也变差了实战里通常要在前级加限幅或中位值否则一个尖峰可能带偏整个输出。3.4 一阶滞后滤波用一个系数实现无限记忆一阶滞后滤波的公式是y[k] a * x[k] (1 - a) * y[k-1]a是滤波系数范围0到1。它把上一拍的输出和本拍的输入按比例混合内存占用小到极致。#define FILTER_COEF 30 /* 相当于 a 0.3 */ int first_order_filter(int new_value) { static int last_output 0; static unsigned char initialized 0; int output; /* 冷启动处理第一次直接把采样值当作输出 */ if (!initialized) { last_output new_value; initialized 1; } output (FILTER_COEF * new_value (100 - FILTER_COEF) * last_output) / 100; last_output output; return output; }一阶滞后滤波几乎是为单片机量身定做的只要两个变量、一次乘加运算没有数组也没有排序而且系数是连续可调的。a越大越信任新值响应快但平滑弱a越小越依赖历史平滑强但滞后明显。它本质是IIR滤波器历史影响会一直存在所以代码里专门做了冷启动处理第一次采样直接赋值避免从0慢慢爬导致的初始失真。4. 组合打法同时解决毛刺和平滑的两种策略现实的传感器数据往往不是单一噪声。有的信号既有偶发脉冲又有持续抖动只用一种策略压不住。组合策略的思路就是先把脉冲剔除再对剩余数据做平滑处理。4.1 中位值平均滤波排序后掐头去尾再平均中位值平均滤波也叫防脉冲干扰平均滤波它把中位值和算术平均结合连续采样N个数排序后去掉最大值和最小值剩下的N-2个数求平均。#define FILTER_N 7 int median_average_filter(int *buf, int len) { int temp, i, j, sum 0; for (i 0; i len - 1; i) { for (j 0; j len - i - 1; j) { if (buf[j] buf[j 1]) { temp buf[j]; buf[j] buf[j 1]; buf[j 1] temp; } } } /* 去掉最小值buf[0]和最大值buf[len-1] */ for (i 1; i len - 1; i) { sum buf[i]; } return sum / (len - 2); }这个组合的价值在于去掉最大最小之后剩下的数据基本剔除了偶发脉冲影响再用平均处理剩余随机噪声输出的稳定性比单独用中位值或算术平均都好。N一般取5或7工业测量里用它处理热电阻、压力变送器这类缓慢变化信号效果很稳。代价是要排序N等于7时冒泡排序最坏要21次比较对几十毫秒采样周期来说完全够用。4.2 限幅平均滤波拦截异常值后交给滑动平均限幅平均滤波是“限幅滑动平均”的组合逐个采样值先进限幅关卡通过后才进入滑动平均窗口不满足限幅条件的值直接用上一轮输出顶替避免异常值污染窗口。int limit_average_filter(int new_value, int last_output) { int sum 0, i; int valid_value new_value; if ((new_value - last_output LIMIT_VALUE) || (last_output - new_value LIMIT_VALUE)) { valid_value last_output; } history_buf[history_index] valid_value; history_index (history_index 1) % WIN_SIZE; for (i 0; i WIN_SIZE; i) { sum history_buf[i]; } return sum / WIN_SIZE; }这个方案比中位值平均的实时性更好滑动平均每采一个点就能输出一个值不被N点一批的节奏拖住。它非常适合电流环、速度环这类实时闭环控制系统的采样通道既要求滤波平滑又要求输出延迟尽量小。需要重点提醒的是限幅的那一步必须在进窗口之前做如果把原始脉冲值直接塞进窗口滑动平均会把脉冲摊到多个周期上和没滤差不多。5. 参数定夺与实测对比同一串数据的不同归宿算法选好了参数定不对等于白选。窗口N、限幅阈值、滤波系数三个参数分别对应响应时间、物理约束、平滑程度三个维度。5.1 一组真实采样数据在三种算法下的对比为了更直观地说明问题我拿一组模拟的ADC采样值做了一组对比序列中间故意放了一个1800的脉冲毛刺真实值大约在1250附近采样序号原始值限幅输出(阈值50)滑动平均输出(N4)一阶滞后(a0.3)1125012501250125021248124812491249312701270125612554180012701392141951260126013941371612531253139513367124912491390131081258125812551294限幅把脉冲挡在了门外输出几乎不受影响。但滑动平均被1800带偏了4个周期直到第8个点才回到1255左右一阶滞后输出虽然回落的趋势明显但第8个点仍比真实值高约36个码值。这印证了前面反复提到的观点纯平均类算法没办法独立对付脉冲干扰实战中组合策略的价值就在这里。5.2 窗口长度N不要拍脑袋从滞后时间倒推窗口N决定的其实是“输出对真实变化的反应速度”。滑动平均的群延迟约为(N-1)/2个采样周期也就是说真实信号发生阶跃后你要等大约(N-1)/2拍才能跟踪到变化的中点。所以N应该从需求反推比如系统允许滤波延迟50ms采样周期5ms那N最大取21左右再大就会明显滞后。根据经验不同信号可以参考以下取值信号类型经验窗口N温度8~20压力、液位4~8加速度2~4电机电流2~8配合控制周期调整另外有一个代码层面的提醒如果N取2的幂比如4、8、16取平均时可以用右移代替除法sum 2或者sum 3运行速度会快不少。5.3 限幅阈值用最大物理变化率反算限幅阈值是整个限幅滤波里最关键、也最容易拍脑袋定的参数。一个靠谱的办法是阈值 信号最大物理变化率 × 采样周期然后留1.5到3倍余量。举个例子某温度传感器最大升温速率是0.5℃每秒采样周期100ms那么一个采样周期内最多变化0.05℃。如果ADC分辨率是0.1℃每个码值那理论上阈值取1就够了但实际噪声和量化误差难免建议取3到5。如果没有物理模型可以参考就抓一段正常工况下的数据统计相邻采样点差值的最大值再乘1.5到2倍作为阈值。阈值太小会误杀正常信号数据变得僵硬阈值太大又等于没滤毛刺长驱直入。5.4 一阶滞后系数从0.3起步盯着响应调一阶滞后系数a给了一个连续可调的旋钮但也正因如此新手容易来回乱调。我的习惯是从0.3起步然后给信号一个人为的阶跃输入观察输出跟踪到目标90%需要多久。a等于0.3时大约7个采样周期到90%a等于0.1时要20多个周期a等于0.5时3到4个周期就差不多了。要求输出跟手就加大a要求滤波结果平滑就减小a。实际项目里我还用过一种动态系数思路数据平稳时用小系数把噪声压住当检测到输入与输出偏差大于某个阈值时临时切到大系数让输出尽快跟上真实变化跟上了再切回去。这种“可变一阶滞后”在工业现场效果很好但只在确实需要同时兼顾响应和平滑时才建议用。6. 嵌入式工程落地从能跑到好用的几个细节代码能跑只是第一步。嵌入式环境资源有限还要面对冷启动、浮点性能、调试手段等问题。下面几个细节是工程化时最容易踩坑的地方。6.1 首采值处理滤波器的“冷启动”问题滑动平均窗口没填满时怎么输出一阶滞后第一次输出要不要跳变这些问题不加处理开机头几百毫秒的数据基本不能看。我习惯用两个方案之一解决一是先用第一个采样值填充整个窗口缓冲区再开始正常滤波二是启动阶段按实际已有数据量做部分平均窗口填满后才完全进入滑动平均模式。for (i 0; i WIN_SIZE; i) { history_buf[i] first_value; }一阶滞后的冷启动处理更简单第一次调用时直接把last_output赋成当前采样值也就是前面代码里initialized标志的作用。这样开机后的输出是从真实起点开始的而不是从0慢慢爬上去省去好几秒的收敛时间。6.2 整数运算替代浮点省下的不止是时间很多单片机的内核没有FPU浮点运算要靠软件库仿真一次浮点乘法可能消耗几十甚至上百个时钟周期代码体积也会增加。所以滤波参数我全部用整数表达。一阶滞后的系数a用0到100的整数表示百分比换算成整式int output last_output (FILTER_COEF * (new_value - last_output)) / 100;这个写法和原式完全等价还少了一次乘法。如果系数固定编译器甚至会把除以100优化成乘法和移位。限幅判断里用两次减法比较差值避免调用abs函数也省掉了标准库依赖。这些优化对跑在STM32F103、STC8这类芯片上的项目效果非常明显。6.3 留一个调试后门参数在运行时可调最后分享一个贯穿我整个开发周期的小习惯给滤波器开一个调试后门。哪怕只是临时加一个串口打印把原始值、滤波值、当前参数周期性地打出来调试效率都能翻倍。我通常先抓一段原始数据在PC上用同样的算法离线试算确定参数后再固化进单片机如果现场工况复杂把限幅阈值、滤波系数做成运行时可改的动态变量通过串口命令调整就不用反复烧录固件试错。我现在做滤波方案基本是“先分类、再组合、后调参”每次遇到新传感器先花半天时间抓数据剩下的就是套用这10种算法组合成一套合适的处理链。思路理顺了这些算法就是一组随时可以调用的零件真正难的从来不是代码本身而是能不能判断出当下那个信号需要哪种策略。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询