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还要跑控制算法和通信,时间窗口就那么窄。
我个人习惯用「时间片轮转+优先级中断」的方案。核心思路是:
- 固定采样周期:比如1ms一个tick,作为系统心跳
- 分时触发:温度每100ms采一次,压力每10ms采一次,血氧每2ms采一次
- 中断优先:血氧数据用硬件中断触发,保证不丢点
// 伪代码:时间片轮转调度
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),保留脉搏波特征
下面这张图是我自己总结的滤波选型流程:
4.6 实战中的坑与对策
最后,我把自己踩过的坑总结一下:
- 采样率不匹配:ADC采样率太低,信号混叠。我建议采样率至少是信号最高频率的5倍,而不是理论上的2倍
- 滤波延迟:滑动平均窗口越大,延迟越大。做闭环控制时要注意相位滞后
- 浮点运算:MCU做浮点滤波很慢。我习惯把数据放大成整数再算,最后再归一化
- 初始化阶段:滤波刚开始时数据不够,输出会跳变。加一个「预热」标志,等缓冲区填满再输出
小技巧:调试滤波参数时,我习惯用串口把原始数据和滤波后数据同时打印出来,用Excel画图对比。一眼就能看出延迟和滤波效果,比盯着数字强多了。
好了,这一章的内容就到这里。传感器数据采集和滤波是嵌入式医疗设备的基石,基础打牢了,后面的控制算法才能站得住脚。