第1章:中位值滤波法 — 原理与实现
各位工程师朋友,咱们今天聊一个很实在的话题——中位值滤波法。
说实话,轨压传感器这玩意儿,在柴油发动机ECU里是个关键角色。它输出的信号直接决定了喷油量的计算。但问题来了,发动机舱里电磁干扰多,传感器信号上经常会出现一些莫名其妙的毛刺。我最早做轨压标定时,就被这些毛刺坑过好几次。
中位值滤波法,说白了就是「取中间那个数」。你采集N个样本,排序后取中间值作为有效输出。这个方法对付脉冲噪声特别管用。
1.1 原理:为什么取中间值有效?
想象一下,你连续采了5个轨压值:
原始数据:120.1, 120.3, 135.7, 120.2, 120.0
看到没?第三个点135.7明显是个异常值。可能是点火瞬间的电磁耦合,也可能是传感器线束抖动了一下。
如果用平均值法:(120.1+120.3+135.7+120.2+120.0)/5 = 123.26。这个结果被污染了。
如果用中位值法:先排序 → 120.0, 120.1, 120.2, 120.3, 135.7 → 取中间值120.2。嗯,这才是真实压力。
为什么会这样?因为中位值对离群点不敏感。你想想看,无论异常值多大或多小,只要它不占据中间位置,就不会影响最终结果。这是中位值滤波最核心的优势。
核心要点:中位值滤波的本质是「用排序代替平均」,它天然免疫单次大幅度的脉冲干扰。
1.2 采样窗口大小选择
窗口大小,也就是一次取多少个样本,这个参数很关键。我个人的经验是:
| 窗口大小 | 适用场景 | 优缺点 |
|---|---|---|
| 3 | 快速响应,干扰较少 | 延迟小,但抗干扰能力弱 |
| 5 | 通用场景,平衡之选 | 我项目中用得最多的参数 |
| 7 | 干扰严重,响应要求不高 | 滤波效果好,但延迟明显 |
| 9及以上 | 极恶劣环境 | 慎用,实时性会受影响 |
这里有个避坑指南:我曾经在一个项目中把窗口设成11,结果轨压响应慢了将近100ms。发动机在急加速时,ECU读到的是「历史数据」,喷油量计算严重滞后。那次调试花了我整整两天。
我的建议是:从5开始试。如果示波器上还能看到毛刺,再增加到7。如果响应速度不够,降到3。别一上来就搞大窗口。
1.3 对脉冲噪声的抑制效果
脉冲噪声,就是那种突然冒出来的尖峰。中位值滤波对它的抑制效果,我用一个实际测试数据来说明:
原始信号(含脉冲噪声):
120.0, 120.1, 120.3, 180.5, 120.2, 120.1, 120.0, 200.0, 120.3, 120.1
中位值滤波后(窗口=5):
120.1, 120.2, 120.2, 120.2, 120.1, 120.1, 120.1, 120.1, 120.1, 120.1
看到没?180.5和200.0这两个大毛刺,被完全消除了。输出曲线非常平滑。
但要注意一点:如果脉冲噪声连续出现多次,比如连续3个点都是异常值,窗口=5时中间值可能就被污染了。这时候你需要增大窗口,或者考虑结合其他滤波方法。
警告:中位值滤波无法抑制连续多个脉冲噪声。如果干扰源是周期性的,建议先用硬件滤波,再用软件滤波。
1.4 代码实现与测试
好了,理论说完了,咱们直接上代码。这是我个人比较喜欢的一种实现方式——用插入排序维护一个有序窗口,避免每次全排序:
#include <stdint.h>
#define MEDIAN_WINDOW_SIZE 5
static uint16_t buffer[MEDIAN_WINDOW_SIZE];
static uint8_t index = 0;
static uint8_t is_full = 0;
// 插入排序辅助函数
static void insert_sorted(uint16_t value) {
int8_t i;
for (i = index - 1; i >= 0; i--) {
if (buffer[i] > value) {
buffer[i + 1] = buffer[i];
} else {
break;
}
}
buffer[i + 1] = value;
}
uint16_t median_filter(uint16_t new_sample) {
uint16_t sorted[MEDIAN_WINDOW_SIZE];
uint8_t i;
// 1. 存入环形缓冲区
buffer[index] = new_sample;
index = (index + 1) % MEDIAN_WINDOW_SIZE;
if (index == 0) is_full = 1;
// 2. 如果窗口未满,直接返回原始值
if (!is_full) return new_sample;
// 3. 复制到临时数组并排序
for (i = 0; i < MEDIAN_WINDOW_SIZE; i++) {
sorted[i] = buffer[i];
}
// 4. 冒泡排序(窗口小,效率可接受)
for (i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
for (uint8_t j = 0; j < MEDIAN_WINDOW_SIZE - 1 - i; j++) {
if (sorted[j] > sorted[j + 1]) {
uint16_t temp = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = temp;
}
}
}
// 5. 返回中位值
return sorted[MEDIAN_WINDOW_SIZE / 2];
}
小技巧:对于嵌入式MCU,如果RAM够用,可以预分配两个数组——一个做环形缓冲,一个做排序用。这样能避免频繁的内存操作。
测试方法也很简单。我一般用信号发生器模拟轨压信号,叠加一个方波脉冲。然后用示波器同时看原始信号和滤波后的信号。如果滤波后的波形在脉冲到来时没有明显跳变,说明算法生效了。
嗯,这里要注意:测试时一定要覆盖边界情况。比如窗口刚填满时的输出是否正确?连续多个脉冲时表现如何?这些我在项目里都踩过坑。
最后说一句,中位值滤波不是万能的。它适合处理稀疏的脉冲噪声,但对于高斯噪声效果一般。下一章我们会聊到滑动平均滤波,两者结合使用效果更好。
好了,中位值滤波法就讲到这里。代码可以直接移植到你的STM32、Infineon或者NXP平台上。记得根据实际采样率和干扰情况调整窗口大小。