电子工坊

MAX30100 MAX30102实现SOP2脉搏血氧测量

对于我的智能手表项目,我决定尝试使用传感器读取脉冲。 环顾四周,我偶然发现了Maxim–MAX30100制造的传感器。 令我惊讶的是,一旦获得开发板并深入研究传感器的数据表,我发现它并不像将传感器连接到微控制器并读取数据那样简单。 您必须自己做很多工作。 在本教程中,我将尝试解释我对脉搏血氧仪的了解以及如何理解其数据。

介绍

在本教程中,我将简要说明脉搏血氧仪的工作原理以及如何理解来自MAX30100的数据。 本文将以一种方式来结构化,其中将解释每个连续步骤以及为何应用这种过滤以及如何进行计算。 该实现主要包括两个部分:仅使用IR LED读取脉冲,并使用RED和IR LED计算SaO2。

到本文结尾,您应该能够理解信号经过的各个阶段。 这些方法应适用于任何传感器,甚至包括您自己制造或由其他制造商制造的传感器。

什么是脉搏血氧仪?

脉搏血氧仪基本上是一种可以测量血液中脉搏和血氧饱和度的设备。 通常,此传感器由两个发光的LED组成:一个是红色光谱(650nm),另一个是红外光谱(950nm)。 该传感器放置在手指或耳垂上,基本上在皮肤不太厚的任何地方,因此两种光频率都可以轻松穿透组织。 例如,一旦两个手指都通过手指照亮,吸收率就可以用光电二极管测量。 并且,根据您血液中的氧气量,吸收的红光和红外灯之间的比率也会有所不同。 从这个比率可以“轻松地”计算出您的血红蛋白中的氧气水平(见图1)。

图1 血红蛋白的光吸收图

在这里可以找到关于脉搏血氧仪背后很详细的理论解释:https://www.howequipmentworks.com/pulse_oximeter/
它缺少实现MAX30100驱动程序的细节。 但是应该使您对这些传感器的总体运行情况有一个很好的了解。

MAX30100能获取到什么数据

最初,我认为该传感器MAX30100将为我做所有事情。错误理解它将自动测量脉冲和氧饱和度水平,并将存入放入一个寄存器,我可以很容易地通过I2C读取它,类似于BMP280。这非事实如此,读取血样饱和度还很远。

即使MAX30100并不能为您做任何事情,它在帮助测量这两个光频率之间的吸收方面仍然发挥了很大作用。如果您想构建自己的传感器,它肯定会令你设计很复杂。您必须在读取IR和RED LED吸收之间手动切换,使用PWM手动调节LED的亮度,从信号中滤除50 / 60Hz噪声等等。

我在上一段中提到的所有这些事情都是由MAX30100自动完成的。您只需配置传感器,然后运行它,它将读数存储在FIFO缓冲区中。然后,您唯一需要做的就是去读取FIFO数据并使之有意义。顺便说一下,如果您创建了自己的传感器,那将非常相似。在此基础上,如果您确实使用其他传感器或从头开始自己制作传感器,那么本文也应有所帮助。

简而言之,不要以为MAX30100会为您做所有事情,仍然需要您进行很多解密工作。

MAX30100手册资料

我们要做的第一件事是将传感器连接到我们的微控制器并读取其数据。 我不会详细介绍; 只是一些小笔记和技巧。 因为我觉得这是一个相当简单的过程。
首先,了解有关MAX30100的一些重要的信息:

  1. MAX30100的I2C地址:0x57
  2. 数据存储在FIFO缓冲区中。 它最多可以存储16个测量值,其中每个样本的大小为4个字节。 前两个字节用于IR测量,后两个字节用于RED测量。
  3. 由于FIFO指向相同的地址,因此I2C无法读取FIFO缓冲区。 您必须完成事务以使FIFO输出地址包含下一个值。
  4. MAX30100内置50/60Hz滤波器
  5. 如果您只想检测脉冲,则只需开启红外
  6. 如果测量血氧饱和,您需要同时启用红外和红色LED
  7. 通过更改LED的采样率和脉冲宽度,您还可以更改ADC分辨率。 重要的是要注意,采样率和脉冲宽度直接相互关联。 请参见第19页的数据表表8和表9或参见图2。不要只是随机配置它们。

    图2采样率与脉冲宽度配置表

