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

传感器数据进了ADC,你以为就干净了?

我见过太多工程师,花大价钱买高精度传感器,结果被一个50Hz工频干扰搞得焦头烂额。硬件滤波固然重要,但数字滤波才是真正的“最后一公里”。

这一章,咱们就把四种最常用的数字滤波算法掰开揉碎。移动平均、中值滤波、限幅滤波,还有那个听起来高大上的卡尔曼滤波。我会告诉你它们各自适合什么场景,以及在STM32上怎么写、怎么选。

核心观点:没有最好的滤波算法,只有最合适的。选错了,再好的算法也是白搭。

数字滤波算法选型决策树 原始传感器数据 有随机噪声? → 移动平均滤波 有尖峰脉冲? → 中值滤波 有跳变毛刺? → 限幅滤波 需要预测/平滑? → 卡尔曼滤波 RAM占用: 低 CPU占用: 极低 RAM占用: 中 CPU占用: 低 RAM占用: 极低 CPU占用: 极低 RAM占用: 高 CPU占用: 中 选型口诀:噪声用平均,脉冲用中值, 跳变用限幅,预测用卡尔曼

4.1 移动平均滤波:最朴素的降噪利器

移动平均滤波的原理,说白了就是“取最近N个值的平均数”。你想想看,随机噪声的均值是零,多取几次平均,噪声自然就被抵消了。

适用场景:温度、湿度、光照等变化缓慢的模拟量。我做过一个温控项目,用移动平均滤波后,温度波动从±1.5℃降到了±0.3℃。

我的习惯:N值一般取4、8、16这样的2的幂次。为什么?因为做除法时可以用移位操作,STM32上快很多。

// 移动平均滤波 - STM32实现
#define FILTER_N 8

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

uint16_t moving_average(uint16_t new_sample) {
    sum -= buffer[index];          // 减去最旧的数据
    sum += new_sample;             // 加上最新数据
    buffer[index] = new_sample;    // 更新缓冲区
    index = (index + 1) & (FILTER_N - 1);  // 循环索引
    return (uint16_t)(sum >> 3);   // 除以8,用移位实现
}

这段代码里有个小技巧:index = (index + 1) & (FILTER_N - 1)。当N是2的幂时,位与操作比取模快得多。我在STM32F103上实测过,快了将近5倍。

注意:移动平均滤波会引入延迟。N越大,延迟越大。如果传感器数据变化很快(比如加速度计),就别用太大的N值。

4.2 中值滤波:专治“野值”和脉冲干扰

中值滤波的思路更直接:把最近N个数据排序,取中间那个值。这样一来,哪怕有一个数据是1000,其他都是100左右,中值还是100,那个野值直接被“开除”了。

适用场景:有偶发性脉冲干扰的信号,比如按键检测、红外避障传感器。我记得有一次做激光测距,偶尔会冒出个离谱的数值,用中值滤波后,问题迎刃而解。

// 中值滤波 - STM32实现(N=5,冒泡排序)
#define MEDIAN_N 5

