4. 数据采集与滤波:ADC采样配置、滑动平均滤波、中值滤波、卡尔曼滤波入门
各位同学,今天我们来聊聊嵌入式系统里一个绕不开的话题——数据采集与滤波。说白了,就是怎么把传感器那点微弱的模拟信号,变成我们单片机里能用的、干净的数字量。
我做了这么多年嵌入式,见过太多项目栽在“数据脏”这个坑里。你想想看,传感器明明没动,ADC读出来的数值却像心电图一样上下乱跳,这谁受得了?所以,滤波不是锦上添花,而是刚需。
核心要点:ADC采样是基础,滤波是灵魂。没有滤波的原始数据,就像没洗过的土豆,你敢直接下锅吗?
4.1 ADC采样配置——第一步要稳
ADC配置,我个人的习惯是“先看手册,再动手”。每个MCU的ADC模块都有点小脾气,你得顺着它来。
配置时,有几个关键参数必须盯紧:
- 采样时间:别太短。给ADC电容充电的时间不够,数据肯定不准。我一般设到最大值的70%左右。
- 分辨率:12位还是10位?看需求。精度要求高就上12位,但转换速度会慢一点。
- 参考电压:这个最容易忽略。内部参考还是外部参考?外部参考电压的纹波会直接耦合到结果里。
// 以STM32为例,配置ADC1
void ADC_Config(void) {
ADC_InitTypeDef ADC_InitStruct;
GPIO_InitTypeDef GPIO_InitStruct;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
// 配置PA0为模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AN;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// ADC配置
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStruct);
// 配置采样时间——我习惯设长一点
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
// 使能ADC
ADC_Cmd(ADC1, ENABLE);
// 校准
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
// 开始转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
我的小技巧:配置完ADC后,先读几次数据扔掉。这叫“预热”,让ADC内部稳定下来。我曾经因为这个习惯,救了一个量产项目的良率问题。
4.2 滑动平均滤波——简单但有效
滑动平均滤波,说白了就是“取最近N个值的平均”。它对付随机噪声特别好用,比如热噪声、量化噪声。
为什么不用普通平均?因为普通平均要等N个数据都采完才能算一次,实时性太差。滑动平均是每来一个新数据,就扔掉最老的那个,重新算一次平均。这样每个采样点都能得到一个滤波后的值。
#define FILTER_WINDOW_SIZE 10
static uint16_t filter_buffer[FILTER_WINDOW_SIZE];
static uint8_t filter_index = 0;
static uint32_t filter_sum = 0;
uint16_t MovingAverageFilter(uint16_t new_sample) {
// 减去最老的数据
filter_sum -= filter_buffer[filter_index];
// 存入新数据
filter_buffer[filter_index] = new_sample;
// 加上新数据
filter_sum += new_sample;
// 更新索引
filter_index = (filter_index + 1) % FILTER_WINDOW_SIZE;
// 返回平均值
return (uint16_t)(filter_sum / FILTER_WINDOW_SIZE);
}
注意:窗口大小N的选择很关键。N太小,滤波效果差;N太大,响应变慢。我一般从10开始试,根据实际波形调整。曾经有个项目,窗口设到50,结果系统反应慢得像蜗牛,被客户投诉了。
4.3 中值滤波——对付“野值”的利器
中值滤波,就是取一组数据中间的那个值。它对脉冲噪声(比如突然的尖峰干扰)特别有效。
你想想看,如果传感器线被踢了一脚,突然冒出一个离谱的数值。滑动平均会被这个值带偏,但中值滤波直接把它扔到排序的末尾或开头,根本不影响结果。
// 冒泡排序取中值——简单粗暴
uint16_t MedianFilter(uint16_t *data, uint8_t len) {
uint8_t i, j;
uint16_t temp;
uint16_t sorted[FILTER_WINDOW_SIZE];
// 复制数据
for(i = 0; i < len; i++) {
sorted[i] = data[i];
}
// 冒泡排序
for(i = 0; i < len - 1; i++) {
for(j = 0; j < len - 1 - i; j++) {
if(sorted[j] > sorted[j + 1]) {
temp = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = temp;
}
}
}
// 返回中值
return sorted[len / 2];
}
经验之谈:中值滤波的窗口一般取奇数,比如3、5、7。取3时效果有限,取7以上计算量就上来了。我常用5,性价比最高。
4.4 卡尔曼滤波入门——有点难,但值得学
卡尔曼滤波,听起来高大上,其实它就是个“预测+修正”的循环。它不像滑动平均那样只看历史数据,而是结合系统的物理模型来做预测。
嗯,这里要注意,卡尔曼滤波不是万能的。它假设噪声是高斯分布,如果你的噪声不是这样,效果会打折扣。
我刚开始学的时候也被公式吓到了,后来发现核心就五个公式:
- 预测状态:根据上一时刻的状态,猜当前时刻的状态
- 预测协方差:猜的误差有多大
- 计算卡尔曼增益:相信测量值还是相信预测值?
- 更新状态:用测量值修正预测值
- 更新协方差:修正后的误差有多大
// 一维卡尔曼滤波——简单版本
typedef struct {
float Q; // 过程噪声协方差
float R; // 测量噪声协方差
float P; // 估计误差协方差
float K; // 卡尔曼增益
float X; // 状态估计值
} KalmanFilter_t;
void Kalman_Init(KalmanFilter_t *kf, float init_value) {
kf->Q = 0.01; // 我一般从0.01开始调
kf->R = 0.1; // 根据ADC的噪声水平调整
kf->P = 1.0; // 初始估计误差
kf->X = init_value;
}
float Kalman_Update(KalmanFilter_t *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 - kf->K) * kf->P;
return kf->X;
}
避坑指南:我曾经在一个项目中,直接把卡尔曼滤波的参数从网上复制过来,结果数据发散得一塌糊涂。后来才明白,Q和R必须根据实际系统调。Q越大,越相信测量值;R越大,越相信预测值。没有万能参数,必须亲手调。
4.5 三种滤波怎么选?
我给大家一个简单的选择思路:
| 场景 | 推荐滤波 | 理由 |
|---|---|---|
| 随机噪声(白噪声) | 滑动平均 | 简单、计算量小、效果好 |
| 脉冲干扰(尖峰) | 中值滤波 | 对野值免疫,不影响正常数据 |
| 需要平滑跟踪 | 卡尔曼滤波 | 预测能力强,滞后小 |
| 资源受限 | 滑动平均 | 只需要加法和除法,RAM占用小 |
当然,实际项目中经常是组合使用。比如先中值滤波去掉野值,再滑动平均平滑数据。我做过一个红外测温项目,就是中值+滑动平均的组合,效果出奇的好。
总结一下:ADC采样是基础,滤波是灵魂。滑动平均保平滑,中值滤波保鲁棒,卡尔曼滤波保精度。没有最好的滤波,只有最合适的滤波。
好了,这一章的内容就到这里。数据采集和滤波是嵌入式开发的看家本领,多练练,你也能成为高手。