要开始从MAX30100读取数据,您只需要做两件事:

  1. 设置模式,建议一开始只将其设置为心率模式
  2. 设置红外灯的电流
    完成过滤后,这将使我们能够测量心率。 您可以通过查看库中的三个函数来检查我如何做到这一点:setMode(),setLEDCurrents()和readFIFO().

读取红外数据

设法将MAX30100设置为HR模式并读取原始IR数据后,绘制完成后应如图3所示:

图3 原始IR数据,可见振荡

直流去除

您应该在图中注意两点(图3):

  1. 该图略微振荡
  2. 直流Y轴起始峰值与最后峰值偏移为5万点

为了正确读取心律和SaO2,我们需要去除DC信号,仅保留AC部分
它实际上非常简单,可以使用以下两个方程式完成:

直流去除:
(w(t)=x(t)+ ∝ *w(t-1))

(y(t)=w(t)-w(t-1))

y(t): 滤波器的输出
x(t): 当前输入/值
w(t): 中间值,就像DC以前的值
α: 滤波器的响应常数
如果α= 1,那么一切都会通过
如果α= 0,则没有任何东西通过
对于DC直流去除,您希望α相当接近1。我将使用α= 0.95。

如果您想了解有关DC移除的更多信息,请参阅此处,它是一个很好的教程,并对其功能进行了更详细的描述:http://sam-koblenski.blogspot.co.uk/2015/11/everyday-dsp-for-programmers-dc-and.html

这是在代码中实现的过滤器:

struct fifo_t {
  uint16_t rawIR;
  uint16_t rawRed;
};
dcFilter_t MAX30100::dcRemoval(float x, float prev_w, float alpha)
{
  dcFilter_t filtered;
  filtered.w = x + alpha * prev_w;
  filtered.result = filtered.w - prev_w;

  return filtered;
}

一旦使信号通过直流去除滤波器,我们将得到类似于图4的信号:

图4 红外信号通过直流去除滤波
如您在图4中看到的,我们现在只剩下信号的交流部分,它在0个DC值而不是50000左右振荡,低的无效DC信号滤除。

均值中值滤波器

现在,我们已经对信号进行了直流滤波,为了进一步提高检测脉冲的能力,我们必须对信号进行差分处理。 我们的脉冲是数据中突然出现最大值变化的地方。
但是,我决定采用均值中值滤波器,而不是仅仅采用差值来进一步净化信号。 顾名思义,这将使我们的平均值产生变化。 这是这种过滤器的简单实现:

struct meanDiffFilter_t
{
  float values[MEAN_FILTER_SIZE];
  byte index;
  float sum;
  byte count;
};

float MAX30100::meanDiff(float M, meanDiffFilter_t* filterValues)
{
  float avg = 0;

  filterValues->sum -= filterValues->values[filterValues->index];
  filterValues->values[filterValues->index] = M;
  filterValues->sum += filterValues->values[filterValues->index];

  filterValues->index++;
  filterValues->index = filterValues->index % MEAN_FILTER_SIZE;

  if(filterValues->count < MEAN_FILTER_SIZE)
    filterValues->count++;

  avg = filterValues->sum / filterValues->count;
  return avg - M;
}

将直流滤波后的信号通过均值差滤波器后,我们得到了熟悉的信号,可以重新组合心电图(见图5)

图5 均值差滤波后的IR信号

真正高大的山峰是我的心跳。 从该数据可以轻松推断出我的心跳,但是,如果您仔细观察波形,数据中会有一些更高层次的和谐。 它们在信号的底部特别明显。 如果将信号通过低通滤波器或带通滤波器,则可以轻松过滤掉它们。

