4、数字滤波算法:移动平均滤波、中值滤波、卡尔曼滤波入门

模拟滤波搞完了,咱们聊聊数字滤波。说实话,我入行头两年总觉得数字滤波是“软件的事”,硬件工程师把前端调理电路做好就行了。直到有一次做电机电流采样,模拟滤波器把相位搞得一塌糊涂,我才老老实实回来研究数字滤波。

数字滤波的好处很明显:不改硬件、参数可调、没有温漂。坏处也直接——占CPU、有延迟。怎么选?看场景。

核心原则:模拟滤波负责抗混叠和粗滤,数字滤波负责精细处理和特征提取。两者是搭档,不是对手。

4.1 移动平均滤波:最朴素的平滑器

移动平均滤波,说白了就是取最近N个采样值的平均数。你想想看,电流信号上叠加了白噪声,每个点抖一下,平均一下不就稳了?

公式很简单:

y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N

我在项目里常用的是滑动窗口实现,不存全部数据,只维护一个累加和:

// 移动平均滤波 - 滑动窗口实现
#define WINDOW_SIZE 16

static uint16_t buffer[WINDOW_SIZE] = {0};
static uint8_t index = 0;
static uint32_t sum = 0;

uint16_t moving_average(uint16_t sample) {
    sum -= buffer[index];          // 去掉最旧的值
    sum += sample;                 // 加上最新的值
    buffer[index] = sample;        // 更新缓冲区
    index = (index + 1) % WINDOW_SIZE;
    return sum / WINDOW_SIZE;      // 返回平均值
}
我的习惯:窗口大小选2的幂次(8、16、32),这样除法可以用移位代替,省掉除法器那几十个时钟周期。在Cortex-M0上,这差别挺明显的。

优缺点:

优点 缺点
实现简单,计算量小 对脉冲干扰敏感(一个坏点会污染N个输出)
平滑效果好,噪声抑制与√N成正比 有固定延迟 = N/2个采样周期
适合白噪声环境 不能滤除与信号频率相近的噪声

什么时候用?电流采样做平均值计算、ADC前级有白噪声、对实时性要求不苛刻的场景。我做过一个电池管理系统,电流采样就用32点移动平均,效果很稳。

4.2 中值滤波:脉冲干扰的克星

移动平均最怕什么?脉冲干扰。比如电机启动瞬间的尖峰、继电器动作时的毛刺。一个异常大值,能把平均值拉偏好一阵子。

中值滤波的思路完全不同——取窗口内所有值排序后的中间值。你想想,不管脉冲多大,只要它只出现一次,排序后肯定被挤到边上,中间值不受影响。

// 中值滤波 - 窗口大小3(最常用)
uint16_t median_filter_3(uint16_t a, uint16_t b, uint16_t c) {
    // 排序三个值,取中间
    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; }
    return b;
}
注意:窗口越大,排序开销越大。3点中值滤波只需要3次比较,5点就需要完整排序了。在资源紧张的MCU上,窗口不要超过5。

中值滤波 vs 移动平均:

特性 中值滤波 移动平均
脉冲抑制 优秀(完全消除单点脉冲) 差(脉冲会扩散)
平滑效果 一般(信号会有阶梯感) 优秀(连续平滑)
计算量 大(需要排序) 小(只需加减法)
延迟 窗口/2 窗口/2

我曾经在一个变频器项目里,电流采样总是莫名其妙跳变。查了半天,发现是IGBT开关带来的共模干扰。加上3点中值滤波后,问题直接消失。嗯,有时候解决问题就这么简单。

4.3 卡尔曼滤波入门:状态估计的瑞士军刀

卡尔曼滤波,听起来高大上,其实核心思想就一句话:用模型预测 + 用测量修正。

你想想看,电流信号不是随机乱跳的——它受电路方程约束。卡尔曼滤波就是利用这个约束,把“预测值”和“测量值”按置信度加权融合,得到最优估计。

对于电流采样这种一维系统,卡尔曼滤波可以简化成几个公式:

// 一维卡尔曼滤波(简化版)
typedef struct {
    float x;      // 状态估计值
    float p;      // 估计误差协方差
    float q;      // 过程噪声协方差
    float r;      // 测量噪声协方差
    float k;      // 卡尔曼增益
} kalman_1d_t;

void kalman_init(kalman_1d_t *kf, float init_x, float init_p, float q, float r) {
    kf->x = init_x;
    kf->p = init_p;
    kf->q = q;
    kf->r = r;
}

float kalman_update(kalman_1d_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 - kf->k) * kf->p;
    
    return kf->x;
}
调参经验:q/r的比值决定了滤波器的“信任倾向”。q/r大 → 更相信测量值(响应快但噪声大);q/r小 → 更相信预测值(平滑但延迟大)。我一般从q/r=0.01开始调,看波形再微调。

三种滤波器的选择指南:

场景 推荐算法 理由
白噪声环境,实时性要求高 移动平均 计算量最小,延迟可控
有脉冲干扰,信号变化慢 中值滤波 彻底消除脉冲,实现简单
需要高精度估计,有模型可用 卡尔曼滤波 最优估计,自适应能力强
资源极度受限(8位MCU) 移动平均或中值滤波 卡尔曼的浮点运算扛不住

下面这张图帮你理清三种滤波器的核心逻辑:

数字滤波算法选型逻辑 原始电流采样值 移动平均滤波 滑动窗口求均值 中值滤波 排序取中间值 卡尔曼滤波 预测+测量修正 适用场景 白噪声环境 实时性要求高 适用场景 脉冲干扰 信号变化慢 适用场景 高精度估计 有模型可用 滤波后的电流值 选型口诀: 有脉冲用中值,有噪声用平均 要精度上卡尔曼,资源不够回平均

4.4 实战建议:怎么搭滤波链路

实际项目中,我很少只用一种滤波器。更常见的做法是组合使用:

  1. 第一级:中值滤波(窗口3) — 干掉脉冲干扰,保护后续算法不被异常值污染
  2. 第二级:移动平均(窗口8-16) — 平滑白噪声,输出稳定的电流值
  3. 第三级(可选):卡尔曼滤波 — 如果需要对电流做动态估计(比如预测下一时刻的电流),加上卡尔曼做状态跟踪
避坑提醒:我曾经在一个项目里把移动平均窗口设到64,结果电流环响应慢了200ms,电机一启动就过流保护。后来把窗口缩到16,问题解决。记住:滤波器的延迟会直接进入控制环路,必须算清楚。

最后说一句:别迷信卡尔曼滤波。如果你的MCU是8位、主频只有几MHz,老老实实用移动平均或中值滤波。卡尔曼的浮点运算和矩阵乘法,在这种平台上跑起来,CPU占用率直接拉满,别的活就别干了。