
1. 项目概述为什么ADC采样后必须滤波做嵌入式或者硬件开发的朋友对ADC模数转换器肯定不陌生。无论是测量温度、压力还是采集音频、心电信号我们都需要把现实世界连续变化的模拟量转换成单片机或处理器能理解的数字量。但事情往往没那么简单——你兴冲冲地接好传感器写好采样代码读出来的数据却像心电图一样上蹿下跳根本没法用。这时候数字滤波就成了你的“救命稻草”。这个项目标题“常用ADC采样数字滤波算法最全汇总”直指嵌入式信号处理中的一个核心痛点如何从充满噪声的ADC原始数据中提取出稳定、可信的真实信号。我干了十多年嵌入式从8位单片机到32位ARM Cortex-M系列几乎每个涉及数据采集的项目都绕不开这一步。网上资料虽多但要么过于理论满篇公式看不懂要么过于零散只讲一种方法。这次我就结合自己踩过的无数个坑把那些真正在工程实践中常用、好用的数字滤波算法从原理到代码从选型到调参给你一次性讲透、捋顺。无论你是正在做毕设的学生还是遇到产品稳定性问题的工程师这篇文章都能给你一套可以直接“抄作业”的解决方案。2. 核心需求解析ADC数据为什么“脏”在深入算法之前我们必须先搞清楚敌人是谁。ADC采样数据不干净噪声主要来自以下几个方面理解它们有助于我们选择合适的滤波武器。2.1 噪声的主要来源1. 环境噪声与传导干扰这是最普遍的噪声。你的电路板可能处在电机、继电器、开关电源旁边这些设备启停时会产生强烈的电磁干扰EMI通过空间辐射或电源线传导到你的采样电路。比如我用STM32采集一个直流电压附近有个风扇一开ADC值就能跳几十个LSB最低有效位。2. 传感器自身噪声很多传感器如热敏电阻、麦克风、应变片其输出信号本身就带有噪声。例如热电偶测量高温炉温度时其微弱的毫伏级信号极易被淹没。3. PCB布局与接地不良这是硬件工程师的“必修课”但软件工程师也常背锅。如果模拟地AGND和数字地DGND处理不当数字电路高速开关产生的噪声会直接串扰到敏感的模拟采样通道。我曾遇到一个案例ADC采样值随LCD刷新周期性地波动最后发现是地线走线不合理。4. ADC自身量化噪声与非线性误差ADC不是理想的它有分辨率限制如12位ADC量化台阶是Vref/4096这个过程中就会引入量化噪声。此外还有积分非线性INL和微分非线性DNL误差这些属于器件固有特性。5. 电源纹波给ADC和基准源供电的LDO或DC-DC若性能不佳其输出上的微小波动会直接叠加到采样结果上。实操心得遇到采样数据跳动第一步不要急着写复杂的滤波算法。先用示波器看看ADC输入引脚的实际波形用万用表量量电源和基准电压是否稳定。很多时候问题出在硬件上软件滤波只能缓解无法根治。一个稳定的硬件基础能让后续的软件滤波事半功倍。2.2 滤波的核心目标面对这些噪声数字滤波的目标非常明确平滑抑制随机高频噪声让数据曲线变得平滑便于观察趋势。保真在平滑的同时尽可能保留真实信号的特征如突变、频率成分。这对于心率、振动等信号至关重要。实时性在嵌入式系统中滤波算法必须在下一个采样点到来前完成计算不能占用过多CPU时间和内存。简单可靠算法不能太复杂要便于理解、调试和在不同平台间移植。3. 经典滤波算法深度剖析与实现接下来我们进入正题。我将这些算法分为“非递归无记忆”和“递归有记忆”两大类并按照从简到繁、从通用到专用的顺序讲解。3.1 非递归滤波器滑动平均与它的兄弟们这类滤波器的输出只与当前及过去的有限个输入有关不与过去的输出有关因此永远是稳定的。3.1.1 简单移动平均滤波这是入门必学也是使用最广泛的算法之一。其思想是取最近N个采样值的算术平均值作为当前输出。公式y[n] (x[n] x[n-1] ... x[n-N1]) / NC语言实现固定窗口#define FILTER_WIN_SIZE 10 // 滤波窗口大小 int Moving_Average_Filter(int new_sample) { static int buffer[FILTER_WIN_SIZE] {0}; static int index 0; static long sum 0; // 减去最旧的值加上最新的值 sum sum - buffer[index] new_sample; buffer[index] new_sample; // 更新缓冲区 index (index 1) % FILTER_WIN_SIZE; // 环形缓冲区索引 return (int)(sum / FILTER_WIN_SIZE); // 返回平均值 }优点算法极其简单计算量小只有加减法。对周期性干扰有良好的抑制效果。平滑度高窗口越大越平滑。缺点与注意事项滞后性相位延迟这是最大缺点。窗口越大对信号突变的反应越迟钝。比如一个阶跃信号需要N个采样周期后输出才能达到新值的63.2%。在控制系统中这可能引发振荡。内存占用需要开辟一个大小为N的数组。窗口选择N不是越大越好。N太大滞后严重N太小滤波效果差。通常根据采样频率和噪声频率估算。一个经验是N 采样频率 / (噪声主要频率 * 2)。例如采样率1kHz噪声是50Hz工频N可选10。适用于变化缓慢的信号如温度、压力、电池电压。3.1.2 加权移动平均滤波简单平均认为窗口内所有数据同等重要但有时最新的数据更能反映当前状态。加权平均赋予不同位置的数据不同的权重通常越新的数据权重越大。公式y[n] (w0*x[n] w1*x[n-1] ... w_{N-1}*x[n-N1]) / W_sum其中w0 w1 ... w_{N-1}W_sum是权重之和。实现使用线性递减权重#define FILTER_WIN_SIZE 5 const int weight[FILTER_WIN_SIZE] {5, 4, 3, 2, 1}; // 权重系数可调 int Weighted_Moving_Average_Filter(int new_sample) { static int buffer[FILTER_WIN_SIZE] {0}; static int index 0; long sum 0; int weight_sum 0; buffer[index] new_sample; // 计算加权和 for(int i0; iFILTER_WIN_SIZE; i) { int buf_idx (index FILTER_WIN_SIZE - i) % FILTER_WIN_SIZE; // 从新到旧遍历 sum buffer[buf_idx] * weight[i]; weight_sum weight[i]; } index (index 1) % FILTER_WIN_SIZE; return (int)(sum / weight_sum); }优点相比简单平均对信号变化的响应更快滞后小。缺点需要精心设计权重系数计算量稍大。适用于既需要一定平滑度又对响应速度有要求的场合如低速运动控制中的位置反馈。3.1.3 去极值平均滤波在工业现场传感器或线路偶尔会受到强脉冲干扰如开关电弧产生一两个明显偏离正常范围的“野值”。简单平均法会被这些野值严重拉偏。去极值平均滤波先去掉最大值和最小值再对剩余数据求平均。实现#define FILTER_WIN_SIZE 10 int Remove_Extremes_Average_Filter(int new_sample) { static int buffer[FILTER_WIN_SIZE] {0}; static int index 0; int i, sum 0; int max, min, max_idx, min_idx; buffer[index] new_sample; index (index 1) % FILTER_WIN_SIZE; // 假设缓冲区已满或已初始化 max min buffer[0]; max_idx min_idx 0; sum buffer[0]; // 找出最大值、最小值并求和 for(i1; iFILTER_WIN_SIZE; i) { sum buffer[i]; if(buffer[i] max) { max buffer[i]; max_idx i; } if(buffer[i] min) { min buffer[i]; min_idx i; } } // 减去最大值和最小值 sum sum - max - min; // 对剩余 N-2 个值求平均 return sum / (FILTER_WIN_SIZE - 2); }避坑技巧这个方法对偶然的脉冲干扰非常有效但窗口N不能太小一般建议N5。它的计算量比简单平均大因为每次都要遍历找最值。在资源紧张的MCU上如果采样率很高需要评估CPU负载。3.2 递归滤波器一阶低通与它的威力递归滤波器的输出不仅与输入有关还与过去的输出有关因此具有“记忆”功能能用更少的计算量实现更好的滤波效果但设计不当会导致不稳定。3.2.1 一阶低通数字滤波器这是递归滤波器的代表也是工程中的“万金油”。它通过一个简单的差分方程模拟了RC低通电路的行为。公式最重要y[n] α * x[n] (1 - α) * y[n-1]其中y[n]是当前滤波输出。x[n]是当前ADC采样输入。y[n-1]是上一次滤波输出。α是滤波系数0 α 1。α越大截止频率越高响应越快但平滑度越差α越小截止频率越低滤波效果越好但滞后越严重。它与硬件RC电路的关系 假设采样周期为T硬件RC电路的时间常数为τ R*C其对应的数字滤波系数α满足α T / (τ T)。通常我们更关注截止频率f_c-3dB点τ 1 / (2π * f_c)。所以α T / (1/(2πf_c) T) 2πf_cT / (1 2πf_cT)。当2πf_cT 1时可以近似为α ≈ 2πf_cT。这个公式是调试的钥匙。C语言实现浮点与定点// 浮点版本直观但慢 float LowPass_Filter_Float(float new_sample) { static float output_last 0; float alpha 0.1f; // 滤波系数根据需求调整 float output; output alpha * new_sample (1 - alpha) * output_last; output_last output; return output; } // 定点版本快无浮点单元MCU必备 #define ALPHA_Q15 3277 // 对应 0.1 计算方法0.1 * 32768 3276.8 ≈ 3277 (Q15格式) int LowPass_Filter_Fixed(int new_sample) { static long output_last 0; // 用long防止中间计算溢出 long output; // 公式: y α*x (1-α)*y_old // 等价于: y y_old α*(x - y_old) output output_last ((ALPHA_Q15 * (new_sample - output_last)) 15); output_last output; return (int)output; }优点计算量极小每次滤波只需一次乘法、一次加法和一次减法或两次乘法一次加法。内存占用极小只需保存上一个输出值。平滑效果好对高频噪声抑制明显。可调性强通过一个参数α就能灵活调节滤波特性。缺点与调参经验参数α选择是关键。我常用的调试步骤确定你的信号有效带宽f_signal和采样频率f_sample。设定目标截止频率f_c应略高于f_signal以保留信号远低于采样频率的一半奈奎斯特频率通常f_c f_sample / 10。计算α ≈ 2π * f_c / f_sample。 例如采样率f_sample100Hz信号是缓慢变化的温度f_signal1Hz想滤除50Hz工频干扰。取f_c5Hz则α ≈ 2*3.14*5/100 0.314。可以从0.3开始尝试。初始值问题上电时y[-1]未知如果设为0可能导致滤波输出需要很长时间才能收敛到真实值。解决办法前几个采样周期直接输出原始值或者用第一个采样值初始化y[-1]。适用于绝大多数需要平滑ADC数据的场景尤其是单片机资源紧张时。是首推的通用滤波算法。3.2.2 限幅滤波法严格来说这不是传统意义上的滤波而是一种“数据清洗”手段。它基于合理假设物理量在短时间内不可能发生剧烈跳变。算法根据经验设定一个最大允许变化量Δmax。当|本次采样值 - 上次有效值| Δmax时则认为本次采样是干扰丢弃并用上次有效值代替否则认为采样有效。实现int Limiting_Filter(int new_sample) { static int last_valid_value 0; const int DELTA_MAX 50; // 最大允许变化量根据具体信号设定 if(abs(new_sample - last_valid_value) DELTA_MAX) { // 变化超限认为是干扰返回上次有效值 return last_valid_value; } else { // 变化在合理范围内更新有效值并返回 last_valid_value new_sample; return new_sample; } }优点能有效滤除因随机干扰或传感器故障引起的突发性尖峰脉冲。缺点无法滤除高频但幅度小的噪声。Δmax的设置很关键设大了没效果设小了会滤掉真实的快速变化信号。适用于适合变化缓慢的物理量如液位、体重常与其他滤波法如一阶低通结合使用作为第一道“防线”。4. 进阶与复合滤波策略当单一算法无法满足需求时就需要组合拳。4.1 中位值平均滤波防脉冲干扰平均滤波法这是“中值滤波”和“平均滤波”的结合堪称对抗脉冲干扰的“黄金组合”。先采样N个数据去掉其中的最大值和最小值中值滤波思想再对剩余的N-2个数据求算术平均。算法步骤连续采样N个数据N为奇数通常3,5,7。对这N个数据按大小排序。去掉最大值和最小值。计算剩余N-2个数据的算术平均值。实现以N5为例int Median_Mean_Filter() { int i, j, temp; int value_buf[5]; int sum 0; // 1. 连续采样5次 (在实际中这5次采样应有微小的时间间隔) for(i0; i5; i) { value_buf[i] ADC_Read(); // 假设的ADC读取函数 // 可以在这里加微小延时避免采样到完全相同的值 } // 2. 冒泡排序数据量小效率可接受 for(i0; i4; i) { for(j0; j4-i; j) { if(value_buf[j] value_buf[j1]) { temp value_buf[j]; value_buf[j] value_buf[j1]; value_buf[j1] temp; } } } // 3. 去掉首尾最小和最大对中间3个值求平均 for(i1; i3; i) { sum value_buf[i]; } return sum/3; }优点融合了中值滤波抗脉冲干扰和平均滤波平滑随机噪声的优点。缺点采样和计算耗时较长排序操作不适合高速采样。N值大会增加延迟。适用于慢变信号但环境干扰复杂既有随机噪声又有偶发脉冲的场合如工业现场仪表。4.2 一阶低通限幅滤波这是我个人在多数项目中的标配组合。先用限幅滤波滤掉不可能的突变硬件故障或强干扰再用一阶低通进行平滑。实现int LowPass_with_Limiting(int new_sample) { static int last_lp_output 0; const int DELTA_LIMIT 100; // 限幅阈值 float alpha 0.05f; float output; // 第一级限幅 if(abs(new_sample - last_lp_output) DELTA_LIMIT) { // 变化过大疑似干扰输入值沿用上次输出相当于滤除此次突变 new_sample last_lp_output; } // 第二级一阶低通 output alpha * new_sample (1 - alpha) * last_lp_output; last_lp_output (int)output; // 更新静态变量用于下次计算 return (int)output; }优点兼具抗脉冲和平滑能力且一阶低通的滞后性被限幅环节部分补偿因为异常跳变被抑制了整体响应比纯一阶低通更快、更稳。调试顺序先单独调好一阶低通的α确定基本平滑度。再根据信号正常变化速率设置DELTA_LIMIT通常设为信号在数个采样周期内最大可能变化量的1.5-2倍。5. 算法选型指南与实战场景分析面对这么多算法到底该怎么选我总结了一个决策流程和场景对照表。5.1 选型决策流程评估信号特性信号变化快慢主要噪声类型高频随机、工频、脉冲评估系统资源MCU主频、RAM、Flash大小采样频率要求明确性能要求实时性要求延迟允许多少平滑度要求计算精度要求快速原型验证在PC上用Python或MATLAB或者直接在开发板上用不同参数测试观察滤波效果。5.2 场景-算法对照表应用场景信号与噪声特点推荐算法参数设置建议备注温度/压力监测变化极慢噪声随机一阶低通滤波α取0.01~0.1采样周期1s以上资源占用最少效果足够电池电压检测缓变可能有毛刺限幅一阶低通Δmax0.1Vα0.1~0.3防止插拔负载时的电压骤变被误平滑直流电机转速中速变化有抖动加权移动平均窗口N5~10权重线性递减比简单平均响应快比低通更易理解按键ADC采样需要稳定值防抖动去极值平均滤波N5~10采样间隔5-10ms有效消除触点抖动造成的异常值声音传感器信号频带较宽滑动平均窗口小N3~5采样率8kHz仅做轻微平滑保持声音细节振动传感器需要分析频率成分慎用滤波-滤波可能滤掉特征频率优先在硬件前端处理存在强电磁干扰偶发大幅脉冲中位值平均滤波N5或7对抗脉冲干扰能力最强5.3 在STM32 HAL库中的集成示例以STM32CubeMX和HAL库为例展示如何将一阶低通滤波集成到ADC DMA循环采样中。// 在ADC DMA传输完成中断回调函数中处理数据 #define ADC_BUFFER_SIZE 100 #define ALPHA 0.05f uint16_t adc_dma_buffer[ADC_BUFFER_SIZE]; float filtered_value 0.0f; uint8_t filter_init_flag 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 假设DMA将多个通道的数据循环存入adc_dma_buffer uint32_t raw_adc_value adc_dma_buffer[0]; // 取第一个通道的值 // 首次初始化滤波器状态 if(filter_init_flag 0) { filtered_value (float)raw_adc_value; filter_init_flag 1; } else { // 应用一阶低通滤波 filtered_value ALPHA * (float)raw_adc_value (1.0f - ALPHA) * filtered_value; } // 此时 filtered_value 就是滤波后的结果可用于显示、控制等 // ... }实操心得在中断服务函数中避免进行浮点运算尤其是对于高性能MCU或频繁中断的情况。上面的浮点示例是为了清晰。在实际产品中应使用定点数运算Q格式或者将滤波计算移到主循环中中断只负责填充数据缓冲区。6. 常见问题、调试技巧与误区避坑即使选对了算法调试不当也会事倍功半。这里分享一些血泪教训。6.1 滤波器参数整定“黑盒”法很多时候理论计算出的α或N并不理想需要现场调试。我常用的“黑盒”调试法搭建数据通路让设备输出原始ADC值和滤波后值通过串口打印或SWO。制造典型场景输入一个阶跃信号如快速改变传感器输入或者让设备处于典型噪声环境。观察响应曲线在串口助手上观察波形。如果滤波后曲线噪声仍大说明滤波不够强增大窗口N或减小α。如果信号变化时滤波输出反应迟钝滞后圆角大说明滤波过强减小窗口N或增大α。如果出现相位滞后导致系统振荡在控制回路中这是严重问题。需要大幅减小滞后增大α减小N或者考虑使用相位补偿更复杂的滤波器如贝塞尔滤波器但这在简单MCU上不现实通常优先调整控制器的PID参数来容忍滞后。6.2 资源与性能的权衡8位/低端32位MCU如STM32F0优先使用一阶低通定点和限幅滤波。避免浮点、避免大数组、避免排序。中高端MCU如STM32F4可以尝试加权平均、去极值平均。如果有多通道高速采样需求依然要精打细算。采样率与滤波时机如果采样率很高如10kHz但你需要的数据更新率很低如100Hz。不要在每个采样点都滤波可以每100个点做一次抽取并滤波或者使用滑动平均每采一个新点就更新一次输出效率更高。6.3 典型误区与避坑指南误区一滤波越强越好。 过度滤波会让信号失真。比如一个快速上升的边缘会被滤成一个缓慢上升的斜坡丢失了关键的变化率信息。滤波的目标是去除噪声保留信号特征。误区二用软件滤波弥补硬件缺陷。 再次强调软件滤波是“最后一道防线”。电源纹波、地线干扰、传感器噪声这些必须在硬件层面解决。良好的PCB布局、适当的滤波电容、磁珠、模拟地数字地单点连接比任何复杂的软件算法都有效。误区三忽视滤波器的初始状态。 如前所述一阶低通滤波器需要一个初始的y[-1]。如果系统刚上电时传感器还未稳定用第一个采样值初始化可能会带入一个错误初值。稳妥的做法是在系统初始化后的前几百毫秒直接输出原始采样值不进行滤波待系统稳定后再开启滤波。误区四在控制环路中盲目滤波。 在电机速度、位置闭环控制中反馈信号的延迟相位滞后会降低系统稳定性甚至引发振荡。此时需要仔细评估滤波引入的延迟。有时稍微“脏”一点但及时的反馈比平滑但滞后的反馈更好。可能需要为控制环单独配置一个延迟更小的滤波器。调试滤波就像给照片加滤镜目的是让主体更清晰而不是把细节都抹掉。最好的滤波效果往往是算法、参数和硬件设计三者平衡的艺术。从简单的一阶低通开始理解其特性再根据实际遇到的问题逐步升级你的滤波策略这才是稳健的工程实践路径。