3. 传感器数据采集与预处理:数据采集频率、噪声滤波、时间戳同步

各位同学,咱们今天聊点实在的。传感器数据采集,听起来好像就是读个数,对吧?其实不然。我做了这么多年嵌入式,发现真正让系统跑得稳、跑得准的,往往不是那个最贵的传感器,而是你手里那套数据预处理的本事。

说白了,传感器出来的原始数据,就像刚从泥地里拔出来的萝卜——带着土、带着根须,甚至还有虫子。你不能直接拿去做菜。得先洗、先切、先处理。今天我就把这三步走的心得,掰开了揉碎了讲给你听。

传感器数据采集与预处理 · 知识体系 数据采集频率 噪声滤波 时间戳同步 奈奎斯特采样定理 过采样 vs 欠采样 动态频率调整 均值滤波 中值滤波 卡尔曼滤波 硬件同步(IRQ) 软件时间戳 多传感器对齐

3.1 数据采集频率——不是越快越好

先说说采样频率。很多新手上来就想把采样率拉到最高,觉得数据越多越准。嗯,这个想法其实挺危险的。我见过一个做无人机飞控的哥们,把陀螺仪采样率设到10kHz,结果MCU忙得连控制算法都跑不动了,飞机直接翻了。

为什么会这样?因为采样频率的选择,本质上是个平衡问题。你得考虑三个东西:

  • 信号本身的带宽——根据奈奎斯特采样定理,采样频率至少是信号最高频率的2倍。但实际工程中,我一般取5~10倍,留点余量。
  • MCU的处理能力——你采得快,处理得过来吗?别忘了还有别的任务要跑。
  • 功耗和存储——尤其是电池供电的设备,高频采样就是烧电。

核心原则:采样频率 = 信号最高频率 × (5~10)。别贪心,够用就行。

我个人习惯的做法是:先根据传感器数据手册确定理论采样率,然后在实际系统中用示波器或者逻辑分析仪看一下信号的实际变化速度。比如温度传感器,变化很慢,1Hz就够了。但IMU这种,至少100Hz起步。

3.2 噪声滤波——把脏数据洗干净

数据采回来了,但里面全是噪声。怎么办?滤波。我常用的有三种方法,各有各的脾气。

3.2.1 均值滤波

最简单,也最粗暴。连续采N个值,取平均。适合对付白噪声,但对突发性的尖峰噪声毫无抵抗力。我在项目中遇到过一个问题:一个压力传感器偶尔会蹦出一个异常值,均值滤波直接把整个结果带偏了。后来我换成了中值滤波,问题才解决。

// 均值滤波示例
#define FILTER_WINDOW 10
int buffer[FILTER_WINDOW];
int index = 0;
long sum = 0;

int mean_filter(int new_value) {
    sum -= buffer[index];
    buffer[index] = new_value;
    sum += new_value;
    index = (index + 1) % FILTER_WINDOW;
    return sum / FILTER_WINDOW;
}

3.2.2 中值滤波

对付尖峰噪声的利器。把窗口内的数据排序,取中间那个值。你想想看,一个异常大的值,排序后要么在最左边要么在最右边,根本影响不到中间那个数。

我的经验:中值滤波的窗口大小一般取奇数,3、5、7都行。窗口越大,滤得越干净,但延迟也越大。对于实时性要求高的系统,窗口别超过5。

// 中值滤波(窗口大小5)
int median_filter(int new_value) {
    static int window[5] = {0};
    static int pos = 0;
    int temp[5], i, j;
    
    window[pos] = new_value;
    pos = (pos + 1) % 5;
    
    // 复制并排序
    for(i = 0; i < 5; i++) temp[i] = window[i];
    for(i = 0; i < 4; i++) {
        for(j = i+1; j < 5; j++) {
            if(temp[i] > temp[j]) {
                int t = temp[i];
                temp[i] = temp[j];
                temp[j] = t;
            }
        }
    }
    return temp[2]; // 中间值
}

3.2.3 卡尔曼滤波

这个就高级一点了。卡尔曼滤波不是简单地对数据做平滑,而是根据系统的状态模型和观测模型,动态地估计出最可能的值。说白了,它知道你的系统是怎么运动的,所以能预测下一步的数据大概是多少,然后再用实际测量值来修正这个预测。

我曾经在一个激光测距项目里用过卡尔曼滤波。那个传感器在户外受阳光干扰很大,数据跳得厉害。用了卡尔曼滤波之后,数据曲线平滑得像丝绸一样。嗯,当然代价就是计算量大了点。

// 一维卡尔曼滤波简化版
typedef struct {
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
    float x;  // 估计值
    float P;  // 估计误差协方差
    float K;  // 卡尔曼增益
} KalmanFilter;

void kalman_init(KalmanFilter* kf, float init_x) {
    kf->Q = 0.01;
    kf->R = 0.1;
    kf->x = init_x;
    kf->P = 1.0;
}

float kalman_update(KalmanFilter* 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设得太大,结果滤波器完全跟着噪声跑了,还不如不滤。

3.3 时间戳同步——让数据对齐

最后一个问题,也是很多人容易忽略的:时间戳同步。你想想看,一个系统里可能有温度传感器、压力传感器、加速度计,它们各自在不同的时间点采集数据。如果不做时间同步,你拿到的数据就是错位的,分析出来的结果也是错的。

我遇到过最典型的场景是:一个机器人项目里,IMU和摄像头的数据时间戳差了50ms,结果做视觉惯性里程计的时候,位置估计直接漂了。后来花了整整两天才找到原因。

时间戳同步有两种做法:

  • 硬件同步:用传感器的同步引脚,或者MCU的定时器触发采集。精度最高,但需要硬件支持。
  • 软件时间戳:在中断服务函数里,第一时间读取系统时钟,打上时间戳。精度稍差,但实现简单。

我的建议:如果条件允许,优先用硬件同步。如果只能用软件,记得在中断里第一时间打时间戳,别等处理完数据再打——那会儿已经晚了。

好了,关于传感器数据采集与预处理,我就讲这么多。记住三个关键词:频率要合适、滤波要选对、时间要对齐。把这三点做好了,你的数据质量就能上一个台阶。

避坑指南:我曾经在一个项目中,因为忘记给传感器数据打时间戳,导致后期数据分析完全无法进行。从那以后,我养成了一个习惯:任何传感器数据,采集到的第一件事就是打时间戳,然后再做滤波处理。顺序不能反。

可可精品资料库