传感器数据采集过程中,那些突然出现的异常跳变值就像音乐会上的杂音,让整个数据流变得刺耳难听。去年我在一个工业温度监控项目里就遇到过这种情况——PID控制算法因为几个突变的温度读数直接失控,差点造成产线停机。这种数据毛刺通常由电磁干扰、传感器瞬态响应或传输链路噪声引起,特点是持续时间短但幅值大。
传统解决思路有两种极端:要么直接忽略(可能丢失真实突变),要么全盘接收(导致系统震荡)。而滤波算法的价值就在于找到平衡点——既平滑噪声又保留真实变化趋势。在嵌入式开发中,算数平均滤波因其实现简单、计算量小,成为应对这类问题的首选方案。
关键认知:滤波不是要消除所有波动,而是区分"信号"与"噪声"的统计特征。温度这类缓变信号适合平均滤波,而振动信号这种高频变化则需要其他方法。
用一个长度为N的队列存储历史数据,新数据入队时,最旧数据出队,始终保持队列最新状态。滤波输出值就是当前队列所有数据的算术平均值:
c复制#define FILTER_WIN_SIZE 5
float filter_buf[FILTER_WIN_SIZE];
float moving_average(float new_val) {
static int index = 0;
float sum = 0;
filter_buf[index++] = new_val;
if(index >= FILTER_WIN_SIZE) index = 0;
for(int i=0; i<FILTER_WIN_SIZE; i++) {
sum += filter_buf[i];
}
return sum / FILTER_WIN_SIZE;
}
这个基础版本在STM32F103上测试,执行时间仅12μs(72MHz主频),内存占用20字节(float为4字节时),非常适合资源受限的MCU。
窗口大小N的选取需要权衡:
在工业温度监测中,我通常这样确定N值:
基础算法对所有历史数据平等对待,但实际上越新的数据参考价值越大。改进的加权平均滤波:
c复制float weighted_average(float new_val) {
static float weights[] = {0.1, 0.15, 0.2, 0.25, 0.3}; // 权重递增
/* 其余部分同基础版本 */
for(int i=0; i<FILTER_WIN_SIZE; i++) {
sum += filter_buf[i] * weights[i];
}
return sum; // 注意权重和需为1
}
实测显示,这种方法在突变量测时响应速度提升40%,但计算量略有增加。
针对偶尔出现的超大毛刺,可结合阈值判断:
c复制float filtered = basic_average(new_val);
if(fabs(new_val - filtered) > threshold) {
// 视为毛刺,用上次滤波值替代
return last_filtered_value;
} else {
return filtered;
}
阈值threshold一般取历史平均偏差的3倍(根据3σ原则)。
在RAM紧张的MCU中,可以用环形缓冲区+累加变量避免重复计算:
c复制float sum = 0;
float filter_buf[N];
int index = 0;
float optimized_average(float new_val) {
sum = sum - filter_buf[index] + new_val;
filter_buf[index] = new_val;
index = (index + 1) % N;
return sum / N;
}
这种方法将计算复杂度从O(N)降到O(1),在N=10时速度提升3倍。
对于不支持硬件浮点的MCU(如STM8),改用Q格式定点数:
c复制typedef int32_t q16_t; // Q16.16格式
#define Q_SHIFT 16
q16_t fixed_average(q16_t new_val) {
static q16_t sum = 0;
static q16_t buf[N];
static int idx = 0;
sum = sum - buf[idx] + new_val;
buf[idx] = new_val;
idx = (idx + 1) % N;
return sum / N; // 自动右移Q_SHIFT位
}
曾在一个电机转速监测项目中踩坑:采样频率1kHz,但窗口大小N=50,导致:
早期版本使用uint16_t累加N个uint16_t值时,当N>16时就可能溢出。解决方案:
系统上电时,缓冲区可能包含随机值。解决方法:
c复制if(init_cnt < N) {
filter_buf[init_cnt++] = new_val;
return new_val; // 初始阶段直接返回原始值
}
在STM32F407上测试不同实现方式的性能(N=10,1万次迭代):
| 实现方式 | 执行时间(μs) | RAM占用(字节) | 效果评分 |
|---|---|---|---|
| 基础浮点版 | 158 | 40 | 7.2 |
| 优化浮点版 | 52 | 40 | 7.2 |
| 定点数Q16版 | 38 | 20 | 6.8 |
| 带权重优化版 | 217 | 48 | 8.1 |
| 去极值混合版 | 185 | 44 | 8.6 |
效果评分依据:噪声抑制能力(40%) + 响应速度(30%) + 资源占用(30%)
先通过中值滤波去除脉冲噪声,再进行平均滤波:
c复制float hybrid_filter(float new_val) {
float median = median_filter(new_val); // 3点中值
return moving_average(median);
}
这种组合在存在偶发大毛刺的场景下,效果提升显著。
动态调整窗口大小:当数据波动大时减小N,稳定时增大N。通过计算窗口内数据的变异系数(标准差/均值)来判断稳定性:
c复制float cv = calculate_cv(filter_buf, N);
if(cv > 0.1) N = max(3, N-1);
else N = min(20, N+1);
根据五年来的项目经验,总结典型应用场景的最佳配置:
| 应用场景 | 推荐N值 | 采样频率 | 补充建议 |
|---|---|---|---|
| 工业温度监测 | 5-7 | 0.2-1Hz | 配合硬件低通滤波 |
| 电机转速检测 | 3-5 | 100Hz+ | 优先选用优化浮点版 |
| 电池电压采集 | 10-15 | 10Hz | 建议增加去极值判断 |
| 环境光传感器 | 7-10 | 5Hz | 可尝试加权平均 |
| 车载加速度检测 | 3 | 50Hz | 需结合IIR滤波 |
在最终实施时,建议先用SD卡存储原始数据,在PC上用Python分析数据特征后,再确定最佳滤波参数。Matplotlib绘制原始与滤波后的数据对比图,能直观评估效果:
python复制import matplotlib.pyplot as plt
plt.plot(raw_data, label='Raw')
plt.plot(filtered_data, label='Filtered')
plt.legend()
plt.show()