uint16_t median_filter(uint16_t new_sample) {
    static uint16_t buf[MEDIAN_N] = {0};
    static uint8_t count = 0;
    uint16_t temp[MEDIAN_N];
    uint8_t i, j;

    // 更新环形缓冲区
    buf[count] = new_sample;
    count = (count + 1) % MEDIAN_N;

    // 拷贝到临时数组并排序
    for(i = 0; i < MEDIAN_N; i++) temp[i] = buf[i];

    // 冒泡排序(N小,够用)
    for(i = 0; i < MEDIAN_N - 1; i++) {
        for(j = 0; j < MEDIAN_N - 1 - i; j++) {
            if(temp[j] > temp[j+1]) {
                uint16_t t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    return temp[MEDIAN_N / 2];  // 取中值
}

这里我用了冒泡排序,因为N=5时排序次数很少。如果N比较大(比如9或11),建议换成插入排序,效率更高。

避坑指南:我曾经在一个项目中把N设成了15,结果每次滤波都要排序,CPU占用率飙升。后来改成N=5,效果差不多,CPU占用降了70%。记住:中值滤波的N值,够用就行。

4.3 限幅滤波:最简单的“防抖”方案

限幅滤波的逻辑特别简单:如果当前采样值和上次值的差值超过某个阈值,就丢弃当前值,用上次值代替。说白了,就是“变化太快我不信”。

适用场景:传感器偶尔出现跳变毛刺的场景,比如电机启动时的电磁干扰、继电器吸合瞬间的尖峰。

// 限幅滤波 - STM32实现
#define LIMIT_THRESHOLD 50   // 最大允许变化量

uint16_t limit_filter(uint16_t new_sample) {
    static uint16_t last_value = 0;
    uint16_t diff;

    if(new_sample > last_value) {
        diff = new_sample - last_value;
    } else {
        diff = last_value - new_sample;
    }

    if(diff <= LIMIT_THRESHOLD) {
        last_value = new_sample;  // 变化在允许范围内,更新
    }
    // 否则保持上次值不变

    return last_value;
}

阈值怎么设?我一般先看传感器正常情况下的最大变化率,然后留出20%-30%的余量。比如温度传感器每秒最多变化2℃,那就设阈值为3℃对应的ADC值。

注意:限幅滤波不能消除周期性噪声,只能对付偶发毛刺。如果传感器信号本身变化就很快(比如电机转速),阈值设小了会把正常信号也滤掉。

4.4 卡尔曼滤波入门:看起来难,用起来香

卡尔曼滤波,很多人一听就觉得是“天书”。其实你把它当成一个“会自我学习的滤波器”就行。它根据上一刻的估计值和当前的测量值,动态计算出一个最优估计值。

我刚开始学的时候也被公式吓到了,后来发现,在嵌入式里只用一维卡尔曼滤波就够了。说白了,就是两个公式来回算:预测、更新。

// 一维卡尔曼滤波 - STM32实现
typedef struct {
    float Q;      // 过程噪声协方差
    float R;      // 测量噪声协方差
    float P;      // 估计误差协方差
    float K;      // 卡尔曼增益
    float X;      // 滤波后的值
} Kalman_Struct;

void Kalman_Init(Kalman_Struct *kf, float init_val) {
    kf->Q = 0.01;    // 调参关键:Q越小越平滑
    kf->R = 0.1;     // 调参关键:R越大越信任预测
    kf->P = 1.0;
    kf->X = init_val;
}

float Kalman_Update(Kalman_Struct *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.0 - kf->K) * kf->P;

    return kf->X;
}

这段代码里,Q和R是两个关键参数。Q越小,滤波结果越平滑;R越大,越不相信测量值。我调参时有个笨办法:先固定R,从小到大调Q,看波形什么时候最顺眼。

我的经验:卡尔曼滤波在STM32F103上跑一维版本,一次更新大约需要5-10微秒。对于100Hz以下的采样率,完全够用。但如果是多维卡尔曼(比如MPU6050的六轴融合),建议用F4或H7系列。

4.5 性能对比:到底该用哪个?

四种算法各有千秋,我整理了一张对比表,方便你选型时参考。

算法 RAM占用 CPU占用 延迟 抗脉冲 抗随机噪声 适用场景
移动平均 低 (N×2字节) 极低 N/2个采样周期 差 优 温度、湿度、光照
中值滤波 中 (N×2字节) 低 N/2个采样周期 优 中 红外、超声波、按键
限幅滤波 极低 (2字节) 极低 1个采样周期 优 差 电机电流、继电器信号
卡尔曼滤波 高 (多个float) 中 1个采样周期 优 优 加速度计、陀螺仪、GPS

从表里能看出来:

  • 资源紧张、噪声随机 → 移动平均滤波
  • 有脉冲干扰、不差那点RAM → 中值滤波
  • 偶尔跳变、要求实时性 → 限幅滤波
  • 既要平滑又要低延迟、CPU有余量 → 卡尔曼滤波

我的建议:别一上来就上卡尔曼。先试试移动平均,不行再加中值,还不行再考虑卡尔曼。很多时候,组合滤波的效果比单一算法好得多。比如“限幅+移动平均”就是一对黄金搭档。

嗯,四种算法都讲完了。代码可以直接复制到你的STM32工程里用。记住,滤波算法是工具,不是目的。你的目标是让传感器数据“干净可用”,而不是追求算法本身有多炫酷。