ADC采样数字滤波算法全解析:从限幅到卡尔曼滤波实战指南
1. 从“毛刺”到“真值”:为什么ADC采样离不开数字滤波
做嵌入式或者硬件开发的朋友,对ADC(模数转换器)采样肯定不陌生。无论是读取一个电位器的电压,还是监测电池电量,甚至是采集传感器(比如温度、压力、光照)的信号,最终都要通过ADC把连续的模拟电压转换成单片机或处理器能处理的离散数字值。但只要你实际动手做过,十有八九会遇到一个让人头疼的问题:采回来的数据怎么老是“跳”?明明电压很稳定,为什么ADC值却在某个范围内上下波动,像心电图一样?这些“毛刺”和“噪声”就是数字滤波算法要解决的核心问题。
我刚开始接触STM32的ADC时,也天真地以为HAL_ADC_GetValue()拿到的就是最终答案。结果一个简单的3.3V基准电压,采样值能在2047到2053之间来回蹦跶,换算成电压就是3.298V到3.302V。对于精度要求不高的场合,比如判断个按键,或许能忍。但一旦涉及到精密测量、闭环控制(比如PID温控)、或者需要做数据分析时,这种噪声就是致命的。它会让你的控制系统产生不必要的振荡,让你的数据分析图表充满锯齿,严重时甚至会导致逻辑误判。
所以,数字滤波算法的本质,不是“创造”数据,而是“还原”数据。它基于一个基本假设:真实的物理信号通常是连续且变化相对缓慢的(高频噪声除外),而噪声通常是随机的、高频的。通过各种数学或逻辑方法,我们试图从夹杂着噪声的采样序列中,提取出最接近真实信号的那个“估计值”。这就像在一间嘈杂的房间里听一个人说话,你需要集中注意力(滤波),才能听清他真正在说什么(信号)。
网上关于滤波算法的资料很多,但往往要么过于理论,满篇公式;要么过于零散,只给个代码片段。这篇文章,我想结合我这些年踩过的坑和项目经验,把最常用、最实用的ADC数字滤波算法做个系统性的梳理和对比。我不会只扔给你一个函数,我会重点讲清楚每种方法的适用场景、核心思想、参数怎么调、以及实际用的时候最容易栽在哪儿。无论你是刚入门的新手,还是想优化现有方案的老手,这篇文章都能给你提供直接的参考。
2. 基础入门:从“懒人”滤波到“滑动”窗口
在深入那些“高大上”的算法之前,我们先看几个最简单、几乎零计算开销的方法。别小看它们,在资源极其受限的单片机(比如8位的51、AVR)或者对实时性要求极高的中断服务程序里,这些方法往往是首选。
2.1 限幅滤波法(又称程序判断滤波法)
这可能是最“粗暴”也最直观的滤波方法。它的核心思想就一句话:我不相信一次剧烈的变化。
算法逻辑:
- 根据信号特性,设置一个最大允许变化量
Δ(Delta)。 - 本次采样值记为
new_val,上次有效的滤波结果记为last_val。 - 如果
|new_val - last_val| <= Δ,则认为本次采样是合理的,接受new_val作为本次结果。 - 如果
|new_val - last_val| > Δ,则认为本次采样可能是噪声毛刺,丢弃new_val,仍将last_val作为本次结果输出。
C语言示例:
#define DELTA 10 // 假设ADC值最大允许变化量为10 int last_filtered_value = 0; int limit_filter(int new_sample) { if (abs(new_sample - last_filtered_value) <= DELTA) { last_filtered_value = new_sample; } // 如果变化超限,last_filtered_value 保持不变 return last_filtered_value; }适用场景与坑点:
- 适用:适用于物理量本身变化缓慢的场景,比如温度、液位。对于缓慢变化的信号,突然出现一个大跳变,大概率是噪声。
- 关键参数
Δ:这个值的设置是门艺术。设大了,滤波效果差,该滤的没滤掉;设小了,系统反应迟钝,真实的快速变化(比如电机启动电流)会被当成噪声滤掉,导致数据严重滞后。我的经验是:先观察信号在稳定时的最大波动范围,Δ可以设为这个波动范围的2-3倍。最好能在系统中有个配置接口,方便现场调试。 - 致命缺点:无法处理连续噪声。如果噪声不是单个毛刺,而是一连串的、但每次跳变都不超过
Δ的干扰,这个算法就完全失效了,输出会跟着噪声慢慢“漂移”。而且,它完全无法抑制小幅度的随机波动。
2.2 中位值滤波法
中位值滤波的哲学是:在一堆数据里,那个处在中间位置的值,最能代表这组数据的中心趋势,受极端值影响最小。这就像评委打分,去掉一个最高分,去掉一个最低分,能有效避免个别评委的极端偏好影响最终成绩。
算法逻辑:
- 连续采样
N次(N为奇数,通常取3, 5, 7),得到一个采样数组。 - 将这
N个数据从小到大(或从大到小)进行排序。 - 直接取排序后的中位数(第
(N+1)/2个值)作为本次滤波输出值。
C语言示例(N=3):
int median_filter(int a, int b, int c) { int arr[3] = {a, b, c}; // 简单的三数排序取中值 if (arr[0] > arr[1]) swap(&arr[0], &arr[1]); if (arr[0] > arr[2]) swap(&arr[0], &arr[2]); if (arr[1] > arr[2]) swap(&arr[1], &arr[2]); return arr[1]; // 中位数 } // 在ADC中断中可能这样调用 int adc_buffer[3]; int buffer_index = 0; int filtered_result; void ADC_IRQHandler() { adc_buffer[buffer_index] = ADC_DR; // 读取ADC值 buffer_index = (buffer_index + 1) % 3; if (buffer_index == 0) { // 每采满3个点计算一次 filtered_result = median_filter(adc_buffer[0], adc_buffer[1], adc_buffer[2]); } }适用场景与坑点:
- 适用:对脉冲性干扰(瞬间的尖峰噪声)有非常好的抑制效果。比如工频干扰、开关电源引起的毛刺。因为无论尖峰是高是低,排序后都会被甩到两边,中位数稳如泰山。
- 缺点一:速度与资源。排序操作,尤其是当N较大时(比如7或9),比较耗时。在低速MCU上,频繁进行排序可能影响整体程序节奏。需要权衡滤波效果和计算开销。
- 缺点二:对周期性干扰无能为力。如果噪声是规律的、周期性的,那么中值滤波效果有限。它更擅长处理“偶发性”的异常点。
- N的选择:N越大,抗脉冲干扰能力越强,但延迟也越大(需要采够N个点才能输出一次),且排序更慢。通常N=3或5在工程上最常用。一个进阶技巧:对于变化稍快的信号,可以采用“滑动窗口中值滤波”,即维护一个长度为N的队列,每次新采样进来,就剔除一个最老的,然后对窗口内数据取中值,这样输出速率就和采样率一致了。
2.3 算术平均滤波法
这是很多人第一个想到的方法:“多点平均,噪声抵消”。其依据是随机噪声的统计特性——通常认为噪声是均值为0的随机信号,多次平均后正负噪声会相互抵消。
算法逻辑:
- 连续采样
N个数据。 - 将这
N个数据求和。 - 将和除以
N,得到的平均值即为滤波输出。
C语言示例:
#define N 10 int adc_sum = 0; int sample_count = 0; int filtered_value = 0; void process_adc_sample(int new_sample) { adc_sum += new_sample; sample_count++; if (sample_count >= N) { filtered_value = adc_sum / N; // 注意整数除法截断问题 adc_sum = 0; sample_count = 0; // 此时可以使用 filtered_value } }适用场景与坑点:
- 适用:对抑制一般性随机噪声(如白噪声)效果显著。信号本身在
N次采样时间内变化不大时,效果最好。常用于直流信号或低频信号的测量。 - 缺点一:严重滞后。这是平均滤波法的阿喀琉斯之踵。
N越大,平滑效果越好,但系统输出对输入变化的响应也越慢。输入一个阶跃信号,输出要等N个采样周期后才能缓慢达到新值。绝对不适用于跟踪快速变化的信号。 - 缺点二:对脉冲干扰敏感。一个巨大的脉冲干扰(比如一个采样点被严重污染)会显著拉高或拉低整个平均值,导致输出出现一个“毛刺”。为了解决这个问题,常常会结合中值滤波,先对数据进行“去野值”处理,再进行平均,这就是“中位值平均滤波法”(又称防脉冲干扰平均滤波法)。
- N的选择与数据类型:
N通常取2的整数次幂(如4, 8, 16, 32),这样除法可以用右移操作代替,在无硬件除法器的MCU上能极大提升速度。另外,adc_sum的变量类型要注意,防止累加溢出。例如,12位ADC值范围0-4095,N=32时,累加和最大可达131040,需要用uint32_t来存储。
3. 进阶策略:在“平滑”与“快速”之间寻找平衡
基础方法各有各的“偏科”。我们既想要平均滤波的平滑,又想要限幅滤波的快速响应,还想要中值滤波的抗脉冲能力。于是,一些更聪明的复合或迭代策略被设计出来。
3.1 滑动平均滤波法
算术平均滤波的“批量处理”模式导致了固有延迟。滑动平均(Moving Average)将其改造成了“流水线”模式,每一次采样都能立即得到一个输出,大大提高了实时性。
算法逻辑:
- 维护一个长度为
N的队列(环形缓冲区),用于存储最近N次采样值。 - 初始化时,用前
N次采样值填满队列,并计算其总和sum。 - 每次得到新采样值
new_val时: a. 从sum中减去队列中最老的那个值oldest_val。 b. 将new_val加入队列尾部,并更新sum = sum + new_val。 c. 滤波输出output = sum / N。
C语言示例(使用环形缓冲区):
#define N 10 int buffer[N]; int sum = 0; int index = 0; int is_buffer_full = 0; // 标志缓冲区是否已填满 int moving_average_filter(int new_sample) { // 减去即将被覆盖的旧值(如果缓冲区已满) if (is_buffer_full) { sum -= buffer[index]; } // 加入新值 sum += new_sample; buffer[index] = new_sample; index = (index + 1) % N; // 更新缓冲区满标志 if (!is_buffer_full && index == 0) { is_buffer_full = 1; } // 计算平均值 if (is_buffer_full) { return sum / N; } else { // 缓冲区未满时,用当前实际数据个数计算 return sum / index; } }适用场景与坑点:
- 适用:需要实时输出且对平滑度有要求的场景。相比算术平均,它对信号变化的延迟固定为
N-1个采样周期(理论上),响应更快。广泛应用于音频处理、股票价格分析、传感器数据实时显示等。 - 内存与计算开销:需要额外的
N个存储单元。虽然单次计算量很小(一次加法、一次减法、一次除法),但在资源极其紧张的MCU中,N较大时仍需考虑。 - “相位滞后”问题:滑动平均本质上是一个低通滤波器,它仍然会带来相位延迟。对于
N点滑动平均,其群延迟是(N-1)/2个采样周期。这意味着滤波后的波形在时间上会比原始信号“靠后”一点。在需要精确相位关系的控制系统中(如锁相环),这一点必须考虑。 - 加权滑动平均:这是滑动平均的增强版。它认为“近期的数据比早期的数据更重要”,因此在计算平均时给予近期数据更高的权重。最简单的实现是指数加权移动平均(EWMA),也就是下面要讲的“一阶滞后滤波法”,它可以用递归的方式实现,无需保存历史队列。
3.2 一阶滞后滤波法(一阶低通滤波法)
这是我个人在嵌入式项目中最偏爱的滤波算法,没有之一。它完美平衡了简单性、有效性和计算效率。它的思想来源于模拟电路中的RC低通滤波器。
算法逻辑: 这是一个递归公式:本次输出 = α * 本次输入 + (1-α) * 上次输出其中,α是一个介于0和1之间的系数,称为滤波系数。
公式解读:
α越大(越接近1),本次输入权重越大,滤波器响应越快,但平滑效果变差。α越小(越接近0),上次输出权重越大,滤波器平滑效果越好,但响应越迟钝,滞后越严重。- 它相当于一个“惯性”系统,输出会紧紧“跟随”输入,但又不会突变。
C语言示例(浮点版):
float alpha = 0.1; // 滤波系数,根据需求调整 float last_output = 0.0; float first_order_lpf(float new_sample) { float output = alpha * new_sample + (1 - alpha) * last_output; last_output = output; return output; }定点数优化(关键技巧!): 在MCU中跑浮点运算通常比较慢。我们可以用整数运算来模拟。设Q = 1/α(取整数),则公式可变形为:本次输出 = 本次输出 + (本次输入 - 本次输出) / Q或者更常用的一种形式,通过移位实现:本次输出 = 本次输出 + (本次输入 - 本次输出) >> n,其中α = 1/(2^n)
C语言示例(定点整数版):
#define SHIFT_N 3 // 相当于 alpha = 1/8 = 0.125 int last_output = 0; int first_order_lpf_fixed(int new_sample) { // 公式: output = output + (input - output) >> n last_output = last_output + ((new_sample - last_output) >> SHIFT_N); return last_output; }适用场景与坑点:
- 适用:几乎所有需要平滑且实时性要求高的场合。尤其适合MCU,因为定点数版本只需要一次减法和一次移位,速度极快,内存占用极小(只保存一个状态量
last_output)。广泛用于电机电流环、姿态传感器(加速度计、陀螺仪)数据融合、电池电压采样等。 - 参数
α的调试:这是该算法的核心。α的选取与你的采样周期T和期望的滤波器截止频率Fc有关。近似关系为:α ≈ 2πFcT(当α较小时)。例如,采样率100Hz(T=0.01s),希望滤除10Hz以上的噪声,则Fc=10Hz,α ≈ 2*3.14*10*0.01 = 0.628。这个值偏大,实际可先取0.2~0.3试试。更实用的方法是“看波形调参数”:在串口绘图工具上观察原始信号和滤波后信号,调整α(或SHIFT_N),直到在平滑度和响应速度之间找到满意的平衡点。 - 缺点:对脉冲干扰的抑制能力一般。一个大的脉冲输入会使得输出产生一个“台阶”变化,需要好几个周期才能衰减掉。因此,在噪声尖锐的场合,可以在前级先加一个限幅或中值滤波,再用一阶滞后滤波平滑,效果极佳。
3.3 复合滤波法:组合拳的威力
在实际工程中,单一算法往往难以应对所有情况。这时就需要打“组合拳”,将多种简单滤波法按顺序串联使用,各取所长。
常见组合示例:
中位值平均滤波法(又称防脉冲干扰平均滤波法):
- 步骤:连续采样N个数据 -> 去掉其中的最大值和最小值 -> 对剩余的N-2个数据求算术平均。
- 优点:兼具了中值滤波抗脉冲干扰和均值滤波平滑随机噪声的优点。是工程上非常可靠、常用的一种方法。
- 代价:需要排序,计算量稍大。
限幅平均滤波法:
- 步骤:每次采样先经过限幅滤波 -> 将限幅后的“有效值”存入队列 -> 当队列满后,对队列内所有数据进行算术平均。
- 优点:先剔除明显异常的毛刺,防止其污染用于平均的数据池,使得最终平均值更可靠。
- 适用:信号基本稳定,但偶有大干扰的场景。
一阶滞后+限幅:
- 步骤:采样值先经过限幅滤波 -> 输出结果再送入一阶滞后滤波器。
- 优点:先用限幅拦住“坏蛋”,再用一阶滞后进行平滑。既能抵抗突发大干扰,又能提供良好的实时平滑效果,计算量还很小。
- 这是我处理大多数慢变传感器(如温度、压力)的标配流程。
设计复合滤波的原则:
- 前级滤“异常”:把抗脉冲、限幅这类处理“坏点”的算法放在前面。
- 后级做“平滑”:把平均、低通这类平滑噪声的算法放在后面。
- 考虑计算顺序:思考数据流向,确保前级输出是后级期望的输入类型。
- 评估总延迟:串联滤波器的总延迟是各环节延迟的叠加,对于快速控制系统需要仔细评估。
4. 高阶视野:卡尔曼滤波与自适应滤波初探
当系统模型更复杂,或者我们对滤波性能有极致要求时,就需要请出更强大的数学工具。这里介绍两个“名声在外”的算法,旨在让大家理解其思想,知道它们能解决什么问题,而不深究复杂的公式推导。
4.1 卡尔曼滤波:基于模型的最优估计
如果把前面的滤波算法比作“凭经验修图”,那卡尔曼滤波就是“用物理定律和数学模型来预测并修正”。它不仅仅是一个滤波器,更是一套最优估计算法。
核心思想(通俗比喻): 假设你在GPS信号很差的隧道里开车。GPS(观测值)不准,但你的车速表(系统模型)是准的。
- 预测:根据上一秒的位置和车速表显示的速度,预测出当前一秒你应该在哪里。这个预测是有不确定性的(模型误差)。
- 更新:此时,GPS还是给出了一个位置信息,虽然不准但也有参考价值。卡尔曼滤波会聪明地权衡预测值和GPS观测值:如果GPS信号这次特别差(观测噪声大),就更相信自己的预测;如果GPS信号这次比较好,就更相信GPS。
- 融合:根据两者各自的“可信度”(协方差矩阵),计算出一个最优估计值,这个值比单纯用预测或单纯用观测都要准。同时,它还会更新自己对下一个状态预测的“可信度”。
在ADC滤波中的应用简化模型: 我们可以建立一个非常简单的状态模型:
- 状态量:我们关心的真实信号值
x(比如真实电压)。 - 状态方程:假设信号变化很慢,可以认为
x(k) = x(k-1) + w,其中w是过程噪声(描述信号可能的变化)。 - 观测方程:ADC采样值
z(k) = x(k) + v,其中v是观测噪声(ADC本身的噪声)。
卡尔曼滤波通过迭代以下两个步骤,不断给出对真实信号x的最优估计:
- 预测步:基于上一时刻的最优估计,预测当前时刻的状态和不确定性。
- 更新步:结合当前时刻的ADC观测值,修正预测,得到当前时刻的最优估计和新的不确定性。
C语言简化示例(单变量,无控制输入):
// 超简化版,用于理解流程,实际参数Q,R需要根据系统调校 float Q = 0.001; // 过程噪声协方差 (假设信号变化很小) float R = 0.1; // 观测噪声协方差 (ADC噪声大小) float x_hat = 0; // 状态估计值 float P = 1.0; // 估计误差协方差 float K; // 卡尔曼增益 float kalman_filter(float z) { // 1. 预测步 (由于模型简单,状态预测值不变,但不确定性增加) // x_hat_minus = x_hat; // 本例中状态预测等于上一时刻估计 P = P + Q; // 预测误差协方差增大 // 2. 更新步 K = P / (P + R); // 计算卡尔曼增益 x_hat = x_hat + K * (z - x_hat); // 用观测值修正估计 P = (1 - K) * P; // 更新估计误差协方差 return x_hat; }适用场景与坑点:
- 适用:动态系统的状态估计。比如,不仅要滤波,还要预测信号下一步会怎么变。经典应用包括无人机/机器人导航(融合IMU、GPS、磁力计)、电池SOC(荷电状态)估计、跟踪快速变化但模型已知的信号。
- 优势:在系统模型准确的情况下,它能提供理论上最优的估计(最小均方误差)。它能同时处理噪声,并给出估计值的可信度(协方差)。
- 巨大坑点:参数调校(Q, R)是门玄学。
Q(过程噪声)表示你有多相信自己的模型,R(观测噪声)表示你有多相信传感器。这两个参数需要根据实际系统反复调试,调不好效果可能还不如一阶滞后滤波。对于简单的ADC静态采样,杀鸡用牛刀,性价比不高。 - 计算复杂度:完整的卡尔曼滤波涉及矩阵运算(对于多传感器融合)。上面的单变量简化版计算量很小,但一旦状态变量增多,计算量会平方级增长,对MCU有压力。
4.2 自适应滤波:让滤波器“聪明”起来
前面所有的滤波器,参数(如α、N)都是固定的。但实际信号和噪声特性可能会变。比如,设备静止时噪声小,运动时噪声大;信号有时变化慢,有时变化快。固定参数的滤波器无法兼顾所有工况。
自适应滤波的核心思想是:让滤波器的参数能够根据输入信号的特性自动调整,以达到始终(或接近)最优的滤波效果。
一个直观的例子——自适应限幅滤波: 我们可以让限幅滤波的Δ门限不是固定值,而是动态变化的。
- 当最近一段时间内信号很平稳时,我们调小
Δ,提高滤波器的“灵敏度”和“分辨率”。 - 当检测到信号可能处于快速变化阶段时,我们调大
Δ,避免误滤除真实信号,提高“跟踪速度”。
如何判断信号是否平稳?可以计算最近几个采样值的方差或标准差。方差小则平稳,方差大则变化快。
更复杂的算法——LMS(最小均方)自适应滤波: LMS算法有一个“期望响应”的概念。它通过不断调整滤波器系数(权重),使得滤波器输出与“期望响应”之间的误差的均方值最小。它需要一个“参考噪声”或对信号特性的先验知识,在音频降噪、回声消除中应用广泛。但在单纯的ADC采样平滑中,我们很少需要用到这么复杂的自适应滤波。
在ADC滤波中的实用自适应思路: 对于嵌入式开发者,实现完整的自适应算法可能负担较重,但可以借鉴其思想:
- 双系数一阶滞后滤波:准备两套参数,一套平滑系数(
α小)用于稳态,一套快速系数(α大)用于跟踪。通过判断信号变化率(差分值)来切换系数。 - 变长度滑动平均:信号平稳时,用较大的
N获得高平滑度;信号快速变化时,自动减小N甚至暂时切换到单点采样,以保证快速响应。
适用场景与坑点:
- 适用:信号或噪声特性时变的场景。例如,车载传感器(振动噪声随车速变化)、语音信号处理、通信信道均衡。
- 优点:能在不同工况下自动保持较好的性能,鲁棒性更强。
- 缺点:算法复杂,计算量大。参数自适应规则本身也需要设计和调试,搞不好会引入不稳定性(比如在稳态和快速变化之间频繁振荡切换)。对于大多数工况固定的嵌入式应用,精心调试的固定参数滤波器往往更简单可靠。
5. 实战指南:如何为你的项目选择并调优滤波算法
看了这么多算法,到底该怎么选?这里我结合自己的项目经验,给出一个选择流程图和具体的调优步骤。
5.1 算法选择决策树
面对一个ADC滤波需求,你可以顺着以下问题来思考:
信号类型是什么?
- 慢变直流信号(如温度、电池电压):一阶滞后滤波是首选。简单、高效、效果好。想更稳一点,前面加个限幅。
- 带有脉冲干扰的慢变信号(如工业环境下的传感器):中位值平均滤波或限幅+一阶滞后。
- 快速变化信号(如音频、振动):滑动平均滤波。需要仔细权衡窗口大小
N与延迟。 - 需要预测的趋势信号(如飞行器状态):考虑卡尔曼滤波,但前提是你有明确的系统模型和耐心调参。
MCU资源是否紧张?
- 极其紧张(8位机,主频低):优先考虑限幅滤波、一阶滞后滤波(定点数)。避免排序和大的数组。
- 资源一般(如Cortex-M0):可以使用滑动平均(N不宜过大)、复合滤波。
- 资源充足(如Cortex-M4及以上):可以尝试更复杂的算法,如卡尔曼滤波、高阶数字滤波器(IIR/FIR)。
对实时性要求多高?
- 要求极高(控制环路内):算法必须在几个微秒内完成。一阶滞后(定点)、限幅是唯二选择。
- 要求一般(数据上传、显示):可以使用滑动平均、中位值平均等。
噪声主要是什么类型?
- 随机白噪声:平均类滤波(算术平均、滑动平均)效果最好。
- 周期性工频干扰(50/60Hz):这不是后级数字滤波的首选解决方案!应该在硬件前端加RC滤波或使用同步采样技术。数字滤波可作为补充。
- 偶发脉冲:中值滤波、限幅滤波。
5.2 参数调优实战:以一阶滞后滤波为例
理论说了很多,实操才是关键。假设我们选定一阶滞后滤波来平滑一个温度传感器信号,采样周期T = 100ms。
步骤一:采集原始数据不接任何滤波,以100ms间隔通过串口打印ADC原始值,持续几分钟,包含稳定和变化阶段。将数据保存为文本文件。
步骤二:可视化分析使用任何绘图工具(Python的Matplotlib,Excel,甚至一些串口工具自带绘图)将数据画出来。观察:
- 稳定时,噪声的峰峰值大约是多少?(比如 ±5 ADC值)
- 温度变化时,信号的上升/下降速度大概是多少?(比如从25°C到30°C用了10秒)
步骤三:初选参数根据经验,对于100ms采样,α可以从0.1到0.5之间尝试。我们先取α=0.2。对应的定点数移位n满足α ≈ 1/(2^n),1/8=0.125,1/4=0.25。我们取n=2(α=0.25)来近似。
步骤四:算法实现与离线仿真写一个简单的C程序或Python脚本,读入步骤一保存的原始数据,用你写的滤波函数(浮点和定点版都试试)处理一遍,输出滤波后的数据。将原始数据和滤波后数据画在同一张图上对比。
步骤五:效果评估与迭代看图上滤波后的曲线:
- 如果曲线还有明显的高频锯齿:说明平滑不够,需要减小
α(增大n),比如试试n=3(α=0.125)。 - 如果曲线在信号变化时跟不上,滞后太明显:说明响应太慢,需要增大
α(减小n),比如试试n=1(α=0.5)。 - 观察对脉冲干扰的抑制:如果在原始数据中手动加一个尖峰,看滤波后曲线是否出现一个“台阶”?如果这个台阶幅度大且恢复慢,可以考虑在前面增加一级限幅滤波。
步骤六:上机实测将选定的参数和算法写入MCU,再次通过串口绘图观察实时效果。注意,离线仿真时用的是“纯净”的历史数据,实时运行时系统还有其他任务,要确保滤波函数执行时间稳定,不会因为其他中断干扰而引入额外的不确定性。
5.3 常见陷阱与避坑指南
整数除法的精度丢失:在平均滤波或计算
α时,如果使用整数除法,会丢失小数部分。例如sum / 10,当sum=45时结果是4,而不是4.5。解决方案:要么使用浮点数;要么先累加,最后一次性转换;或者采用“舍入”除法:(sum + N/2) / N。累加和溢出:这是新手常犯的错误。
uint16_t类型的变量最大65535。如果ADC是12位(0-4095),N=20,累加和最大可达81900,已经溢出。解决方案:根据ADC最大值 * N来选择合适的变量类型,如uint32_t。滤波引起的相位滞后影响控制系统:在PID控制环路中,对反馈信号进行滤波会引入相位滞后,可能导致系统不稳定(振荡)。解决方案:尽量减小滤波延迟(用一阶滞后且
α取大),或者在对延迟不敏感的地方滤波(如对设定值滤波)。更高级的做法是在控制器设计时就考虑滤波器的相位特性。“过度滤波”淹没有用信号:为了追求一条“光滑”的曲线,把滤波参数调得过于“猛烈”,导致真实信号的变化也被平滑掉了。比如一个缓慢上升的趋势,看起来像一条水平线。解决方案:始终对比原始信号和滤波信号,确保滤波后的信号仍能正确反映真实信号的变化趋势,而不仅仅是平滑。
默认ADC的噪声就是白噪声:ADC的噪声可能包含特定的频率成分(如电源纹波)。如果噪声是周期性的,简单的平均滤波效果有限。解决方案:用示波器看ADC输入引脚的实际波形,或者对采集的数据做FFT分析,找到噪声的主要频率,然后针对性地设计滤波器(如设计一个数字陷波器滤除特定频率)。
滤波算法没有“银弹”,最好的算法永远是那个最适合你具体应用场景、资源约束和性能需求的算法。从简单的开始尝试,理解其原理和局限,必要时进行组合,配合扎实的调试,你一定能得到干净、可靠的采样数据。