10种经典软件滤波算法C语言实现:从限幅到自适应滤波
1. 项目缘起为什么我们需要软件滤波在嵌入式开发、数据采集或者任何涉及传感器信号处理的场景里你肯定遇到过这样的问题从ADC模数转换器读回来的数据总是带着点“毛刺”。比如一个理论上应该稳定在3.3V的电压读数却在3.28V到3.32V之间来回跳动一个静止的物体其距离传感器的读数却像心电图一样有微小的波动。这些“毛刺”就是噪声它们可能来自电源纹波、电磁干扰、传感器自身的热噪声或者电路板上的耦合。硬件上我们可以通过增加滤波电容、优化PCB布局、使用屏蔽线缆等手段来抑制噪声但这往往增加成本和设计复杂度。在很多对成本敏感、或者空间受限的项目中软件滤波算法就成了一个性价比极高的选择。它的核心思想很简单在微控制器MCU或处理器CPU这一侧通过一段精巧的代码对连续采样得到的一批原始数据进行数学处理从而得到一个更接近真实值的“干净”数据。今天我们就来深入聊聊10种在工程实践中非常经典的软件滤波算法并用最朴实无华的C语言手把手实现它们。无论你是正在做单片机课程设计的学生还是苦恼于传感器数据不稳的工程师这篇文章都能给你一套可以直接“抄作业”的解决方案。我们会从最简单的开始逐步深入到需要一些数学理解的算法并重点分析每种算法的适用场景、优缺点以及那些数据手册里不会告诉你的“坑”。2. 基础入门三种最直接的滤波方法在开始写代码之前我们必须建立一个统一的测试模型。假设我们有一个函数int Get_AD()用来获取一次ADC的原始采样值。我们的目标是编写一个滤波函数int Filter_XX()它内部会调用一次或多次Get_AD()经过计算后返回滤波后的值。2.1 限幅滤波法简单粗暴的“守门员”核心思想我认为相邻两次采样值之间的变化应该有一个合理的物理限度。如果本次采样值与前一次的差值超过了这个限度我就认为本次值是干扰造成的“坏点”直接丢弃并用上次值代替。算法逻辑记录上一次的有效滤波值LastValue。获取本次采样值NewValue。计算差值delta abs(NewValue - LastValue)。如果delta大于预设的最大允许偏差A则返回LastValue。否则认为本次值有效更新LastValue NewValue并返回NewValue。C语言实现#define A 10 // 最大允许偏差值需要根据实际信号变化率调整 int Filter_LimitAmplitude(int new_value) { static int last_value; // 静态变量用于保存上一次的有效值 int delta abs(new_value - last_value); if (delta A) { // 变化过大视为干扰返回上次值 return last_value; } else { // 变化在允许范围内更新并返回本次值 last_value new_value; return new_value; } }适用场景与心得适用适用于物理量本身变化缓慢的场景如温度、液位等。对于缓慢变化的信号突然的尖峰脉冲干扰很容易被识别并滤除。不适用对于快速变化的信号如振动、高速编码器合理的A值很难设定。设大了没效果设小了会把真实信号也滤掉导致信号失真。关键参数A这个值是算法的灵魂。设得太小系统会变得“迟钝”真实变化也被抑制设得太大则滤波效果微弱。通常需要观察一段时间内信号在平稳时的最大波动范围来设定。坑点它无法滤除在限幅范围内的噪声。如果噪声幅度小于A它会全部放行。所以它常作为其他滤波算法的“前置卫士”先干掉那些明显的野值。2.2 中位值滤波法投票决定“中间派”核心思想连续采样N次N为奇数把这N个样本值看成一个集合取这个集合的中位数作为本次滤波结果。中位数对偶然出现的脉冲性干扰无论是特别大还是特别小有非常好的抑制能力。算法逻辑连续采样N次例如3次5次存入数组。对数组进行排序升序或降序。直接取排序后数组最中间的那个值。C语言实现以N3为例// 简单的冒泡排序函数用于三个数的排序 void BubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int Filter_Median(int new_value) { static int value_buf[3]; // 静态数组存储最近3次采样值 static int count 0; int temp_buf[3]; // 循环填充缓冲区 value_buf[count] new_value; if (count 3) count 0; // 将静态缓冲区数据复制到临时数组进行排序避免破坏原始顺序虽然这里不影响 for(int i0; i3; i) temp_buf[i] value_buf[i]; // 排序 BubbleSort(temp_buf, 3); // 返回中位数索引为1 return temp_buf[1]; }适用场景与心得适用对脉冲干扰瞬间的高电平或低电平毛刺有奇效。比如开关电源切换、继电器动作时在模拟信号线上耦合的尖峰。不适用对周期性干扰如工频干扰束手无策对缓慢变化的随机噪声如热噪声滤波效果一般。采样次数NN越大抗脉冲干扰能力越强但系统延迟也越大需要攒够N个数据才能输出一次计算量排序也增加。N通常取3或5是一个效果和效率的平衡点。排序算法选择对于固定的、很小的N如3或5完全可以用一个简单的比较交换来实现效率比通用排序算法高得多。例如N3时中位数可以通过几次比较直接得到无需调用完整的排序函数。2.3 算术平均滤波法用“人多力量大”对抗随机噪声核心思想连续采样N次取它们的算术平均值。根据统计学原理对均值为零的随机噪声如高斯白噪声N次平均后其功率会降低到原来的1/N从而有效平滑噪声。算法逻辑连续采样N次累加求和。将累加和除以N得到平均值。C语言实现递推平均更高效#define N 10 // 平均窗口大小 int Filter_ArithmeticMean(int new_value) { static int sum 0; static int value_buf[N]; static int count 0; // 减去最旧的值加上最新的值实现滑动窗口 sum sum - value_buf[count] new_value; value_buf[count] new_value; // 更新缓冲区 if (count N) count 0; // 循环覆盖 // 返回平均值。注意使用整数除法会丢失小数部分。 // 对于需要更高精度的场合可以返回 (sum N/2) / N 实现四舍五入或使用浮点数。 return sum / N; }适用场景与心得适用主要用于抑制周期性、均值为零的随机噪声。对信号本身在某一数值附近上下波动的情况有很好的平滑效果。不适用对脉冲干扰的抑制能力很差。一个巨大的野值会显著拉高或拉低平均值影响后续多个输出。无法用于实时性要求高、信号快速变化的场合因为平均带来了滞后相位延迟。窗口大小NN是平滑度和响应速度的权衡。N越大曲线越平滑但对信号变化的响应越慢滞后越严重。需要根据信号频率和采样频率来选取。精度问题如代码注释所述整数除法会截断小数。在ADC位数较高如12位且N较大时累加和sum可能溢出需要使用long或long long类型。高精度场合建议使用浮点运算。3. 进阶平滑应对周期性波动与滞后补偿基础方法解决了部分问题但面对更复杂的噪声或对实时性有要求时我们就需要更巧妙的算法。3.1 滑动平均滤波法算术平均的“流动”版本算术平均滤波法每次计算都要用到完整的N个新数据延迟固定为N个采样周期。滑动平均Moving Average是其优化版本它维护一个长度为N的队列窗口每次新的采样值进来就挤掉一个最旧的值然后计算窗口内所有值的平均。这样每采样一次就能输出一个结果延迟固定为1个采样周期理论上。C语言实现循环队列法#define N 10 typedef struct { int buffer[N]; int index; // 指向下一个要写入的位置 int sum; int is_full; // 标志位指示缓冲区是否已第一次填满 } MovingAverageFilter; void MAF_Init(MovingAverageFilter *f) { for(int i0; iN; i) f-buffer[i] 0; f-index 0; f-sum 0; f-is_full 0; } int MAF_Update(MovingAverageFilter *f, int new_value) { // 减去即将被覆盖的旧值如果缓冲区已满 if (f-is_full) { f-sum - f-buffer[f-index]; } // 加上新值并更新缓冲区 f-sum new_value; f-buffer[f-index] new_value; // 更新索引 f-index; if (f-index N) { f-index 0; f-is_full 1; // 缓冲区第一次被填满后标志位置位 } // 计算平均值 int count f-is_full ? N : f-index; // 未填满时按实际数据个数算 return f-sum / count; }心得这个实现通过维护一个sum变量避免了每次计算平均值都要遍历整个数组求和极大地提高了效率是工程中的标准做法。is_full标志位用于处理缓冲区初始填充阶段的特殊情况。3.2 去极值平均滤波法结合中位值与平均的优点算术平均怕野值中位值对随机噪声平滑不够。去极值平均滤波法又称中位值平均滤波法将两者结合连续采样N次去掉其中的最大值和最小值然后对剩下的N-2个数据求算术平均值。算法逻辑采样N次存入数组。找出数组中的最大值和最小值。累加数组中所有值减去最大值和最小值。将结果除以 (N-2)。C语言实现N10为例#define N 10 int Filter_ExtremeRemovedMean(int new_value) { static int buf[N]; static int count 0; int sum 0; int max, min; buf[count] new_value; if (count N) count 0; // 假设缓冲区已满进行滤波计算。实际应用中可能需要等待缓冲区满。 // 这里简化处理每次调用都计算未满时结果不准确。 max min buf[0]; sum buf[0]; for (int i 1; i N; i) { sum buf[i]; if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; } sum sum - max - min; // 去掉一个最高分去掉一个最低分 return sum / (N - 2); // 求剩余数据的平均 }适用场景与心得这种方法融合了中位值抗脉冲干扰和平均平滑随机噪声的优点是实际项目中非常受欢迎的一种方法尤其适合数据竞赛评分、传感器数据预处理等场景。缺点是需要采样N次后才能输出一个值实时性较差且计算量比单纯的平均或中位值都要大需要遍历找最值。N通常取4到14之间的偶数这样去掉两个极值后剩余数据个数仍是整数。3.3 一阶滞后滤波法赋予历史数据“话语权”以上方法都是非递归的当前输出只与最近N次输入有关。一阶滞后滤波法或称一阶低通数字滤波器是递归的本次输出是上次输出与本次输入的加权和。它引入了“惯性”的概念。公式Y(n) α * X(n) (1-α) * Y(n-1)其中X(n)是本次采样值Y(n-1)是上次滤波输出值Y(n)是本次滤波输出值。α是滤波系数0 α 1。物理意义α越大表示越信任本次采样值系统响应快但平滑度差α越小表示越信任历史滤波值系统惯性大平滑效果好但响应迟钝。C语言实现// 使用浮点数提高精度 #define ALPHA 0.3f // 滤波系数根据需求调整 float Filter_FirstOrderLag(float new_value) { static float last_output 0.0f; // 静态变量保存上次输出 float output; output ALPHA * new_value (1 - ALPHA) * last_output; last_output output; // 更新状态 return output; } // 整数版本牺牲一些精度提高速度适用于无FPU的MCU #define ALPHA_NUM 3 // 对应 0.3 * 10 #define ALPHA_DEN 10 int Filter_FirstOrderLag_Int(int new_value) { static long last_output 0; // 使用long防止中间计算溢出 long output; // 公式变形Y(n) Y(n-1) α * (X(n) - Y(n-1)) // 这被称为“偏差加权递推平均”计算更稳定 output last_output (ALPHA_NUM * (new_value - last_output)) / ALPHA_DEN; last_output output; return (int)output; }适用场景与心得适用适用于信号变化缓慢、波动频繁的场合如电源电压监测、慢速温度采集。它能非常有效地抑制周期性高频噪声。优点计算量极小只涉及一次乘法和一次加法非常适合在资源紧张的MCU上运行。通过调整一个参数α就能控制滤波效果。缺点会产生相位滞后所有低通滤波器的通病α越小滞后越严重。对于快速变化的信号这种滞后可能导致控制失灵。参数整定α的选取至关重要。一个经验方法是α T / (T τ)其中T是采样周期τ是滤波器的时间常数你希望滤波器对信号变化的响应速度。通常通过实际测试观察滤波效果来调整。4. 高级策略动态调整与复合滤波前面的算法参数大多是固定的。但在实际系统中信号和噪声的特性可能变化。高级滤波策略的核心在于让算法自己适应环境。4.1 加权递推平均滤波法越新的数据越重要算术平均滤波法对所有历史数据一视同仁。但直观上离现在越近的数据对当前状态的反映应该越准确。加权递推平均就是给不同时刻的数据赋予不同的权重通常权重随时间指数衰减。公式Y(n) β * X(n) β*(1-β) * X(n-1) β*(1-β)^2 * X(n-2) ...其中0 β 1。这个公式可以递归实现Y(n) β * X(n) (1-β) * Y(n-1)。看这和一阶滞后滤波法的公式一模一样所以一阶滞后滤波法本质上就是一种指数加权的递推平均。权重系数随着时间向后呈指数衰减。因此其C语言实现与一阶滞后滤波法完全相同。关键在于理解其“加权”的内涵它并不是给一个固定窗口内的数据平均分配权重而是给全部历史数据分配了指数衰减的权重理论上窗口长度是无限的但越老的数据影响力越小。4.2 消抖滤波法为开关量设计的“防抖弹簧”严格来说这不是对模拟量的滤波而是针对开关量或数字信号的防抖动处理。机械开关如按键、限位开关在闭合或断开时由于触点弹跳会在短时间内产生一系列脉冲。消抖滤波法就是通过持续监测确认信号稳定一段时间后才认为状态真正改变。算法逻辑以检测低电平有效为例设置一个计数器count和一个阈值N。每次采样到低电平count加1采样到高电平count清零。当count累加到超过阈值N时才判定按键为“稳定按下”并输出有效信号同时将count锁定在N防止重复触发。只有当采样再次变为高电平并保持一段时间后才判定为“释放”并重置状态。C语言实现简化版#define DEBOUNCE_THRESHOLD 5 // 消抖阈值需要根据采样频率和抖动时间调整 typedef enum { BTN_IDLE, BTN_DEBOUNCING, BTN_PRESSED, BTN_RELEASING } ButtonState; ButtonState DebounceFilter(int raw_input) { // raw_input: 1高电平(未按下), 0低电平(按下) static ButtonState state BTN_IDLE; static int stable_count 0; switch (state) { case BTN_IDLE: if (raw_input 0) { // 首次检测到低电平 state BTN_DEBOUNCING; stable_count 1; } break; case BTN_DEBOUNCING: if (raw_input 0) { stable_count; if (stable_count DEBOUNCE_THRESHOLD) { state BTN_PRESSED; // 这里可以触发按键按下事件 return BTN_PRESSED; } } else { // 中间又变成高电平认为是抖动回到空闲状态 state BTN_IDLE; stable_count 0; } break; case BTN_PRESSED: if (raw_input 1) { // 首次检测到释放 state BTN_RELEASING; stable_count 1; } break; case BTN_RELEASING: if (raw_input 1) { stable_count; if (stable_count DEBOUNCE_THRESHOLD) { state BTN_IDLE; stable_count 0; // 这里可以触发按键释放事件 return BTN_IDLE; // 返回空闲状态表示已完成一次完整按下释放 } } else { // 释放过程中又按下认为是抖动回到按下状态 state BTN_PRESSED; stable_count 0; } break; } return state; // 返回当前状态 }心得这是一个简单的状态机实现比单纯的“连续N次低电平就认为按下”要健壮因为它能处理按下和释放过程中的抖动。DEBOUNCE_THRESHOLD需要根据MCU的采样周期和机械开关的典型抖动时间来设定通常10-20ms的稳定时间是个不错的起点。4.3 限幅消抖滤波法模拟信号的“双重保险”这是将限幅滤波和消抖滤波的思想结合用于缓慢变化的模拟信号。它先对信号进行限幅消除大幅度的脉冲干扰然后再对小幅度的波动进行“消抖”即连续判断多次变化趋势才确认信号真的改变了方向。算法逻辑一种常见实现设置限幅值A、消抖计数器阈值N、消抖计数器count、当前趋势trend上升、下降、平稳。获取新值new_val与上次最终输出值last_output做限幅比较。若超差则直接忽略本次采样或使用限幅逻辑。若未超差判断new_val与last_output的差值。如果差值 一个正的小阈值Δ则count倾向于上升。如果差值 一个负的小阈值-Δ则count--倾向于下降。如果在[-Δ, Δ]之间count向0方向衰减。当count达到N判定为“确认上升”更新last_output并重置count。当count达到-N判定为“确认下降”更新last_output并重置count。适用场景适用于那些变化非常缓慢但又希望输出值阶梯式平稳变化而不是每个采样点都微动的场景。比如用ADC读取一个多圈电位器的位置由于噪声和ADC本身误差即使电位器不动读数也有±2个字的跳动。使用限幅消抖滤波后只有当读数持续向一个方向变化超过一定步数时输出值才会“跳”到新的水平从而避免了输出的频繁微小波动使显示或控制更稳定。5. 自适应与智能滤波让滤波器“活”起来前面的算法参数都是静态的。自适应滤波的核心思想是根据输入信号的特性动态调整滤波器的参数或行为以达到最优的滤波效果。5.1 自适应加权递推平均滤波法在一阶滞后滤波中系数α是固定的。我们可以让它动起来当系统认为本次采样值可能不可靠例如与历史值差异巨大时就减小α更相信历史值滤波力度加强当差异在正常范围内时就使用较大的α快速跟踪信号变化。一种简单的实现思路float Filter_AdaptiveLag(float new_value) { static float last_output 0.0f; float delta fabs(new_value - last_output); float alpha; float output; // 动态计算alpha差异越大alpha越小越不相信新值 // 这里的公式需要根据实际信号范围调整例如 if (delta 50.0f) alpha 0.1f; else if (delta 20.0f) alpha 0.2f; else alpha 0.5f; output alpha * new_value (1 - alpha) * last_output; last_output output; return output; }当然更高级的做法可以是让alpha与delta呈连续的函数关系如反比关系并设置上下限。5.2 复合滤波没有银弹那就组合拳在实际工程中单一滤波算法往往难以应对所有情况。最可靠的做法是多种滤波算法级联使用形成复合滤波器。一个经典的复合滤波流程原始采样值 | V [限幅滤波] -- 干掉明显的野值脉冲干扰 | V [中位值滤波] -- 进一步抑制残留的脉冲干扰 | V [算术平均或一阶滞后滤波] -- 平滑随机噪声 | V 滤波输出值C语言实现示例int Get_AD(void); // 假设的ADC读取函数 int Filter_Cascade(void) { int raw_value Get_AD(); int value1, value2, final_value; int buf[5]; // 用于中位值滤波的缓冲区 static int buf_index 0; static int last_final_value 0; // 第一级限幅滤波 (使用上次的最终输出作为参考) if (abs(raw_value - last_final_value) 15) { // 限幅阈值 value1 last_final_value; // 超差用旧值 } else { value1 raw_value; } // 第二级中位值滤波 (N5) buf[buf_index] value1; buf_index (buf_index 1) % 5; // ... (这里需要实现一个取5个数中位数的函数代码略) value2 Get_Median(buf, 5); // 第三级一阶滞后滤波 final_value (int)(0.3f * value2 0.7f * last_final_value); // alpha0.3 last_final_value final_value; return final_value; }心得复合滤波是工程实践的精华。它可能牺牲一些效率但极大地提高了系统的鲁棒性。设计复合滤波器时通常把抗脉冲干扰的算法限幅、中位值放在前面把平滑随机噪声的算法平均、滞后放在后面。每一级的参数需要单独调试并观察整体效果。6. 滤波算法的选择与实践指南面对这么多算法到底该怎么选这里提供一个简单的决策思路分析你的信号和噪声信号变化快慢快速变化如电机转速慎用平均类滞后大可用限幅或一阶滞后α调大。噪声主要类型脉冲干扰尖峰用中位值、限幅。周期性随机噪声用平均、一阶滞后。系统资源MCU性能弱、RAM小优先选择计算量小、不需大数组的算法如一阶滞后、限幅。实时性要求要求输出延迟小避免需要攒N个数据的算法如中位值平均选择递归算法如一阶滞后或滑动平均。一个实用的选择流程第一步必加限幅滤波。作为数据安全的“底线”成本极低防止后续处理被野值带偏。第二步如果存在明显脉冲干扰加一级中位值滤波N3或5。第三步对于高频随机噪声使用一阶滞后滤波或滑动平均滤波。追求极致平滑用后者追求快速响应和低计算量用前者。第四步如果效果仍不理想考虑复合滤波或尝试去极值平均滤波。第五步对于变化极其缓慢、希望输出稳定的显示值可以考虑限幅消抖滤波。参数调试是灵魂不要想当然所有阈值限幅值A、平均窗口N、滤波系数α、消抖计数都必须基于实际信号测试来设定。如何测试让系统在典型工况下运行通过串口、显示屏或调试器连续打印出原始采样值和各级滤波后的值。用PC工具如串口绘图助手、Excel、Python的Matplotlib绘制成曲线图直观对比。观察要点滤波效果噪声是否明显减小脉冲是否被抑制信号跟踪当信号发生真实变化时滤波输出是否能及时跟上滞后是否可接受过冲与振铃信号突变后滤波输出是否平稳过渡有没有剧烈的上下摆动关于C语言实现的几个技巧整数与浮点数在无FPU浮点运算单元的MCU上尽量使用整数运算。对于一阶滞后滤波可以用α K / M的形式用整数乘法和移位代替浮点除法。防止溢出计算累加和如滑动平均的sum时使用足够宽的数据类型long,long long。初始化静态变量如last_value,sum在第一次使用前是未定义的。务必在系统上电初始化时或滤波器函数首次调用前将其设置为一个合理的初始值例如第一次采样值。重入与线程安全如果滤波器函数可能在多任务或中断环境中被调用且使用了静态变量则需要考虑临界区保护如关中断、使用信号量防止数据错乱。最后记住软件滤波的黄金法则滤波算法只能改善信号质量无法创造信息。如果硬件层面的噪声太大再好的软件算法也是徒劳。软件滤波是硬件设计的补充而非替代。在电路设计、PCB布局、电源处理上多下功夫从源头降低噪声才能让你的系统运行在最稳健的状态。