巴特沃斯过滤器

为了消除更高级别的谐波,我将在低通滤波器配置中使用Butterworth滤波器。 从技术上讲,它是一个带通滤波器。 而且,任何低通滤波器都可以。 与Butterworth一起工作相对容易。 有一个很好的在线工具可以为您所需的频率生成巴特沃斯滤波器常数:http://www.schwietering.com/jayduino/filtuino/

因此,要实现此滤波器,我们必须建立两个变量:采样率(FS)和截止频率(FC)

从技术上讲,可用于MAX30100的最快采样率为1kHz,尽管如此,我选择的配置还是采用了长脉冲宽度,因此采样率仅为100Hz。 因此,我们可以推断出我们的采样率为100Hz。

接下来,我们需要选择截止频率。据我所知,由于我们正在测量心率,因此220 BPM是危险的高心率,但在某些情况下仍然可以实现。 因此,我选择将其作为我们必须通过的最大频率。

我们需要让我们通过的最快频率可以这样计算: f = {220 BPM \over 60} = 3.66Hz

如果我们假设要测量低至50 BPM,则可以应用相同的计算: f = {50 BPM \over 60} = 0.83Hz

重要的是要记住,巴特沃斯滤波器工作在归一化频率上 R_n = {F_C \over F_S}

因此,如果您的采样率不是100Hz,则Butterworth滤波器将开始截止不同的频率。 在图6中,您可以看到一个循环在我当前的实现中完成的速度。

显示了在读取和过滤MAX30100数据时一个循环完成的速度。 请注意,每次循环结束时,输出引脚都会翻转。 实际执行速度为75Hz

如图6所示,我们的采样率约为75Hz。 假设我们实际上以FS = 100Hz和FC = 4Hz实施了巴特沃斯滤波器。

如果应用归一化,则在我们的真实FS = 75Hz时,我们的截止频率将为FC = 3Hz

因此,我们有一个问题,我们的截止频率低于我们预期的3.66Hz。 这意味着我们最多只能测量180BPM,而不是所需的220BPM。 但是,如果更新速度更低,我们将切断甚至更多的频率。

要解决此问题,我们有两个选项可用,要么具有精确的采样率,要么增加截止频率。 有效地增加可用采样率误差容限,并稍微降低滤波信号的质量。

我采用了第二个选项,并选择了一个新的FC值。
FS=100Hz FC=10Hz

那将使我们的比率为:R_n = {100Hz \over 10Hz} = 0.1

假设220BPM或3.66Hz是我们的目标频率。 巴特沃思滤波器现在仍将以低至以下采样率的频率通过所需的频率:
{3.66Hz \over 0.1} = 36.6Hz

在我们的FS = 75Hz的实际示例中,它将为我们提供实际的FC = 7.5Hz。
我相信它足以满足我们的滤波需求,因为我们不需要对信号进行滤波就非常精确。 足以清除它并改善检测峰值的信号。
考虑到我们的FS = 100Hz和FC = 10Hz,我们得到了Butterworth滤波器的以下代码:

struct butterworthFilter_t
{
  float v[2];
  float result;
};
void MAX30100::lowPassButterworthFilter( float x, butterworthFilter_t * filterResult )
{  
  filterResult->v[0] = filterResult->v[1];

  //Fs = 100Hz and Fc = 10Hz
  filterResult->v[1] = (2.452372752527856026e-1 * x) + (0.50952544949442879485 * filterResult->v[0]);

  //Fs = 100Hz and Fc = 4Hz
  //filterResult->v[1] = (1.367287359973195227e-1 * x) + (0.72654252800536101020 * filterResult->v[0]); //Very precise butterworth filter 

  filterResult->result = filterResult->v[0] + filterResult->v[1];
}

一旦传递了心电图观察信号,我们就会得到更加平滑的信号(见图7)

