4. 时域滤波技术:移动平均滤波、中值滤波、指数加权移动平均(EWMA)、滑动窗口滤波的工程实现
各位同学,咱们今天聊点实在的。时域滤波,说白了就是在时间轴上直接对信号动手脚。你想想看,碰撞传感器送过来的信号,里面掺杂着路面颠簸、车门震动、甚至是你关后备箱那一下的冲击。如果不把这些“杂音”滤掉,安全气囊该爆的时候不爆,不该爆的时候乱爆,那可就出大事了。
我个人习惯把时域滤波比作“信号清洗工”。它不关心信号的频率成分,只看当前时刻附近的数据长什么样。今天咱们就挨个看看,这几位“清洗工”各自有什么绝活。
4.1 移动平均滤波:最简单的“和稀泥”
移动平均滤波,名字听着挺唬人,其实就是把最近N个数据点加起来,再除以N。我刚开始做嵌入式开发时,第一个用的滤波算法就是它。为啥?因为简单啊,不费脑子。
它的数学表达式长这样:
y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N
其中N是窗口大小。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可没那么多闲工夫算小数。
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]; // 取中位数
}
我在项目中遇到过一种情况:车辆经过减速带时,加速度信号会突然出现一个尖锐的脉冲。移动平均滤波会让这个脉冲“扩散”开来,导致误判。但中值滤波直接把它当毛刺滤掉了。当然,代价就是计算量大一些,排序嘛,总是要花时间的。
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位乘法,动辄几十个时钟周期。而移位和加减法,一个周期就搞定。
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);
}
4.5 四种滤波器的对比与选型
说了这么多,到底该用哪个?我整理了一张表,方便你对照着选:
| 滤波器类型 | 计算量 | 内存占用 | 延迟 | 抗毛刺能力 | 适用场景 |
|---|---|---|---|---|---|
| 移动平均 | 低 | 低 | 中 | 弱 | 平滑噪声,基线估计 |
| 中值滤波 | 中高 | 中 | 中 | 强 | 去除脉冲噪声、毛刺 |
| EWMA | 极低 | 极低 | 可调 | 中 | 实时跟踪,资源受限场景 |
| 滑动窗口 | 取决于算法 | 高 | 取决于窗口 | 取决于算法 | 需要灵活切换策略的场景 |
你看,没有哪个滤波器是万能的。移动平均省资源但怕毛刺,中值滤波抗干扰但计算慢,EWMA轻量但平滑效果有限,滑动窗口灵活但吃内存。实际工程中,我经常把它们组合起来用。比如先用中值滤波把明显的毛刺干掉,再用EWMA做平滑,最后用滑动窗口做特征提取。
好了,时域滤波这块咱们就聊到这儿。下节课咱们进入频域,看看傅里叶变换在碰撞信号处理中是怎么玩的。记住,滤波不是目的,目的是让信号干净到足以支撑正确的决策。安全气囊的决策,容不得半点马虎。