4、软件滤波消除抖动:均值滤波、中值滤波、一阶低通滤波的原理与代码实现
启动抖动的本质,说白了就是电流或速度信号在启动瞬间来回跳变。硬件上我们可以加RC滤波,但成本、空间、可调性都不太理想。我个人习惯在软件层面做滤波,灵活、零成本、还能随时调参数。
这一章我们重点聊三种最常用的软件滤波方法:均值滤波、中值滤波、一阶低通滤波。它们各有各的脾气,用对了地方就是神器,用错了反而添乱。
核心观点:没有万能的滤波算法,只有最合适的场景。均值滤波适合平滑噪声,中值滤波擅长剔除野点,一阶低通滤波则是在实时性和平滑度之间找平衡。
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的一阶低通。这个组合我在三个项目里验证过,效果都很稳定。
嗯,滤波算法就聊到这里。记住一句话:没有最好的滤波,只有最合适的滤波。动手试,调参数,看波形,这才是工程师该干的事。