图7 Fs = 100Hz和Fc = 10Hz的巴特沃斯滤波信号。 实际采样率Fs = 75Hz,即Fc = 7.5Hz

这就是我们红外输出所要做的。 在此阶段,应该清楚地知道脉搏在哪里。现在能输出良好波形的心电图。

节拍检测(心率计算)

现在我们收到了来自MAX30100的相对干净的信号,我们可以开始计算心率了。 我决定实现一个非常简单的状态机。 我的设计绝不是无错误的,也不是行业准备就绪的。它不是,并且很容易错过检测脉冲或根本不检测到脉冲,但这很好地证明了概念。

状态机的想法很简单。一旦达到阈值,请遵循曲线。一旦信号开始下降一次或多次,请保存时间戳。 一旦有了两个时间戳,它们之间的差就是我们在两个拍子之间测得的延迟。 由此我们可以计算出BPM。

Arduino有一个很好的函数,称为millis(),可以给您一个以毫秒为单位的时间戳。 如果得到两个时间戳,则可以使用以下公式计算心率:
BPM = 60000/(当前的最佳(峰值)时间戳-之前的拍子(峰值)时间戳)

最重要的是,因为我们以这种方式计算BPM,所以我决定还对BPM结果实施移动平均滤波器。 只是为了更准确地测量心率。

图8 用于检测峰的状态机图

最后是检测和测量脉冲的代码

在这一阶段,我们对信号应用了多个滤波器。 此外,我们还检测了脉搏并测量了心率。 但是,如前所述,该状态机仍然可以大大改进,不应该在实际产品中使用。

测量血氧饱和度

如引言中所述,可以通过计算来自IR LED和Red LED的吸收光之比来测量氧气浓度。 在本节中,我将探讨理论上的完成方式,但由于某些限制,传感器无法正确校准。 不幸的是,为此您需要适当的经验数据来创建查找表。

设定IR和RED电流

首先,我们必须将MAX30100模式切换为SaO2 + HR。 可以通过向MODE配置寄存器发送0x03来完成。 这将使两个LED均能使MAX30100开始使用两个光谱的读数填充FIFO缓冲器。

另外,RED读数应与IR读数通过相同的DC去除滤波器。但是没有必要让它通过平均滤波器和巴特沃斯滤波器,因为我们没有使用红光来检测脉冲。
如果您同时启用两个LED的最大输出电流为50ma,您将很快意识到,红色LED的读数将非常饱和。 为了能够测量两个读数之间的比率,在基准水平上,它们的直流电平应几乎相同(见图9和图10)。

图9 直流电平不匹配。差异约为380000(DC单位)。 IR led已设置为50ma,RED led已设置为50ma

图10 更紧密匹配的直流电平。 现在的差异已减少至42000(DC单位)。 IR led已设置为50ma,RED led已设置为27.1ma

设定很简单:

  1. 检查RED和IR DC读数之间的差异
    2.如果IRED> IIR,则降低IRED电流,如果IRED < IIR,则增加IRED电流

重要的是要注意,IRED不应立即更改,而只有在差异超过特定阈值时才偶尔更改,而该阈值只能通过实验确定。
这是我为平衡IIR和IRED而实施的代码:

void MAX30100::balanceIntesities( float redLedDC, float IRLedDC )
{

  if( millis() - lastREDLedCurrentCheck >= RED_LED_CURRENT_ADJUSTMENT_MS) 
  {
    //Serial.println( redLedDC - IRLedDC );
    if( IRLedDC - redLedDC > MAGIC_ACCEPTABLE_INTENSITY_DIFF && redLEDCurrent < MAX30100_LED_CURRENT_50MA) 
    {
      redLEDCurrent++;
      setLEDCurrents( redLEDCurrent, IrLedCurrent );
      if(debug == true) 
        Serial.println("RED LED Current +");
    } 
    else if(redLedDC - IRLedDC > MAGIC_ACCEPTABLE_INTENSITY_DIFF && redLEDCurrent > 0) 
    {
      redLEDCurrent--;
      setLEDCurrents( redLEDCurrent, IrLedCurrent );
      if(debug == true) 
        Serial.println("RED LED Current -");
    }

    lastREDLedCurrentCheck = millis();
  }
}

