4. 数字滤波算法:移动平均、中值、卡尔曼入门、限幅滤波
传感器数据进了ADC,你以为就干净了?
我见过太多工程师,花大价钱买高精度传感器,结果被一个50Hz工频干扰搞得焦头烂额。硬件滤波固然重要,但数字滤波才是真正的“最后一公里”。
这一章,咱们就把四种最常用的数字滤波算法掰开揉碎。移动平均、中值滤波、限幅滤波,还有那个听起来高大上的卡尔曼滤波。我会告诉你它们各自适合什么场景,以及在STM32上怎么写、怎么选。
核心观点:没有最好的滤波算法,只有最合适的。选错了,再好的算法也是白搭。
4.1 移动平均滤波:最朴素的降噪利器
移动平均滤波的原理,说白了就是“取最近N个值的平均数”。你想想看,随机噪声的均值是零,多取几次平均,噪声自然就被抵消了。
适用场景:温度、湿度、光照等变化缓慢的模拟量。我做过一个温控项目,用移动平均滤波后,温度波动从±1.5℃降到了±0.3℃。
我的习惯:N值一般取4、8、16这样的2的幂次。为什么?因为做除法时可以用移位操作,STM32上快很多。
// 移动平均滤波 - STM32实现
#define FILTER_N 8
static uint16_t buffer[FILTER_N] = {0};
static uint8_t index = 0;
static uint32_t sum = 0;
uint16_t moving_average(uint16_t new_sample) {
sum -= buffer[index]; // 减去最旧的数据
sum += new_sample; // 加上最新数据
buffer[index] = new_sample; // 更新缓冲区
index = (index + 1) & (FILTER_N - 1); // 循环索引
return (uint16_t)(sum >> 3); // 除以8,用移位实现
}
这段代码里有个小技巧:index = (index + 1) & (FILTER_N - 1)。当N是2的幂时,位与操作比取模快得多。我在STM32F103上实测过,快了将近5倍。
注意:移动平均滤波会引入延迟。N越大,延迟越大。如果传感器数据变化很快(比如加速度计),就别用太大的N值。
4.2 中值滤波:专治“野值”和脉冲干扰
中值滤波的思路更直接:把最近N个数据排序,取中间那个值。这样一来,哪怕有一个数据是1000,其他都是100左右,中值还是100,那个野值直接被“开除”了。
适用场景:有偶发性脉冲干扰的信号,比如按键检测、红外避障传感器。我记得有一次做激光测距,偶尔会冒出个离谱的数值,用中值滤波后,问题迎刃而解。
// 中值滤波 - STM32实现(N=5,冒泡排序)
#define MEDIAN_N 5
uint16_t median_filter(uint16_t new_sample) {
static uint16_t buf[MEDIAN_N] = {0};
static uint8_t count = 0;
uint16_t temp[MEDIAN_N];
uint8_t i, j;
// 更新环形缓冲区
buf[count] = new_sample;
count = (count + 1) % MEDIAN_N;
// 拷贝到临时数组并排序
for(i = 0; i < MEDIAN_N; i++) temp[i] = buf[i];
// 冒泡排序(N小,够用)
for(i = 0; i < MEDIAN_N - 1; i++) {
for(j = 0; j < MEDIAN_N - 1 - i; j++) {
if(temp[j] > temp[j+1]) {
uint16_t t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
return temp[MEDIAN_N / 2]; // 取中值
}
这里我用了冒泡排序,因为N=5时排序次数很少。如果N比较大(比如9或11),建议换成插入排序,效率更高。
避坑指南:我曾经在一个项目中把N设成了15,结果每次滤波都要排序,CPU占用率飙升。后来改成N=5,效果差不多,CPU占用降了70%。记住:中值滤波的N值,够用就行。
4.3 限幅滤波:最简单的“防抖”方案
限幅滤波的逻辑特别简单:如果当前采样值和上次值的差值超过某个阈值,就丢弃当前值,用上次值代替。说白了,就是“变化太快我不信”。
适用场景:传感器偶尔出现跳变毛刺的场景,比如电机启动时的电磁干扰、继电器吸合瞬间的尖峰。
// 限幅滤波 - STM32实现
#define LIMIT_THRESHOLD 50 // 最大允许变化量
uint16_t limit_filter(uint16_t new_sample) {
static uint16_t last_value = 0;
uint16_t diff;
if(new_sample > last_value) {
diff = new_sample - last_value;
} else {
diff = last_value - new_sample;
}
if(diff <= LIMIT_THRESHOLD) {
last_value = new_sample; // 变化在允许范围内,更新
}
// 否则保持上次值不变
return last_value;
}
阈值怎么设?我一般先看传感器正常情况下的最大变化率,然后留出20%-30%的余量。比如温度传感器每秒最多变化2℃,那就设阈值为3℃对应的ADC值。
注意:限幅滤波不能消除周期性噪声,只能对付偶发毛刺。如果传感器信号本身变化就很快(比如电机转速),阈值设小了会把正常信号也滤掉。
4.4 卡尔曼滤波入门:看起来难,用起来香
卡尔曼滤波,很多人一听就觉得是“天书”。其实你把它当成一个“会自我学习的滤波器”就行。它根据上一刻的估计值和当前的测量值,动态计算出一个最优估计值。
我刚开始学的时候也被公式吓到了,后来发现,在嵌入式里只用一维卡尔曼滤波就够了。说白了,就是两个公式来回算:预测、更新。
// 一维卡尔曼滤波 - STM32实现
typedef struct {
float Q; // 过程噪声协方差
float R; // 测量噪声协方差
float P; // 估计误差协方差
float K; // 卡尔曼增益
float X; // 滤波后的值
} Kalman_Struct;
void Kalman_Init(Kalman_Struct *kf, float init_val) {
kf->Q = 0.01; // 调参关键:Q越小越平滑
kf->R = 0.1; // 调参关键:R越大越信任预测
kf->P = 1.0;
kf->X = init_val;
}
float Kalman_Update(Kalman_Struct *kf, float measurement) {
// 预测阶段
kf->P = kf->P + kf->Q;
// 更新阶段
kf->K = kf->P / (kf->P + kf->R);
kf->X = kf->X + kf->K * (measurement - kf->X);
kf->P = (1.0 - kf->K) * kf->P;
return kf->X;
}
这段代码里,Q和R是两个关键参数。Q越小,滤波结果越平滑;R越大,越不相信测量值。我调参时有个笨办法:先固定R,从小到大调Q,看波形什么时候最顺眼。
我的经验:卡尔曼滤波在STM32F103上跑一维版本,一次更新大约需要5-10微秒。对于100Hz以下的采样率,完全够用。但如果是多维卡尔曼(比如MPU6050的六轴融合),建议用F4或H7系列。
4.5 性能对比:到底该用哪个?
四种算法各有千秋,我整理了一张对比表,方便你选型时参考。
| 算法 | RAM占用 | CPU占用 | 延迟 | 抗脉冲 | 抗随机噪声 | 适用场景 |
|---|---|---|---|---|---|---|
| 移动平均 | 低 (N×2字节) | 极低 | N/2个采样周期 | 差 | 优 | 温度、湿度、光照 |
| 中值滤波 | 中 (N×2字节) | 低 | N/2个采样周期 | 优 | 中 | 红外、超声波、按键 |
| 限幅滤波 | 极低 (2字节) | 极低 | 1个采样周期 | 优 | 差 | 电机电流、继电器信号 |
| 卡尔曼滤波 | 高 (多个float) | 中 | 1个采样周期 | 优 | 优 | 加速度计、陀螺仪、GPS |
从表里能看出来:
- 资源紧张、噪声随机 → 移动平均滤波
- 有脉冲干扰、不差那点RAM → 中值滤波
- 偶尔跳变、要求实时性 → 限幅滤波
- 既要平滑又要低延迟、CPU有余量 → 卡尔曼滤波
我的建议:别一上来就上卡尔曼。先试试移动平均,不行再加中值,还不行再考虑卡尔曼。很多时候,组合滤波的效果比单一算法好得多。比如“限幅+移动平均”就是一对黄金搭档。
嗯,四种算法都讲完了。代码可以直接复制到你的STM32工程里用。记住,滤波算法是工具,不是目的。你的目标是让传感器数据“干净可用”,而不是追求算法本身有多炫酷。