干了十几年嵌入式,ADC 这块踩过的坑能写一本《防坑指南》。滤波不是玄学,是工程权衡——你总得在「够平滑」和「跟得上」之间找个平衡点。下面按实战中常用的几种方式捋一捋,顺带讲点真实经历。
一、先说清楚:为什么要给 ADC 滤波?
ADC 出来的数,很少能「拿来就用」。电源纹波、地线噪声、电磁干扰、量化误差都会叠在真实信号上,所以硬件滤波在采样前砍掉一部分高频和混叠,软件滤波在采样后把剩下的毛刺和抖动抹平;两者配合,既省成本又稳得住。
有一年接了个工业温控表的单子,样机在实验室怎么看都正常,一到客户现场屏上温度就隔几秒跳一下。带着示波器去蹲了两天,发现车间里变频器一启一停,地线上就窜进来一串脉冲,ADC 照单全收。当时想在前端加 RC,BOM 多一毛钱都被打回,只能在软件里做「中值去尖峰 + 一阶低通抹平」,烧进去再测,数值才稳。从那以后遇到现场和实验室两副面孔的案子,都会先想:硬件能挡多少、软件再补多少。
信号与滤波流程示意:
后端
前端
软件滤波
均值/中值/低通/卡尔曼
控制/显示/保护
硬件滤波
RC/有源
传感器/信号源
ADC 采样
二、硬件滤波(前端滤波)
在 ADC 采样之前做,主要目的是抑制高频和混叠,减轻 ADC 和后续软件的压力。
2.1 RC 低通滤波
电阻 + 电容搭的一阶无源低通,电路简单、成本低、不耗电,可靠性也高;代价是截止特性软(约 -20 dB/decade),想压得狠就得大 RC,响应会变慢,而且无源电路带载能力有限,适合带宽要求不高、成本敏感、对相位要求不严的场景(直流电压、慢变温度之类)。有一版电源监测板为了省一颗运放,模拟前端只留了一级 RC,自家实验室没问题,到了客户产线(有大功率射频设备)ADC 曲线就总有一层细密的小锯齿,RC 拐点压到几十 Hz 毛刺还在,最后在软件里对这条通道又加了一阶低通才通过验收——硬件能砍多少算多少,剩下的交给软件,别指望一颗 RC 包打天下。
结构示意(一阶 RC 低通):
Vi --------[ R ]--------+-------- Vo
|
[C]
|
GND
fc = 1/(2π·R·C) ,幅频约 -20dB/decade
2.2 有源滤波器(如 Sallen-Key)
用运放搭二阶或多阶,能做出更陡的截止沿,带内更平、可做精确频率响应,高输入低输出阻抗也不拖累前级、易驱动 ADC;代价是多器件、要供电,还要考虑运放噪声和失调,设计和调试都比 RC 复杂,适合对带外抑制、带内平坦度有明确要求的场合(振动、电流采样等)。做某款振动采集板时规格书写死「500 Hz 以上衰减 ≥ 40 dB」,单阶 RC 达不到,只能上有源,Sallen-Key 截止设在 400 Hz 左右扫频是够了,结果高温老化时零点漂了几十 mV,查下来是运放选型没细看温漂,只好换料重贴——有源滤波性能强,器件选型和 layout 一点都不能马虎。
结构示意(Sallen-Key 二阶低通概念):
Vi ---[R1]---[R2]---(+)---[运放]--- Vo
| \ |
[C1] \ [C2](反馈)
| \
GND GND
二阶:-40dB/decade,Q 可调,带外衰减比 RC 陡
三、软件滤波(数字滤波)
在 ADC 采样之后用算法处理,不改硬件、不改 BOM,灵活可调,是嵌入式里最常用的手段。
3.1 移动平均滤波(Moving Average)
对最近 N 个采样做算术平均,窗口随新采样滑动。实现简单、运算量小(加减 + 除法),对缓变信号平滑效果好;缺点是 N 一大延迟就上去(约 (N-1)/2 个采样点),抗脉冲差,阻带衰减也慢(约 20 dB/decade)。做电源电压监测时用过 8 点移动平均,数值是稳了,但上电瞬间「电压建立」在屏上显示明显变慢,客户觉得像卡顿;后来把 N 改成 4,显示跟得上,噪声略大一点但在规格内——N 就是你和「延迟」做的交易,调的时候心里要有数。实现上务必用循环缓冲区维护「当前窗口和」,每来一个新值「减最老、加最新」再除以 N,别每次重算整窗求和。
数据流示意:
n
循环缓冲区
N 个采样
sum = sum - 最老 + 最新
out = sum / N
示例代码:
#define WINDOW_SIZE 10
#define ADC_RESOLUTION 12
typedef struct {
uint16_t buffer[WINDOW_SIZE];
uint32_t sum;
uint8_t index;
uint8_t count; /* 有效点数,未满窗时用 count 除 */
} MA_Filter_t;
void ma_filter_init(MA_Filter_t *f) {
memset(f->buffer, 0, sizeof(f->buffer));
f->sum = 0;
f->index = 0;
f->count = 0;
}
uint16_t ma_filter_update(MA_Filter_t *f, uint16_t new_val) {
if (f->count == WINDOW_SIZE)
f->sum -= f->buffer[f->index];
else
f->count++;
f->buffer[f->index] = new_val;
f->sum += new_val;
f->index = (f->index + 1) % WINDOW_SIZE;
return (uint16_t)(f->sum / f->count);
}
3.2 中值滤波(Median Filter)
对窗口内 N 个采样排序取中位数(N 建议奇数,3、5、7)。抗脉冲和毛刺很强,边缘也保持得好,适合按键去抖、有电磁脉冲的工业现场、需要保留边沿的慢变信号;代价是要排序 O(N²)、占整窗内存,对高斯白噪声的平滑不如均值。产线有一台设备「偶尔误动作」,保护灯闪一下又恢复,带着逻辑分析仪蹲了半天发现是继电器吸合瞬间 ADC 读到一两个尖峰,软件当成过流;加了一级 5 点中值,尖峰被压成正常水平,再没误报——对付「偶尔来一下」的脉冲,中值是最直接的一刀。窗别开太大,否则排序很吃 CPU,小窗用插入排序或 3/5 点手写比较就行。
数据流示意:
n
滑动窗口 N 点
排序
取中位数
n
示例代码(5 点中值,插入排序,适合小窗):
#define MEDIAN_WINDOW 5
typedef struct {
uint16_t buffer[MEDIAN_WINDOW];
uint8_t index;
} MedianFilter_t;
void median_filter_init(MedianFilter_t *f) {
memset(f->buffer, 0, sizeof(f->buffer));
f->index = 0;
}
/* 对 buf 进行插入排序,取中值 */
static uint16_t median_of(uint16_t *buf, int n) {
uint16_t t[MEDIAN_WINDOW];
int i, j;
uint16_t v;
for (i = 0; i < n; i++) t[i] = buf[i];
for (i = 1; i < n; i++) {
v = t[i];
for (j = i; j > 0 && t[j - 1] > v; j--)
t[j] = t[j - 1];
t[j] = v;
}
return t[n / 2];
}
uint16_t median_filter_update(MedianFilter_t *f, uint16_t new_val) {
f->buffer[f->index] = new_val;
f->index = (f->index + 1) % MEDIAN_WINDOW;
return median_of(f->buffer, MEDIAN_WINDOW);
}
3.3 一阶低通滤波(一阶惯性 / 指数滑动平均)
差分方程 ( y_n = \alpha x_n + (1-\alpha) y_{n-1} ):新输出 = 新采样与旧输出的加权和,α 越大越跟输入,α 越小越平滑、越慢。计算量极小(两乘一加),只存一个状态,还能写成定点(移位代替乘除),适合电流环、电压环、电池电压、转速等要「既平滑又跟得上」的闭环量;缺点是截止软(-20 dB/decade)、阶跃有建立时间、对脉冲不如中值。电机电流采样用一阶低通时,α 调大了电流环振荡,α 调小了转矩响应又钝,后来按「电流环带宽的 1/5~1/10」设截止频率反推 α,一次算好烧进去就稳了——一阶低通最适合先算清楚截止频率再定 α,别光靠试。
数据流示意:
当前拍
下一拍作为状态
× α
x[n] 新采样
y[n-1] 上一拍输出
× (1-α)
加法
y[n] 输出
示例代码(浮点 + 定点两种):
/* 浮点:按截止频率初始化 */
typedef struct {
float alpha;
float output;
} FirstOrderFilter_t;
void first_order_init(FirstOrderFilter_t *f, float cutoff_hz, float sample_hz) {
float RC = 1.0f / (2.0f * 3.1415926f * cutoff_hz);
float Ts = 1.0f / sample_hz;
f->alpha = Ts / (RC + Ts);
f->output = 0.0f;
}
float first_order_update(FirstOrderFilter_t *f, float input) {
f->output = f->alpha * input + (1.0f - f->alpha) * f->output;
return f->output;
}
/* 定点:无浮点 MCU,α = 1/8 */
#define ALPHA_SHIFT 3
int16_t first_order_fixed(int16_t new_sample) {
static int32_t output = 0;
output = (new_sample << ALPHA_SHIFT) + (8 - 1) * output;
output = output >> ALPHA_SHIFT;
return (int16_t)output;
}
3.4 卡尔曼滤波(Kalman Filter)
用状态空间模型做「预测 + 测量修正」的递归最优估计,模型和噪声合适时估计最优,增益 K 随不确定度自适应,还能自然融合多传感器;代价是计算复杂、Q/R 难调、依赖模型,模型偏了可能发散。做四轴时加速度计抖、陀螺漂,单独用谁都不稳,上了一维卡尔曼用角速度预测、加速度计倾角做测量修正,Q/R 调了一下午:Q 大了跟测量太紧还是抖,R 大了又反应慢,调好后姿态角明显稳了一截——卡尔曼是「模型 + 调参」的活儿,适合有明确物理量的场景,别指望默认参数就能用。一维标量版适合 ADC/单状态平滑,完整版才涉及矩阵。
数据流示意(一维标量:预测 → 测量更新):
测量更新
预测
K = P/(P+R)
X̂ = X̂ + K(z - X̂)
测量 z
P = (1-K)P
P = P + Q
X̂, P
示例代码(一维标量卡尔曼,适合 ADC/单状态平滑):
typedef struct {
float Q; /* 过程噪声协方差 */
float R; /* 测量噪声协方差 */
float P; /* 估计误差协方差 */
float K; /* 卡尔曼增益 */
float X; /* 状态估计值 */
} KalmanFilter_t;
void kalman_init(KalmanFilter_t *kf, float Q, float R, float init_val) {
kf->Q = Q;
kf->R = R;
kf->P = 1.0f;
kf->X = init_val;
}
float kalman_update(KalmanFilter_t *kf, float measurement) {
/* 预测:状态不变,误差协方差增大 */
kf->P = kf->P + kf->Q;
/* 测量更新:计算增益、修正估计、更新协方差 */
kf->K = kf->P / (kf->P + kf->R);
kf->X = kf->X + kf->K * (measurement - kf->X);
kf->P = (1.0f - kf->K) * kf->P;
return kf->X;
}
3.5 滑动窗口滤波(去极值平均)
在移动平均基础上先去掉窗口内最大、最小值(或各去几个极值),再对剩余点求平均。抗野值比纯移动平均强,又比中值省算力(不用排序,只找 min/max);缺点是会削掉真实峰谷,若信号本身就有合法的大峰大谷会被误伤。有一回做电流保护,纯移动平均在负载突变时被一两个异常采样拉偏导致误保护,用 5 点中值又嫌排序吃 CPU,改成 8 点去最大最小再平均,既扛住偶发野值又没明显拖慢保护响应——适合「偶尔坏一两个点」的场合,比中值省 CPU。
数据流示意:
n
滑动窗口 N 点
找 min / max
求和 sum
(sum - min - max) / (N-2)
n
示例代码(去一个最大、一个最小后求平均):
#define WINDOW_SIZE 8
typedef struct {
uint16_t buffer[WINDOW_SIZE];
uint8_t index;
} SlidingWindowFilter_t;
void sliding_window_init(SlidingWindowFilter_t *f) {
memset(f->buffer, 0, sizeof(f->buffer));
f->index = 0;
}
uint16_t sliding_window_update(SlidingWindowFilter_t *f, uint16_t new_val) {
uint16_t min_v, max_v, sum = 0;
uint8_t i;
f->buffer[f->index] = new_val;
f->index = (f->index + 1) % WINDOW_SIZE;
min_v = max_v = f->buffer[0];
for (i = 0; i < WINDOW_SIZE; i++) {
if (f->buffer[i] < min_v) min_v = f->buffer[i];
if (f->buffer[i] > max_v) max_v = f->buffer[i];
sum += f->buffer[i];
}
sum = sum - min_v - max_v;
return (uint16_t)(sum / (WINDOW_SIZE - 2));
}
四、混合滤波策略(实战常用)
单一算法往往不够用,多级组合很常见:硬件 RC + 软件移动平均或一阶低通,先砍高频和混叠再平滑一层,成本低效果好;中值 + 一阶低通,先中值去脉冲和毛刺再低通平滑,适合工业现场既有脉冲又有连续噪声;滑动窗口(去极值)+ 一阶低通则在抗野值和平滑、省算力之间折中。某批仪表在强干扰现场数值仍跳,单用移动平均压不住尖峰,单用中值又嫌抖,改成「3 点中值 + 一阶低通」:中值砍尖峰,低通把剩下的小抖动抹平,现场长期稳定——很多问题的答案都是先分类噪声,再组合拳。
组合示意(中值 + 一阶低通):
ADC
中值滤波
一阶低通
out
示例代码(3 点中值 + 一阶低通):
uint16_t combined_median_lowpass(uint16_t new_sample) {
static uint16_t median_buf[3];
static uint8_t med_idx = 0;
static float lowpass_out = 0.0f;
const float alpha = 0.3f;
uint16_t a, b, c, median_out;
median_buf[med_idx] = new_sample;
med_idx = (med_idx + 1) % 3;
a = median_buf[0]; b = median_buf[1]; c = median_buf[2];
/* 3 点取中值(比较排序) */
if (a > b) { uint16_t t = a; a = b; b = t; }
if (b > c) { uint16_t t = b; b = c; c = t; }
if (a > b) { uint16_t t = a; a = b; b = t; }
median_out = b;
lowpass_out = alpha * (float)median_out + (1.0f - alpha) * lowpass_out;
return (uint16_t)lowpass_out;
}
五、选择建议(按场景)
选型思路示意(按噪声与约束对号入座):
脉冲/毛刺
高频随机噪声
慢漂/直流波动
多源融合/最优估计
是
否
ADC 滤波选型
主要干扰类型?
中值 或 中值+低通
一阶低通 或 移动平均
移动平均 或 一阶低通
卡尔曼
资源是否紧张?
一阶低通 或 小 N 移动平均
可考虑中值/滑动窗口/组合
| 场景 | 更合适的滤波方式 | 简要原因 |
|---|---|---|
| 温度、压力等慢变物理量 | 移动平均 或 一阶低通 | 信号慢,要的是平滑和可读性,对延迟不敏感。 |
| 存在脉冲/毛刺(按键、继电器、电磁干扰) | 中值滤波 | 目标是「别被偶尔一下带偏」,中值最对症。 |
| 电机电流、电压环、转速等控制量 | 一阶低通(或配合硬件 RC) | 要平衡平滑与动态响应,一阶低通易设计、易实现。 |
| 姿态、导航、多传感器融合 | 卡尔曼(或 EKF/UKF) | 有模型、要最优估计、可融合多源时使用。 |
| RAM/算力非常紧张的 MCU | 移动平均(小 N)或 一阶低通 | 少状态、少运算、易用定点实现。 |
| 既有脉冲又有连续噪声的工业现场 | 中值 + 一阶低通 或 去极值平均 + 低通 | 先除脉冲再平滑,兼顾抗干扰与波形。 |
六、注意事项(老工程师的几条原则)
- 采样率与奈奎斯特:采样频率至少要大于信号最高频率的 2 倍,否则混叠进来再好的数字滤波也救不回来;必要时前端硬件低通先把带外压下去。
- 滤波与延迟的权衡:滤波越「狠」,通常延迟越大、动态越慢。做控制时尤其要算清楚相位裕度和建立时间,别为了「数字好看」把系统搞得不稳定。
- 先看噪声再选方法:高频随机噪声 → 低通类;脉冲/毛刺 → 中值或去极值;慢漂 → 均值或低通;要最优估计和融合 → 卡尔曼。不同噪声用不同工具。
- 能硬件先砍一刀就别全交给软件:前端 RC 或有源滤波成本不高时,能显著减轻 ADC 和软件压力,调试和量产都更省心。
七、小结
ADC 滤波没有「银弹」:移动平均简单好用,适合慢变和资源紧张;中值专治脉冲和毛刺;一阶低通在控制里最常用、也最易算清楚;卡尔曼适合有模型、要融合、要最优估计的场景;去极值平均是移动平均的增强版,抗野值更好。 实际项目里往往是「硬件滤一道 + 软件一种或两种组合」,再根据现场噪声和系统要求微调参数。多试、多测、多留余量,滤波这块就能少踩坑。
— 以上是结合多年嵌入式项目整理的 ADC 滤波方式与选型笔记,偏实战和取舍,公式和理论只保留到够用为止。若有具体电路或代码场景,可以按「噪声类型 + 资源约束 + 动态要求」三条线对号入座再细调。