4、软件滤波消除抖动:均值滤波、中值滤波、一阶低通滤波的原理与代码实现

启动抖动的本质,说白了就是电流或速度信号在启动瞬间来回跳变。硬件上我们可以加RC滤波,但成本、空间、可调性都不太理想。我个人习惯在软件层面做滤波,灵活、零成本、还能随时调参数。

这一章我们重点聊三种最常用的软件滤波方法:均值滤波、中值滤波、一阶低通滤波。它们各有各的脾气,用对了地方就是神器,用错了反而添乱。

核心观点:没有万能的滤波算法,只有最合适的场景。均值滤波适合平滑噪声,中值滤波擅长剔除野点,一阶低通滤波则是在实时性和平滑度之间找平衡。

软件滤波方法知识体系图 软件滤波消除抖动 — 三种核心方法 启动抖动信号 均值滤波 原理:取N次采样平均值 特点:平滑噪声,有延迟 适用:稳态信号 中值滤波 原理:取排序后中间值 特点:抗脉冲干扰强 适用:有野点的信号 一阶低通滤波 原理:加权递推平滑 特点:实时性好,可调 适用:动态变化信号 三种方法核心对比 计算量 低 中(需排序) 极低

4.1 均值滤波:最简单的平滑方案

均值滤波的思路很直白——连续采N个值,加起来除以N。你想想看,如果信号本身是稳定的,只是叠加了随机噪声,多采几次平均一下,噪声自然就被削弱了。

我在项目中遇到过用均值滤波处理电流采样。电机启动时电流有高频毛刺,用10点均值滤波后,波形干净了很多。但要注意,N不是越大越好。N太大,响应变慢,启动瞬间的真实电流变化会被"抹平"。

经验值:对于1kHz的采样率,我一般取N=8~16。既能把噪声压下去,又不至于让响应滞后太明显。

// 均值滤波 - 循环队列实现
#define FILTER_N 10

typedef struct {
    int16_t buffer[FILTER_N];
    uint8_t index;
    int32_t sum;
} MeanFilter_t;

int16_t MeanFilter_Process(MeanFilter_t *f, int16_t new_val) {
    // 减去最旧的值,加上新值
    f->sum -= f->buffer[f->index];
    f->sum += new_val;
    f->buffer[f->index] = new_val;
    f->index = (f->index + 1) % FILTER_N;
    
    return (int16_t)(f->sum / FILTER_N);
}

这段代码用了循环队列,每次只需要做一次减法和一次加法,计算量很小。嗯,这里要注意:sum变量要用int32_t,否则N大了容易溢出。我曾经在这个坑里摔过一次,调试了半天才发现是累加和溢出了。

4.2 中值滤波:专治"野点"

均值滤波怕什么?怕脉冲干扰。假如10个数据里突然蹦出一个离谱的跳变,均值会被严重拉偏。这时候中值滤波就派上用场了。

中值滤波的原理更简单:把N个数据排序,取中间那个值。说白了,不管数据怎么跳,只要正常数据占多数,中间值就不会被野点带偏。

我记得有一次做无刷电机启动,霍尔传感器偶尔会冒出一个错误的脉冲。用均值滤波根本压不住,换了中值滤波后,问题立刻解决。为什么?因为错误脉冲只占少数,排序后它要么在最左边要么在最右边,根本影响不到中间值。

注意:中值滤波的窗口N一般取奇数,比如3、5、7。偶数的话中间两个值还要再平均,多一步计算。另外,排序算法在嵌入式里要选对——数据量小的时候,用插入排序或冒泡排序就够了,别上快速排序,杀鸡用牛刀。

// 中值滤波 - 窗口大小为5
#define MEDIAN_WIN 5

int16_t MedianFilter_Process(int16_t *buf, int16_t new_val) {
    int16_t temp[MEDIAN_WIN];
    int16_t i, j, tmp;
    
    // 数据平移:丢掉最旧,加入最新
    for(i = 0; i < MEDIAN_WIN - 1; i++) {
        buf[i] = buf[i + 1];
    }
    buf[MEDIAN_WIN - 1] = new_val;
    
    // 拷贝到临时数组排序
    for(i = 0; i < MEDIAN_WIN; i++) {
        temp[i] = buf[i];
    }
    
    // 冒泡排序(窗口小,够用)
    for(i = 0; i < MEDIAN_WIN - 1; i++) {
        for(j = 0; j < MEDIAN_WIN - 1 - i; j++) {
            if(temp[j] > temp[j + 1]) {
                tmp = temp[j];
                temp[j] = temp[j + 1];
                temp[j + 1] = tmp;
            }
        }
    }
    
    // 取中间值(索引2)
    return temp[2];
}

性能对比:中值滤波的计算量主要花在排序上。窗口5时,每次最多10次比较;窗口3时只要3次比较。所以除非必要,别把窗口设太大。

4.3 一阶低通滤波:实时性最好的选择

前面两种滤波都需要缓存多个数据。如果系统对实时性要求很高,比如电流环控制,等不起那N个采样周期,怎么办?

一阶低通滤波就是为这种场景设计的。它不需要缓存历史数据,只靠一个递推公式就能平滑信号。公式长这样:

y[n] = α · x[n] + (1 - α) · y[n-1]

其中α是滤波系数,范围0~1。α越大,滤波越弱,响应越快;α越小,滤波越强,响应越慢。说白了,这就是在"相信新数据"和"相信历史趋势"之间做权衡。

我个人习惯把α设成0.1~0.3之间。对于1kHz的电流环,α=0.2时既能滤掉大部分高频噪声,又不至于让电流响应滞后太多。

// 一阶低通滤波
typedef struct {
    float alpha;    // 滤波系数 0.0 ~ 1.0
    float last_out; // 上一次输出值
} LPF1st_t;

float LPF1st_Process(LPF1st_t *f, float new_val) {
    float out = f->alpha * new_val + (1.0f - f->alpha) * f->last_out;
    f->last_out = out;
    return out;
}

// 初始化
void LPF1st_Init(LPF1st_t *f, float alpha, float init_val) {
    f->alpha = alpha;
    f->last_out = init_val;
}

定点化技巧:如果MCU没有FPU,可以把α用Q15格式表示。比如α=0.2,对应Q15的值为0.2 × 32768 ≈ 6554。计算时用整数乘加,最后右移15位。这样既快又准。

4.4 三种滤波的选型指南

说了这么多,到底该用哪一种?我整理了一个对比表,方便你快速决策:

场景 推荐方法 理由
稳态电流/电压采样 均值滤波 噪声随机分布,平均后效果明显
有脉冲干扰的信号 中值滤波 野点不影响中间值
实时控制环路 一阶低通滤波 无延迟累积,计算量最小
启动瞬间的电流检测 一阶低通 + 限幅 既平滑又防止过冲

避坑指南:我曾经在一个项目里对启动电流同时用了均值滤波和一阶低通,结果信号延迟太大,导致过流保护总是慢半拍。后来只保留了一阶低通,把α调到0.3,问题就解决了。记住:滤波不是越多越好,够用就行。

4.5 实战建议:组合使用效果更佳

实际项目中,我经常把中值滤波和一阶低通组合起来用。先用中值滤波剔除野点,再用一阶低通平滑残余噪声。这样既不怕脉冲干扰,又能保证实时性。

举个具体的例子:在启动抖动消除中,电流采样先过3点中值滤波,再过α=0.15的一阶低通。这个组合我在三个项目里验证过,效果都很稳定。

嗯,滤波算法就聊到这里。记住一句话:没有最好的滤波,只有最合适的滤波。动手试,调参数,看波形,这才是工程师该干的事。