4、氧浓度传感器驱动开发:电化学传感器原理、I2C通信协议实现、数据滤波算法

好,咱们进入第四章。这一章我打算聊聊氧浓度传感器的驱动开发。说实话,在麻醉机里,氧浓度这个参数是真正的“生命线”。你想想看,病人吸入的氧气浓度如果偏差太大,要么是缺氧,要么是氧中毒,哪个都受不了。所以这一章,我会把电化学传感器的原理、I2C怎么玩、还有数据滤波那点事儿,掰开了揉碎了讲清楚。

4.1 电化学传感器原理:它到底是怎么测氧的?

电化学氧传感器,说白了就是一个“会发电的小电池”。它内部有阳极、阴极,还有一层电解质膜。当氧气分子穿过透气膜进入传感器内部,在阴极上发生还原反应,产生电流。这个电流的大小,和氧气浓度是成正比的。

我记得第一次接触这玩意儿,是在一个老款的麻醉机上。当时我还在想,这玩意儿怎么这么娇气?后来拆开一看,里面就三个引脚:一个工作电极、一个对电极、一个参比电极。嗯,其实没那么神秘。

核心公式(简化版):

I = k × [O₂]

其中 I 是输出电流(μA),k 是传感器灵敏度常数,[O₂] 是氧气浓度(%)。

实际项目中,这个 k 值会随着温度、湿度、老化程度发生变化。所以你不能指望它永远准。我一般会在产品出厂前做一次两点校准:用 21% 的空气和 100% 的纯氧做标定。

4.2 I2C通信协议实现:怎么把数据读出来?

现在的电化学传感器模块,很多都集成了 ADC 和 I2C 接口。比如我常用的 City Technology 的 4OXV 系列,它内部已经把电流信号转成了数字量,你只需要通过 I2C 去读寄存器就行。

I2C 协议本身不复杂,但有几个坑我得提醒你。

避坑指南:

我曾经在一个项目中,I2C 时钟线 SCL 和数据线 SDA 的上拉电阻选错了。选了 10kΩ,结果总线电容太大,波形都变成正弦波了。后来换成 4.7kΩ,问题解决。记住:上拉电阻一般选 2.2kΩ ~ 4.7kΩ,具体看总线长度和从机数量。

下面是我常用的 I2C 读取代码片段,用的是 STM32 HAL 库:

#include "i2c.h"

#define O2_SENSOR_ADDR  0x70  // 7位地址,左移一位后是0xE0
#define O2_DATA_REG     0x00  // 数据寄存器地址

uint16_t O2_ReadConcentration(void)
{
    uint8_t reg = O2_DATA_REG;
    uint8_t buf[2] = {0};
    uint16_t raw_value = 0;

    // 先写寄存器地址
    HAL_I2C_Master_Transmit(&hi2c1, O2_SENSOR_ADDR << 1, &reg, 1, 100);
    // 再读两个字节
    HAL_I2C_Master_Receive(&hi2c1, O2_SENSOR_ADDR << 1, buf, 2, 100);

    raw_value = (buf[0] << 8) | buf[1];
    return raw_value;
}

这里有个细节:I2C 地址是 7 位的,但 HAL 库的传输函数需要 8 位地址(左移一位,最低位表示读写方向)。我第一次用的时候忘了左移,结果读出来的数据全是 0xFF。嗯,这种低级错误,犯过一次就不会再犯了。

4.3 数据滤波算法:原始数据太“脏”怎么办?

电化学传感器的输出,其实挺“毛躁”的。因为气流波动、泵的振动、甚至病人呼吸都会造成干扰。如果你直接把原始数据送给控制算法,那麻醉机可能会疯掉——一会儿报氧浓度过高,一会儿报过低。

我个人习惯用两种滤波算法:滑动平均和中值滤波。它们各有各的适用场景。

4.3.1 滑动平均滤波

滑动平均,说白了就是取最近 N 个采样点的平均值。它适合滤除高频噪声,但对突发性的尖峰干扰效果不好。

#define FILTER_WINDOW_SIZE  10

static uint16_t buffer[FILTER_WINDOW_SIZE] = {0};
static uint8_t index = 0;
static uint8_t count = 0;

uint16_t MovingAverageFilter(uint16_t new_sample)
{
    uint32_t sum = 0;
    uint8_t i;

    buffer[index] = new_sample;
    index = (index + 1) % FILTER_WINDOW_SIZE;
    if (count < FILTER_WINDOW_SIZE) count++;

    for (i = 0; i < count; i++) {
        sum += buffer[i];
    }

    return (uint16_t)(sum / count);
}

你想想看,窗口大小怎么选?选太小了,滤波效果差;选太大了,响应延迟大。我一般选 8 到 16 之间。在麻醉机上,氧浓度变化不会特别快,所以 10 个点是比较折中的选择。

4.3.2 中值滤波

中值滤波就厉害了。它把窗口内的数据排序,取中间那个值。这个算法对付“野点”特别有效。比如传感器偶尔蹦出一个 200% 的离谱值,滑动平均会被拉偏,但中值滤波直接把它扔掉了。

#define MEDIAN_WINDOW_SIZE  5

static uint16_t sort_buffer[MEDIAN_WINDOW_SIZE];

uint16_t MedianFilter(uint16_t new_sample)
{
    uint8_t i, j;
    uint16_t temp;

    // 把新数据插入到排序缓冲区
    for (i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
        sort_buffer[i] = sort_buffer[i + 1];
    }
    sort_buffer[MEDIAN_WINDOW_SIZE - 1] = new_sample;

    // 冒泡排序(窗口小,无所谓效率)
    for (i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
        for (j = 0; j < MEDIAN_WINDOW_SIZE - 1 - i; j++) {
            if (sort_buffer[j] > sort_buffer[j + 1]) {
                temp = sort_buffer[j];
                sort_buffer[j] = sort_buffer[j + 1];
                sort_buffer[j + 1] = temp;
            }
        }
    }

    return sort_buffer[MEDIAN_WINDOW_SIZE / 2];
}

我的经验:

在实际项目中,我经常把两种滤波组合起来用。先做一次中值滤波,把明显的野点干掉,再做一次滑动平均,把剩下的毛刺磨平。这样既保证了响应速度,又保证了数据平滑度。你可以试试看。

4.4 总结一下

这一章我们聊了三件事:

  • 电化学传感器原理:电流和氧浓度成正比,但要注意温度补偿和老化校准。
  • I2C 通信实现:地址要左移一位,上拉电阻别选太大,时序要严格按 datasheet 来。
  • 数据滤波算法:滑动平均去噪声,中值滤波去野点,组合使用效果更佳。

下一章我会讲怎么把这些数据整合到麻醉机的安全监控系统中。到时候你会看到,一个简单的氧浓度值,背后有多少道“安检”工序。嗯,咱们下章见。