第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够用,可以预分配两个数组——一个做环形缓冲,一个做排序用。这样能避免频繁的内存操作。

测试方法也很简单。我一般用信号发生器模拟轨压信号,叠加一个方波脉冲。然后用示波器同时看原始信号和滤波后的信号。如果滤波后的波形在脉冲到来时没有明显跳变,说明算法生效了。

嗯,这里要注意:测试时一定要覆盖边界情况。比如窗口刚填满时的输出是否正确?连续多个脉冲时表现如何?这些我在项目里都踩过坑。

最后说一句,中位值滤波不是万能的。它适合处理稀疏的脉冲噪声,但对于高斯噪声效果一般。下一章我们会聊到滑动平均滤波,两者结合使用效果更好。

中位值滤波法核心逻辑 原始采样值 环形缓冲区 排序(升序) 中位值输出 窗口大小N N=3(快速) N=5(推荐) N=7(强滤波) 核心思想:排序取中 → 天然免疫单次脉冲干扰

好了,中位值滤波法就讲到这里。代码可以直接移植到你的STM32、Infineon或者NXP平台上。记得根据实际采样率和干扰情况调整窗口大小。