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 卡尔曼滤波入门——有点难,但值得学

卡尔曼滤波,听起来高大上,其实它就是个“预测+修正”的循环。它不像滑动平均那样只看历史数据,而是结合系统的物理模型来做预测。

嗯,这里要注意,卡尔曼滤波不是万能的。它假设噪声是高斯分布,如果你的噪声不是这样,效果会打折扣。

我刚开始学的时候也被公式吓到了,后来发现核心就五个公式:

  1. 预测状态:根据上一时刻的状态,猜当前时刻的状态
  2. 预测协方差:猜的误差有多大
  3. 计算卡尔曼增益:相信测量值还是相信预测值?
  4. 更新状态:用测量值修正预测值
  5. 更新协方差:修正后的误差有多大
// 一维卡尔曼滤波——简单版本
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采样是基础,滤波是灵魂。滑动平均保平滑,中值滤波保鲁棒,卡尔曼滤波保精度。没有最好的滤波,只有最合适的滤波。

数据采集与滤波知识体系 数据采集与滤波 ADC采样配置 采样时间设置 分辨率选择 参考电压配置 滑动平均滤波 对付随机噪声 中值滤波 对付脉冲干扰 卡尔曼滤波 预测+修正 没有最好的滤波,只有最合适的滤波

好了,这一章的内容就到这里。数据采集和滤波是嵌入式开发的看家本领,多练练,你也能成为高手。

专注资料整理