正如我之前说过的,您必须选择一个好的魔术值,以使这两个读数在基本状态下可以接受。 如果您选择的魔术值太小,将会导致很多振荡(见图11)。

图11 由于值太小而调用的振荡。 在此示例中,差异设置为50000

经过一些试验,我得出了很好的值65000。在我的用例中,这完全消除了振荡。 而且仅在极少数情况下,算法会在激活一段时间后调整电流。 但是,它会严重调整不准确的强度以立即匹配IIR(请参见图12)

图12 良好的值65000。立即平衡并保持稳定

这就是我们可以计算SpO2值之前要做的所有事情

SpO2的一点点理论

简而言之,将SpO2定义为含氧血红蛋白水平与总血红蛋白水平之比。
SpO_2 = {HbO_2 \over \text{Total Hb}}

我们的身体组织吸收的光量取决于血液的氧合水平。 但是,必须注意的是,该特性是非线性的。

如前所述,使用了两个不同的波长IR(950nm)和RED(650nm)。这两个波长以交替的方式朝向您的手指、耳垂等发射。打开一个LED,进行测量,然后将其关闭。 对于其他频谱重复此操作。 基本上,它们的测量不是同时进行的。

这两个波长之间的比率R由以下方程式定义:
R = { AC_\text{RMS RED} / DC_\text{RED} \over AC_\text{RMS IR} / DC_\text{IR}}

或者也可以这样表示:
R = { log(I_{AC}) * \lambda_1 \over log(I_{AC}) * \lambda_2}

IAC是仅存在AC的光强度。 λ1用于650nm波长,λ2用于950nm波长

引用TI关于脉搏血氧仪的文章
一旦直流电平匹配,则通过除以RMS值的对数来计算SpO2

如您所知,我们已经平衡了DC电平,剩下要做的就是计算IIR和IRED的RMS

如果您不知道,计算基本RMS值非常简单; 您只需要取信号平方的和,取平均值,然后取平均值的平方根即可。 它不是真正的RMS,但对于我们的应用程序来说绰绰有余。

有关基本说明,请参见本文:http://practicalphysics.org/explaining-rms-voltage-and-current.html

我还想强调一下,不仅在有脉冲的情况下,如何为整个信号计算RMS值。 建议不定期重设一次; 否则它将保存整个历史垃圾数据。 在我的最终实现中,我每4个心跳重置一次RMS。

现在我们已经计算了两个波长的RMS值,还计算了比率R值,剩下要做的就是计算实际的SpO2值。

这是非常有趣的地方。 为了能够精确测量氧饱和度,您需要校准传感器。 没有适合他们的公式。

尽管如此,基本上所有教科书都引用了计算SpO2的标准模型,如下所示:
SpO_2 = 110 – 25 * R

正如我之前所说,这种关系是非线性的。 但是标准模型显然暗示了线性关系,这是不正确的。 参见图13,可对SpO2的经验R和理论R进行出色的比较


图13 对SaO2的经验和理论R。 资料来源:http://www.ti.com/lit/an/slaa274b/slaa274b.pdf

你也应该注意到,即使有经验校准,一旦血氧饱和度低于80%,你可以放心地假设一个线性关系。

这是我遇到的问题。 我真的没有校准MAX30100传感器的方法。 我既没有校准的脉搏血氧仪作为参考,也没有其他方法来确定我的实际血氧饱和度

在实践中完成所有这些计算后,我得到的RMS比率介于:0.84 – 0.86

根据标准模型,SpO2的产生率为88.5%至89%。 或根据TI经验曲线:〜90%。

