4. 传感器数据采集与滤波:温度、压力、血氧传感器接口、数据采集时序、滑动平均滤波、中值滤波

各位好,我是老周。这一章咱们聊聊传感器数据采集和滤波。说实话,在医疗设备里,传感器就是设备的「眼睛」和「耳朵」。眼睛看错了,耳朵听歪了,后面算法再牛也白搭。

我做过一个血氧仪的项目,当时采样时序没调好,波形老是抖。折腾了两周,最后发现是ADC触发时机偏了半个周期。嗯,从那以后我对时序就特别敏感。

4.1 三类核心传感器接口

医疗设备里最常见的传感器,我归纳为三类:温度、压力、血氧。它们的接口方式差别很大。

传感器类型 典型型号 接口协议 采样率需求 关键注意点
温度 MAX30205、DS18B20 I²C、1-Wire 1~10 Hz 自热效应、接触热阻
压力 MPXV5050、MS5803 模拟电压、SPI、I²C 50~200 Hz 管嘴堵塞、温漂补偿
血氧 MAX30102、AFE4400 I²C + 中断 100~1000 Hz 运动伪影、LED驱动时序

我的经验:温度传感器看似简单,但自热效应是个坑。我曾经用MAX30205做体温贴,连续读取时芯片自己发热,读数偏高0.3℃。后来改成间歇读取+关断模式才解决。

4.2 数据采集时序设计

时序设计是实时系统的命根子。你想想看,三个传感器要轮流采样,MCU还要跑控制算法和通信,时间窗口就那么窄。

我个人习惯用「时间片轮转+优先级中断」的方案。核心思路是:

  1. 固定采样周期:比如1ms一个tick,作为系统心跳
  2. 分时触发:温度每100ms采一次,压力每10ms采一次,血氧每2ms采一次
  3. 中断优先:血氧数据用硬件中断触发,保证不丢点
// 伪代码:时间片轮转调度
void SysTick_Handler(void) {
    static uint32_t tick = 0;
    tick++;
    
    // 每2ms采集血氧(高优先级)
    if (tick % 2 == 0) {
        SpO2_TriggerSample();
    }
    
    // 每10ms采集压力
    if (tick % 10 == 0) {
        Pressure_Read();
    }
    
    // 每100ms采集温度
    if (tick % 100 == 0) {
        Temperature_Read();
    }
}

注意:中断服务函数里不要做滤波运算!我曾经犯过这个错——在中断里跑滑动平均,结果中断时间过长,导致血氧数据丢失。正确的做法是:中断只负责搬数据,滤波放在主循环或任务里做。

4.3 滑动平均滤波

滑动平均是最常用的滤波方法,说白了就是取最近N个点的平均值。它对付高频噪声很有效,但会引入延迟。

公式很简单:

y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N

实际工程里,我推荐用「循环队列+累加和」的方式实现,避免每次重新求和:

#define WINDOW_SIZE 16

typedef struct {
    float buffer[WINDOW_SIZE];
    uint8_t index;
    float sum;
    uint8_t count;
} MovingAverage;

float MovingAverage_Update(MovingAverage *ma, float sample) {
    // 减去最旧的值
    ma->sum -= ma->buffer[ma->index];
    // 存入新值
    ma->buffer[ma->index] = sample;
    ma->sum += sample;
    ma->index = (ma->index + 1) % WINDOW_SIZE;
    
    if (ma->count < WINDOW_SIZE) {
        ma->count++;
        return ma->sum / ma->count;  // 初始阶段
    }
    return ma->sum / WINDOW_SIZE;
}

窗口大小怎么选?我一般遵循「3倍信号周期」原则。比如血氧脉搏波频率约1Hz,窗口取300ms左右(对应16个点@50Hz采样)。窗口太大,波形失真;窗口太小,噪声滤不干净。

4.4 中值滤波

中值滤波对付「野点」和「椒盐噪声」特别管用。它的原理很简单:把窗口内的数据排序,取中间那个值。

为什么不用均值?你想想看,如果压力传感器突然跳了一个毛刺,均值会把毛刺平均到结果里,而中值直接把它扔掉了。

// 中值滤波:冒泡排序取中值
#define MEDIAN_WINDOW 5

float MedianFilter(float *window, uint8_t size) {
    float temp[MEDIAN_WINDOW];
    uint8_t i, j;
    
    // 拷贝数据
    for (i = 0; i < size; i++) {
        temp[i] = window[i];
    }
    
    // 冒泡排序(窗口小,效率可接受)
    for (i = 0; i < size - 1; i++) {
        for (j = 0; j < size - 1 - i; j++) {
            if (temp[j] > temp[j + 1]) {
                float t = temp[j];
                temp[j] = temp[j + 1];
                temp[j + 1] = t;
            }
        }
    }
    
    return temp[size / 2];  // 取中值
}

避坑指南:中值滤波的窗口大小必须是奇数。我曾经用偶数窗口,结果取中间两个值的平均,反而引入了新的误差。另外,窗口越大,延迟越大,血氧波形会变「平」。我一般用5点或7点中值,效果最好。

4.5 滤波策略组合

实际项目中,单一滤波往往不够。我常用的组合策略是:

  • 温度:滑动平均(窗口8~16),温度变化慢,延迟可以接受
  • 压力:中值滤波(窗口5)+ 滑动平均(窗口4),先剔除野点再平滑
  • 血氧:中值滤波(窗口3)+ 带通滤波(0.5~5Hz),保留脉搏波特征

下面这张图是我自己总结的滤波选型流程:

传感器滤波策略选型流程图 原始传感器数据 是否有明显野点/毛刺? 是 中值滤波 否 滑动平均滤波 需要保留波形特征? 是 带通/低通滤波 否 直接输出 稳定可靠的传感器数据 血氧:中值3点 + 带通0.5~5Hz

4.6 实战中的坑与对策

最后,我把自己踩过的坑总结一下:

  • 采样率不匹配:ADC采样率太低,信号混叠。我建议采样率至少是信号最高频率的5倍,而不是理论上的2倍
  • 滤波延迟:滑动平均窗口越大,延迟越大。做闭环控制时要注意相位滞后
  • 浮点运算:MCU做浮点滤波很慢。我习惯把数据放大成整数再算,最后再归一化
  • 初始化阶段:滤波刚开始时数据不够,输出会跳变。加一个「预热」标志,等缓冲区填满再输出

小技巧:调试滤波参数时,我习惯用串口把原始数据和滤波后数据同时打印出来,用Excel画图对比。一眼就能看出延迟和滤波效果,比盯着数字强多了。

好了,这一章的内容就到这里。传感器数据采集和滤波是嵌入式医疗设备的基石,基础打牢了,后面的控制算法才能站得住脚。