4. 时域滤波技术:移动平均滤波、中值滤波、指数加权移动平均(EWMA)、滑动窗口滤波的工程实现

各位同学,咱们今天聊点实在的。时域滤波,说白了就是在时间轴上直接对信号动手脚。你想想看,碰撞传感器送过来的信号,里面掺杂着路面颠簸、车门震动、甚至是你关后备箱那一下的冲击。如果不把这些“杂音”滤掉,安全气囊该爆的时候不爆,不该爆的时候乱爆,那可就出大事了。

我个人习惯把时域滤波比作“信号清洗工”。它不关心信号的频率成分,只看当前时刻附近的数据长什么样。今天咱们就挨个看看,这几位“清洗工”各自有什么绝活。

4.1 移动平均滤波:最简单的“和稀泥”

移动平均滤波,名字听着挺唬人,其实就是把最近N个数据点加起来,再除以N。我刚开始做嵌入式开发时,第一个用的滤波算法就是它。为啥?因为简单啊,不费脑子。

它的数学表达式长这样:

y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N

其中N是窗口大小。N越大,滤波效果越平滑,但延迟也越大。嗯,这里要注意,延迟在安全气囊系统中是个大问题。你滤得太狠,碰撞信号都被抹平了,气囊该不该爆你都判断不出来。

我的经验: 在安全气囊的加速度信号预处理中,我一般把N设在3到5之间。再大就影响响应速度了。你可以把它想象成“慢半拍”的观察者,N越大,它反应越迟钝。

代码实现也很直白:

// 移动平均滤波 - 整型优化版本
#define WINDOW_SIZE 4
static int16_t buffer[WINDOW_SIZE] = {0};
static uint8_t index = 0;
static int32_t sum = 0;

int16_t moving_average(int16_t new_sample) {
    sum -= buffer[index];          // 去掉最旧的数据
    sum += new_sample;             // 加上最新的数据
    buffer[index] = new_sample;    // 更新缓冲区
    index = (index + 1) % WINDOW_SIZE;
    return (int16_t)(sum / WINDOW_SIZE);
}

你看,代码里我用了整型运算,没用浮点数。为什么?因为在MCU上,浮点运算太慢了,而且占资源。安全气囊的ECU可没那么多闲工夫算小数。

避坑指南: 我曾经在一个项目里直接用浮点数做移动平均,结果发现每次滤波要花掉好几百微秒。后来改成整型移位运算(窗口大小取2的幂次),速度直接提升了10倍。记住,嵌入式世界里,能用移位就别用乘除,能用整型就别用浮点。

4.2 中值滤波:专治“毛刺”的狠角色

移动平均滤波怕什么?怕那种突然冒出来的大毛刺。比如传感器线束接触不良,突然蹦出一个离谱的数值。移动平均会把这个毛刺“平均”到结果里,导致输出也跟着跳一下。但中值滤波不会。

中值滤波的原理很简单:把窗口内的数据排序,取中间那个值。你想想看,就算窗口里混进一个异常大的值,只要它排不到中间去,就对结果没影响。

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

int16_t median_filter(int16_t new_sample) {
    static int16_t window[MEDIAN_WINDOW] = {0};
    static uint8_t pos = 0;
    int16_t temp[MEDIAN_WINDOW];
    int16_t i, j, tmp;
    
    // 更新环形缓冲区
    window[pos] = new_sample;
    pos = (pos + 1) % MEDIAN_WINDOW;
    
    // 拷贝到临时数组并排序(冒泡排序,够用就行)
    for(i = 0; i < MEDIAN_WINDOW; i++) {
        temp[i] = window[i];
    }
    for(i = 0; i < MEDIAN_WINDOW - 1; i++) {
        for(j = 0; j < MEDIAN_WINDOW - 1 - i; j++) {
            if(temp[j] > temp[j+1]) {
                tmp = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = tmp;
            }
        }
    }
    return temp[MEDIAN_WINDOW / 2];  // 取中位数
}

我在项目中遇到过一种情况:车辆经过减速带时,加速度信号会突然出现一个尖锐的脉冲。移动平均滤波会让这个脉冲“扩散”开来,导致误判。但中值滤波直接把它当毛刺滤掉了。当然,代价就是计算量大一些,排序嘛,总是要花时间的。

关键点: 中值滤波的窗口大小建议选奇数,这样中位数好取。3点中值滤波对单点毛刺效果很好,5点可以对付连续两个点的异常。再大就没必要了,延迟受不了。