感觉仍然很低,因为我希望健康的人至少有94%。 除非我在测量氧饱和度时身体很不健康。 另外,我住在海边,也许在海拔3m左右。 因此,我的海拔高度不应成为低氧水平的因素。

我的决定(不是最科学的)是假设我的氧气含量为94%,并相应地调整了标准模型。
SpO_2 = 110 – 180 * R

我真的要强调!这不是确定SpO2的科学或适当方法。 您必须进行适当的校准; 这仅是一个估计值(而且非常糟糕)!

尽管如此,这是实现我在本文中描述的所有内容之后的最终结果(参见图14)。

图14 从MAX30100驱动程序的最终实现中读取

在图14中,您可以看到我的脉搏速率约为68.81 BPM,氧气浓度约为94.06%。 我绝对可以确定心率测量的准确性,因为我可以使用Omron血压测量设备进行多次检查,该设备还可以测量BPM。 在此特定情况下,欧姆龙测得的我的BPM为68。

结论

这并非像我最初预期的那样测量您的血液中的心率和血氧饱和度那么简单。但是凭借持久性,我对所涉及的DSP和测量SpO2以从零开始实施的理论有了足够的了解。不仅所有这些都仅适用于MAX30100,而且应该对您自己制造的传感器或由Maxim公司以外的公司生产的传感器进行类似的技术和计算。 MAX30100仅提供了非常复杂的模拟电路的超小型封装的便利性。但是,从快速测试中,我必须说,使用此传感器测量手腕的心率非常困难。从本质上讲,当前的检测峰值算法是不可能的。同样重要的是要记住,在文章中,在测量氧饱和度时,我没有正确校准传感器,只是调整了标准模型以适合我的感觉。强烈建议您使用此传感器测量SpO2时,必须正确校准。

Code

您可以从github此处下载该库:https://github.com/xcoder123/MAX30100

参考文献

ElectronicsTutorials. “Butterworth Filter Design.” ElectronicsTutorials. n.d. http://www.electronics-tutorials.ws/filter/filter_8.html (accessed February 17, 2017).

Erken, Eray. How to Measure Heart Rate with MAX30100 Pulse Oximeter. 30 April 2016. https://www.youtube.com/watch?v=36cELpNGJZYHow to Measure Heart Rate with MAX30100 Pulse Oximeter (accessed December 1, 2016).

Intersecans, OXullo. Arduino-MAX30100. n.d. https://github.com/oxullo/Arduino-MAX30100 (accessed January 20, 2017).

IOP Institute of Physics. Explaining rms voltage and current. n.d. http://practicalphysics.org/explaining-rms-voltage-and-current.html (accessed February 27, 2017).

Iowegian International. “IIR Filter Basics.” dspGuru. n.d. http://dspguru.com/dsp/faqs/iir/basics (accessed December 3, 2016).

Kennedy, Stephen M. “AN INTRODUCTION TO PULSE OXIMETERS: EQUATIONS AND THEORY.” University of Wisconsin-Madison. 20 April 2015. http://www.imt.liu.se/FoUtb/kurser/oxikurs/restricted/pulseoximetersequationsandtheory-stephenkennedy.pdf (accessed January 15, 2017).

Koblenski, Sam. Everyday DSP for Programmers: DC and Impulsive Noise Removal. 23 November 2015. http://sam-koblenski.blogspot.co.uk/2015/11/everyday-dsp-for-programmers-dc-and.html (accessed December 4, 2016).

Lopez, Santiago. “Pulse Oximeter Fundamentals and Design.” NXP. November 2012. http://www.nxp.com/assets/documents/data/en/application-notes/AN4327.pdf?&tid=AMdlDR (accessed January 17, 2017).

Maxim. “Pulse Oximeter and Heart-Rate Sensor IC for Wearable Health.” MaximIntegrated. September 2014. https://

本文机译人工校准部分,源文by Raivis Strogonovs In: Microcontrollers, Atmel (https://morf.lv/implementing-pulse-oximeter-using-max30100 “)