4.3 指数加权移动平均(EWMA):老数据慢慢忘

EWMA这名字听着高级,其实思想特别朴素:越老的数据,权重越小。它不像移动平均那样对所有数据一视同仁,而是给新数据更大的“话语权”。

公式很简单:

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

α是一个介于0和1之间的系数。α越大,滤波器对新数据越敏感,响应越快,但平滑效果差。α越小,平滑效果越好,但响应慢。

我个人习惯把α取成2的负幂次,比如1/4、1/8、1/16。这样在MCU上可以用移位运算代替乘法,效率高得多。

// EWMA滤波 - 使用移位实现
// alpha = 1/8,即右移3位
#define ALPHA_SHIFT 3

int16_t ewma_filter(int16_t new_sample, int16_t prev_output) {
    // y = (new + 7 * prev) / 8
    return (int16_t)(((int32_t)new_sample + 
            ((int32_t)prev_output << ALPHA_SHIFT) - 
            (int32_t)prev_output) >> ALPHA_SHIFT);
}

你看,这里我用了个小技巧:把(1-α)拆成(8-1)/8,然后用移位和加减法实现。为什么这么折腾?因为MCU做乘法太慢了,尤其是16位乘法,动辄几十个时钟周期。而移位和加减法,一个周期就搞定。

我的建议: EWMA特别适合做“趋势跟踪”。在安全气囊算法中,我常用它来估计信号的“基线”水平。比如车辆正常行驶时,加速度信号在0g附近波动,EWMA可以平滑地跟踪这个基线,一旦碰撞发生,信号偏离基线,就能快速检测到。

4.4 滑动窗口滤波:灵活但费内存

滑动窗口滤波,其实是个“筐”,什么都能往里装。你可以把它理解成一个固定长度的数据队列,每次有新数据进来,最老的数据就被挤出去。然后你对这个窗口里的数据做任意操作——求平均、求中值、求方差,都行。

它的好处是灵活。你可以根据信号特点,动态切换窗口内的处理算法。比如正常行驶时用移动平均,检测到疑似碰撞时切换成中值滤波。

// 滑动窗口框架 - 支持多种滤波策略
typedef struct {
    int16_t buffer[64];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
    uint8_t size;
} SlidingWindow;

// 初始化
void sw_init(SlidingWindow *sw, uint8_t size) {
    sw->head = 0;
    sw->tail = 0;
    sw->count = 0;
    sw->size = size;
}

// 添加数据
void sw_push(SlidingWindow *sw, int16_t data) {
    sw->buffer[sw->head] = data;
    sw->head = (sw->head + 1) % sw->size;
    if(sw->count < sw->size) {
        sw->count++;
    } else {
        sw->tail = (sw->tail + 1) % sw->size;
    }
}

// 获取窗口内数据的平均值
int16_t sw_mean(SlidingWindow *sw) {
    int32_t sum = 0;
    uint8_t i;
    for(i = 0; i < sw->count; i++) {
        sum += sw->buffer[(sw->tail + i) % sw->size];
    }
    return (int16_t)(sum / sw->count);
}
避坑指南: 我曾经在一个项目里把滑动窗口设得太大(128点),结果RAM直接爆了。要知道,安全气囊ECU的RAM通常只有几KB,一个窗口就吃掉几百字节,其他功能还怎么跑?所以,窗口大小一定要精打细算。我的经验是:加速度信号用16-32点窗口就够了,再多就是浪费。

4.5 四种滤波器的对比与选型

说了这么多,到底该用哪个?我整理了一张表,方便你对照着选:

滤波器类型 计算量 内存占用 延迟 抗毛刺能力 适用场景
移动平均 平滑噪声,基线估计
中值滤波 中高 去除脉冲噪声、毛刺
EWMA 极低 极低 可调 实时跟踪,资源受限场景
滑动窗口 取决于算法 取决于窗口 取决于算法 需要灵活切换策略的场景

你看,没有哪个滤波器是万能的。移动平均省资源但怕毛刺,中值滤波抗干扰但计算慢,EWMA轻量但平滑效果有限,滑动窗口灵活但吃内存。实际工程中,我经常把它们组合起来用。比如先用中值滤波把明显的毛刺干掉,再用EWMA做平滑,最后用滑动窗口做特征提取。

好了,时域滤波这块咱们就聊到这儿。下节课咱们进入频域,看看傅里叶变换在碰撞信号处理中是怎么玩的。记住,滤波不是目的,目的是让信号干净到足以支撑正确的决策。安全气囊的决策,容不得半点马虎。