MAX30102代码分析总篇

随笔7个月前发布 周先生
135 0 0

前言

主要介绍的是arduino中SparkFun_MAX3010x_Sensor_Library这个库。

SparkFun_MAX3010x_Sensor_Library链接地址

这个库可以在arduino中直接搜索下载。

主要分析的是SpO2这个部分。examples中是示例,src中是源码。

如果对max30102的初始化过程不清楚,可以看下面这篇文章。

MAX02分析

实例代码分析

引用部分

头文件的引用

#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"

虽然这里标注的是#include "MAX30105.h",但是MAX30102也可以使用。

#include <Wire.h>其实是不用引用的,因为在#include "MAX30105.h"中已经引用过了,这里引用可能是为了可读性。

创建类

MAX30105 particleSensor;

这里没什么好说的,就是创建了一个MAX30105的对象。

iic初始化部分

if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) //Use default I2C port, 400kHz speed
{
    Serial.println(F("MAX30105 was not found. Please check wiring/power."));
    while (1);
}

这里的F指的是把字符串存放在flash闪存中,不得不说这个处理还是很细节的。

代码的核心部分是particleSensor.begin(Wire, I2C_SPEED_FAST)

I2C_SPEED_FAST

代表的是iic的速度,下面是它的定义。

#define I2C_SPEED_STANDARD        100000
#define I2C_SPEED_FAST            400000

因为现在购买的一般是黑色的MAX30102的模块,这种模块是MAX30102和电容电阻都在一个面,而且面积小,手指在按上去的时候很容易接触到信号线的触点,所以会干扰到iic信号。一般的解决方法是做绝缘,或者把信号线速率降低。

所以这里更建议设置为I2C_SPEED_STANDARD

Wire

这里其实就是传递一个TowWire的引用。

MAX30105::begin

再看一下这个函数的原型。

在H文件中的引用是这样的

boolean begin(TwoWire &wirePort = Wire, uint32_t i2cSpeed = I2C_SPEED_STANDARD, uint8_t i2caddr = MAX30105_ADDRESS);

这样写是代表这三个选项都是可选的,也就是说,你不传递任何值,也是可以正确初始化的。

看一下三个参数

  • wirePort就是接受了一个Wire类
  • i2cSpeed默认是100kHz的速率
  • i2caddr是MAX30102的地址,默认是0x57

也就是说如果什么都不传递,i2cSpeed的速率默认是低速的,所以在初始化时,传递了Wire和I2C_SPEED_FAST,把速度设置为高速。
为啥不直接传I2C_SPEED_FAST,还要传个Wire呢?因为i2cSpeed是第二的参数,所以想要赋值第二个参数,你先得赋值第一个参数。

所以如果想让iic运行在低速时,begin是不用传递参数的。

这里的TwoWire只是Wire类的别名,功能上是完全等价的,为什么要用TwoWire呢,其实就是告诉你,如果你的开发板上有两个iic,而你恰好想用第二条,你就可以传递一个Wire1。

begin在C文件中的实现如下

boolean MAX30105::begin(TwoWire &wirePort, uint32_t i2cSpeed, uint8_t i2caddr) {

  _i2cPort = &wirePort; //Grab which port the user wants us to use

  _i2cPort->begin();
  _i2cPort->setClock(i2cSpeed);

  _i2caddr = i2caddr;

  // Step 1: Initial Communication and Verification
  // Check that a MAX30105 is connected
  if (readPartID() != MAX_30105_EXPECTEDPARTID) {
    // Error -- Part ID read from MAX30105 does not match expected part ID.
    // This may mean there is a physical connectivity problem (broken wire, unpowered, etc).
    return false;
  }

  // Populate revision ID
  readRevisionID();
  
  return true;
}

前四行代码功能就是开启iic总线

  • _i2cPort = &wirePort;在赋值TowWire类
  • _i2cPort->begin();在初始化iic总线
  • _i2cPort->setClock(i2cSpeed);在设置iic速率
  • _i2caddr = i2caddr;在设置iic地址。

进一步深入发现readPartID()readRevisionID其核心是调用了readRegister8,而读取和写入一般都是成对出现的,写入的函数是writeRegister8

uint8_t MAX30105::readPartID() {
  return readRegister8(_i2caddr, MAX30105_PARTID);
}

MAX30105_PARTID:值为0xFF。作用是读取部件id。部件id固定是0x15,所以可以推出来MAX_30105_EXPECTEDPARTID的值是0x15。

void MAX30105::readRevisionID() {
  revisionID = readRegister8(_i2caddr, MAX30105_REVISIONID);
}

MAX30105_REVISIONID:值为0xFE。作用是读取版本号。

MAX30105::readRegister8

来看看读写函数

这是一个用iic总线读取8位数据的函数。

看一下函数原型

uint8_t MAX30105::readRegister8(uint8_t address, uint8_t reg) {
  _i2cPort->beginTransmission(address);
  _i2cPort->write(reg);
  _i2cPort->endTransmission(false);

  _i2cPort->requestFrom((uint8_t)address, (uint8_t)1); // Request 1 byte
  if (_i2cPort->available())
  {
    return(_i2cPort->read());
  }

  return (0); //Fail

}

_i2cPort->beginTransmission(address);的作用是设置传输设备的地址

_i2cPort->write(reg);的作用是设置要传输的数据,这里代表的就是寄存器地址

_i2cPort->endTransmission(false);的作用是结束iic传输,并重新发送一个开始信号,释放资源,如果是传输true的话,会返回一个状态码,指示是否传输成功

状态码 表示
0 表示传输成功
1 表示数据量超过了传送缓存的容纳限制
2 表示在传送地址时收到了NACK(非确认信号)
3 表示在传送数据时收到了NACK
4 表示其他错误

_i2cPort->requestFrom((uint8_t)address, (uint8_t)1);的作用是读取一个字节

_i2cPort->available()的作用是判断是否有可用的数据供读取

_i2cPort->read()的作用是把这个数据读取出来

如果读取失败了,就返回0,这也是在使用这个库时最好iic用低速率的原因。当iic总线收到干扰时(没有做绝缘,手指触到到之后干扰到iic总线信号传输,因为人体相当于一个几十到几百uF的电容)就会直接返回0,这时并不能确定时总线受到干扰,信号没回来,还是传输回来的就是0。
这时候看到的数据就是不稳定的。

MAX30105::writeRegister8

介绍完了读函数,写函数就没什么好说的了。看一下源码。

void MAX30105::writeRegister8(uint8_t address, uint8_t reg, uint8_t value) {
  _i2cPort->beginTransmission(address);
  _i2cPort->write(reg);
  _i2cPort->write(value);
  _i2cPort->endTransmission();
}

加了一个value,就是需要在reg这个地址的寄存内写入的数据。

不同的是_i2cPort->endTransmission();里面没有传递参数,这代表着,发送停止信号,结束iic传输。在读函数中,有参数是因为还要执行一遍读取操作,而写函数不需要再读取了。

用户操作部分

Serial.println(F("Attach sensor to finger with rubber band. Press any key to start conversion"));
while (Serial.available() == 0) ; //wait until user presses a key
Serial.read();

这一段没什么好说的,就是初始化成功了,然后让你随便输入个东西,然后继续执行后面的内容

实际开发中用不到这一块代码

MAX3010X初始化部分

byte ledBrightness = 60; //Options: 0=Off to 255=50mA
byte sampleAverage = 4; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 100; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); //Configure sensor with these settings

这里开始设置MAX3010X的各种参数,对其进行初始化。直接看函数原型吧

MAX30105::setup

在H文件中的定义如下

void setup(byte powerLevel = 0x1F, byte sampleAverage = 4, byte ledMode = 3, int sampleRate = 400, int pulseWidth = 411, int adcRange = 4096);

也都是可选参数

CPP文件中的实现如下

void MAX30105::setup(byte powerLevel, byte sampleAverage, byte ledMode, int sampleRate, int pulseWidth, int adcRange) {
  softReset(); //Reset all configuration, threshold, and data registers to POR values

  //FIFO Configuration
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  //The chip will average multiple samples of same type together if you wish
  if (sampleAverage == 1) setFIFOAverage(MAX30105_SAMPLEAVG_1); //No averaging per FIFO record
  else if (sampleAverage == 2) setFIFOAverage(MAX30105_SAMPLEAVG_2);
  else if (sampleAverage == 4) setFIFOAverage(MAX30105_SAMPLEAVG_4);
  else if (sampleAverage == 8) setFIFOAverage(MAX30105_SAMPLEAVG_8);
  else if (sampleAverage == 16) setFIFOAverage(MAX30105_SAMPLEAVG_16);
  else if (sampleAverage == 32) setFIFOAverage(MAX30105_SAMPLEAVG_32);
  else setFIFOAverage(MAX30105_SAMPLEAVG_4);

  //setFIFOAlmostFull(2); //Set to 30 samples to trigger an 'Almost Full' interrupt
  enableFIFORollover(); //Allow FIFO to wrap/roll over
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  //Mode Configuration
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  if (ledMode == 3) setLEDMode(MAX30105_MODE_MULTILED); //Watch all three LED channels
  else if (ledMode == 2) setLEDMode(MAX30105_MODE_REDIRONLY); //Red and IR
  else setLEDMode(MAX30105_MODE_REDONLY); //Red only
  activeLEDs = ledMode; //Used to control how many bytes to read from FIFO buffer
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  //Particle Sensing Configuration
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  if(adcRange < 4096) setADCRange(MAX30105_ADCRANGE_2048); //7.81pA per LSB
  else if(adcRange < 8192) setADCRange(MAX30105_ADCRANGE_4096); //15.63pA per LSB
  else if(adcRange < 16384) setADCRange(MAX30105_ADCRANGE_8192); //31.25pA per LSB
  else if(adcRange == 16384) setADCRange(MAX30105_ADCRANGE_16384); //62.5pA per LSB
  else setADCRange(MAX30105_ADCRANGE_2048);

  if (sampleRate < 100) setSampleRate(MAX30105_SAMPLERATE_50); //Take 50 samples per second
  else if (sampleRate < 200) setSampleRate(MAX30105_SAMPLERATE_100);
  else if (sampleRate < 400) setSampleRate(MAX30105_SAMPLERATE_200);
  else if (sampleRate < 800) setSampleRate(MAX30105_SAMPLERATE_400);
  else if (sampleRate < 1000) setSampleRate(MAX30105_SAMPLERATE_800);
  else if (sampleRate < 1600) setSampleRate(MAX30105_SAMPLERATE_1000);
  else if (sampleRate < 3200) setSampleRate(MAX30105_SAMPLERATE_1600);
  else if (sampleRate == 3200) setSampleRate(MAX30105_SAMPLERATE_3200);
  else setSampleRate(MAX30105_SAMPLERATE_50);

  //The longer the pulse width the longer range of detection you'll have
  //At 69us and 0.4mA it's about 2 inches
  //At 411us and 0.4mA it's about 6 inches
  if (pulseWidth < 118) setPulseWidth(MAX30105_PULSEWIDTH_69); //Page 26, Gets us 15 bit resolution
  else if (pulseWidth < 215) setPulseWidth(MAX30105_PULSEWIDTH_118); //16 bit resolution
  else if (pulseWidth < 411) setPulseWidth(MAX30105_PULSEWIDTH_215); //17 bit resolution
  else if (pulseWidth == 411) setPulseWidth(MAX30105_PULSEWIDTH_411); //18 bit resolution
  else setPulseWidth(MAX30105_PULSEWIDTH_69);
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  //LED Pulse Amplitude Configuration
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  //Default is 0x1F which gets us 6.4mA
  //powerLevel = 0x02, 0.4mA - Presence detection of ~4 inch
  //powerLevel = 0x1F, 6.4mA - Presence detection of ~8 inch
  //powerLevel = 0x7F, 25.4mA - Presence detection of ~8 inch
  //powerLevel = 0xFF, 50.0mA - Presence detection of ~12 inch

  setPulseAmplitudeRed(powerLevel);
  setPulseAmplitudeIR(powerLevel);
  setPulseAmplitudeGreen(powerLevel);
  setPulseAmplitudeProximity(powerLevel);
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  //Multi-LED Mode Configuration, Enable the reading of the three LEDs
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  enableSlot(1, SLOT_RED_LED);
  if (ledMode > 1) enableSlot(2, SLOT_IR_LED);
  if (ledMode > 2) enableSlot(3, SLOT_GREEN_LED);
  //enableSlot(1, SLOT_RED_PILOT);
  //enableSlot(2, SLOT_IR_PILOT);
  //enableSlot(3, SLOT_GREEN_PILOT);
  //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  clearFIFO(); //Reset the FIFO before we begin checking the sensor
}

看起来挺长的,但是都是类似与枚举的if-else,不是很复杂。

复位操作

softReset(); //Reset all configuration, threshold, and data registers to POR values

函数代码如下

void MAX30105::softReset(void) {
  bitMask(MAX30105_MODECONFIG, MAX30105_RESET_MASK, MAX30105_RESET);

  // Poll for bit to clear, reset is then complete
  // Timeout after 100ms
  unsigned long startTime = millis();
  while (millis() - startTime < 100)
  {
    uint8_t response = readRegister8(_i2caddr, MAX30105_MODECONFIG);
    if ((response & MAX30105_RESET) == 0) break; //We're done!
    delay(1); //Let's not over burden the I2C bus
  }
}

这个bitMask函数还是挺有意思的,之前做这类操作的时候没有想过用这种方法。

后面的部分就是读取了esp32启动以来的毫秒数,然后做循环,判断这个位是不是设置成功了。
就是等待100毫秒,看复位成功了没有。

话说这个操作方式确实比较精准,误差不会太大。

void MAX30105::bitMask

这个函数是一个位操作的函数,控制一位或者几位的bit的赋值。

代码如下:

void MAX30105::bitMask(uint8_t reg, uint8_t mask, uint8_t thing)
{
  // Grab current register context
  uint8_t originalContents = readRegister8(_i2caddr, reg);

  // Zero-out the portions of the register we're interested in
  originalContents = originalContents & mask;

  // Change contents
  writeRegister8(_i2caddr, reg, originalContents | thing);
}

操作流程如下:

  1. 读取该地址的值
  2. 把读取到的数据和mask做与操作
  3. 写入第二步得到的数值与thing的或运算的值

这个操作一开始看,感觉有点傻,为啥不直接传输thing,然后在里面进行取反操作,何必多次一举,看了其他调用这个函数的代码,我大概搞清楚了。
这个操作不是单纯的对一位bit进行操作,而是对多位进行操作,打个比方
现在有第0位到第2位的bit是代表一个模式设置,这个模式有101,110,111三种。那么就可以传输一个二进制是1111 1000的mask,那与读取出来的值进行与操作之后,就把第0位到第2位的数据清零了,这时thing再传递三种模式的其中一种。这样就可以做到任意位数的赋值,这种方式还挺巧妙的。

但是也能感觉到作者在写mark的数据定义的时候挺烦的,一会2进制赋值,一会16进制赋值。

FIFO配置

FIFO就可以想象成一个队列,先进先出,用于缓存数据的。

代码如下

if (sampleAverage == 1) setFIFOAverage(MAX30105_SAMPLEAVG_1); //No averaging per FIFO record
  else if (sampleAverage == 2) setFIFOAverage(MAX30105_SAMPLEAVG_2);
  else if (sampleAverage == 4) setFIFOAverage(MAX30105_SAMPLEAVG_4);
  else if (sampleAverage == 8) setFIFOAverage(MAX30105_SAMPLEAVG_8);
  else if (sampleAverage == 16) setFIFOAverage(MAX30105_SAMPLEAVG_16);
  else if (sampleAverage == 32) setFIFOAverage(MAX30105_SAMPLEAVG_32);
  else setFIFOAverage(MAX30105_SAMPLEAVG_4);

setFIFOAverage函数中只有bitMask(MAX30105_FIFOCONFIG, MAX30105_SAMPLEAVG_MASK, numberOfSamples);这一段代码

定义的代码如下

static const uint8_t MAX30105_FIFOCONFIG =      0x08;

static const uint8_t MAX30105_SAMPLEAVG_MASK =  (byte)~0b11100000;
static const uint8_t MAX30105_SAMPLEAVG_1 =     0x00;
static const uint8_t MAX30105_SAMPLEAVG_2 =     0x20;
static const uint8_t MAX30105_SAMPLEAVG_4 =     0x40;
static const uint8_t MAX30105_SAMPLEAVG_8 =     0x60;
static const uint8_t MAX30105_SAMPLEAVG_16 =    0x80;
static const uint8_t MAX30105_SAMPLEAVG_32 =    0xA0;

具体表达了什么意思可以看前言里我写的文章里的FIFO配置章节,我在这里也做了部分引用

地址 功能 B7 B6 B5 B4 B3 B2 B1 B0 R/W
0x08 FIFO配置 SMP_AVE[2] SMP_AVE[1] SMP_AVE[0] FIFO_ROL LOVER_EN FIFO_A_FULL[3] FIFO_A_FULL[2] FIFO_A_FULL[1] FIFO_A_FULL[0] RW

SMP_AVE:平均值,为了减少数据吞吐量,通过设置这个寄存器,相邻的样本(在每个单独的通道中)可以在芯片上进行平均和抽取。

SMP_AVE 平均量
000 1(不平均)
001 2
010 4
011 8
100 16
101 32
110 32
111 32

FIFO_ROL LOVER_EN:FIFO被填满之后的控制。如果是0,在你读取之前都不会更新,如果是1,会更新覆盖之前的数据

更新使能

这其实也是FIFO的设置,当设置为1时如果FIFO中的数据满了,那么就会覆盖老的数据,设置为0则不会覆盖。

enableFIFORollover(); //Allow FIFO to wrap/roll over

内部也就是调用了bitMask,代码如下

void MAX30105::enableFIFORollover(void) {
  bitMask(MAX30105_FIFOCONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_ENABLE);
}

同上,可以看前言里的MAX30102分析。

可以看出来,设置的过程是按照功能划分的,更新使能和FIFO配置都是一个寄存器里的内容,却分成了两个部分来写。可读性比较好,但是执行效率就不怎么高了。

LED设置

设置红光和红外光,三种模式,同上,可以看前言里的MAX30102分析。

  if (ledMode == 3) setLEDMode(MAX30105_MODE_MULTILED); //Watch all three LED channels
  else if (ledMode == 2) setLEDMode(MAX30105_MODE_REDIRONLY); //Red and IR
  else setLEDMode(MAX30105_MODE_REDONLY); //Red only
  activeLEDs = ledMode; //Used to control how many bytes to read from FIFO buffer

ADC检测设置

设置ADC的采样范围,具体参数,可以看前言里的MAX30102分析。

  if(adcRange < 4096) setADCRange(MAX30105_ADCRANGE_2048); //7.81pA per LSB
  else if(adcRange < 8192) setADCRange(MAX30105_ADCRANGE_4096); //15.63pA per LSB
  else if(adcRange < 16384) setADCRange(MAX30105_ADCRANGE_8192); //31.25pA per LSB
  else if(adcRange == 16384) setADCRange(MAX30105_ADCRANGE_16384); //62.5pA per LSB
  else setADCRange(MAX30105_ADCRANGE_2048);

SpO2采样率控制

采样率和脉冲宽度是相关的,因为采样率设置了脉冲宽度时间的上限。如果用户选择的采样率对于所选LED_PW设置来说太高,则将尽可能高的采样率编程到寄存器中。具体参数,可以看前言里的MAX30102分析。

  if (sampleRate < 100) setSampleRate(MAX30105_SAMPLERATE_50); //Take 50 samples per second
  else if (sampleRate < 200) setSampleRate(MAX30105_SAMPLERATE_100);
  else if (sampleRate < 400) setSampleRate(MAX30105_SAMPLERATE_200);
  else if (sampleRate < 800) setSampleRate(MAX30105_SAMPLERATE_400);
  else if (sampleRate < 1000) setSampleRate(MAX30105_SAMPLERATE_800);
  else if (sampleRate < 1600) setSampleRate(MAX30105_SAMPLERATE_1000);
  else if (sampleRate < 3200) setSampleRate(MAX30105_SAMPLERATE_1600);
  else if (sampleRate == 3200) setSampleRate(MAX30105_SAMPLERATE_3200);
  else setSampleRate(MAX30105_SAMPLERATE_50);

设置LED脉宽控制和ADC分辨率

这些位设置LED脉冲宽度(IR和Red具有相同的脉冲宽度),因此间接设置每个样本中ADC的积分时间。ADC分辨率与积分时间直接相关。具体参数,可以看前言里的MAX30102分析。

  if (pulseWidth < 118) setPulseWidth(MAX30105_PULSEWIDTH_69); //Page 26, Gets us 15 bit resolution
  else if (pulseWidth < 215) setPulseWidth(MAX30105_PULSEWIDTH_118); //16 bit resolution
  else if (pulseWidth < 411) setPulseWidth(MAX30105_PULSEWIDTH_215); //17 bit resolution
  else if (pulseWidth == 411) setPulseWidth(MAX30105_PULSEWIDTH_411); //18 bit resolution
  else setPulseWidth(MAX30105_PULSEWIDTH_69);

LED脉冲宽度设置

设置脉冲宽度,具体参数,可以看前言里的MAX30102分析。

setPulseAmplitudeRed(powerLevel);
setPulseAmplitudeIR(powerLevel);

非MAX30102有效寄存器

这两个设置在MAX30102中是无效的,因为数据手册中这个地址的寄存器并没有分配功能,但是因为MAX30105是向下兼容的,所以MAX30102使用也不会出问题。

setPulseAmplitudeGreen(powerLevel);
setPulseAmplitudeProximity(powerLevel);

多LED模式控制

 enableSlot(1, SLOT_RED_LED);
if (ledMode > 1) enableSlot(2, SLOT_IR_LED);
if (ledMode > 2) enableSlot(3, SLOT_GREEN_LED);

void MAX30105::enableSlot(uint8_t slotNumber, uint8_t device) {

  uint8_t originalContents;

  switch (slotNumber) {
    case (1):
      bitMask(MAX30105_MULTILEDCONFIG1, MAX30105_SLOT1_MASK, device);
      break;
    case (2):
      bitMask(MAX30105_MULTILEDCONFIG1, MAX30105_SLOT2_MASK, device << 4);
      break;
    case (3):
      bitMask(MAX30105_MULTILEDCONFIG2, MAX30105_SLOT3_MASK, device);
      break;
    case (4):
      bitMask(MAX30105_MULTILEDCONFIG2, MAX30105_SLOT4_MASK, device << 4);
      break;
    default:
      //Shouldn't be here!
      break;
  }
}

如果是MAX30102最大只可以设置到2。

清除FIFO

具体参数,可以看前言里的MAX30102分析。

clearFIFO(); //Reset the FIFO before we begin checking the sensor

小结

先看一下函数内部都做了什么吧

flowchart TD
A(复位操作) --> B(FIFO配置)
B --> C(开启更新使能)
C --> D(LED设置)
D --> E(ADC检测设置)
E --> F(SpO2采样率控制)
F --> G(设置LED脉宽控制和ADC分辨率)
G --> H(LED脉冲宽度设置)
H --> I(多LED模式控制)
I --> J(清除FIFO)

这样看还是比较混乱,可以对照这数据手册把信息采集的流程列一下,图片就不放了,markdown放图片不方便移植

flowchart TD
DIGITAL(总驱动)
LED_DRIVERS(LED驱动)
RED_IR(红灯和红外灯)
VISIBLE+IR(可见加红外的采集二极管)
AMBIENT_LIGHT_CANCELLATION(环境光消除)
DIE_TEMP(模具温度)
ADC1(ADC1)
ADC2(ADC2)
DIGITAL_FILTER(数字滤波)
DATA_REGISTER(数据寄存器)
IIC(IIC通讯)

DIGITAL --> LED_DRIVERS
LED_DRIVERS --> RED_IR
RED_IR --> VISIBLE+IR
VISIBLE+IR --> AMBIENT_LIGHT_CANCELLATION
AMBIENT_LIGHT_CANCELLATION --> ADC1
ADC1 --> DIGITAL_FILTER
DIGITAL_FILTER --> DATA_REGISTER
DATA_REGISTER --> IIC
DIE_TEMP --> ADC2
ADC2 --> DATA_REGISTER

所以LED设置是在设置红外和红灯
LED采样和ADC设置都是在设置模拟信号输入方式
FIFO和更新是在设置数据在数据寄存器的存储方式和规则

MAX30102数据采集

bufferLength = 100; //buffer length of 100 stores 4 seconds of samples running at 25sps

//read the first 100 samples, and determine the signal range
for (byte i = 0 ; i < bufferLength ; i++)
{
  while (particleSensor.available() == false) //do we have new data?
    particleSensor.check(); //Check the sensor for new data
  redBuffer[i] = particleSensor.getRed();
  irBuffer[i] = particleSensor.getIR();
  particleSensor.nextSample(); //We're finished with this sample so move to next sample
  Serial.print(F("red="));
  Serial.print(redBuffer[i], DEC);
  Serial.print(F(", ir="));
  Serial.println(irBuffer[i], DEC);
}

这个部分其实是可以放在setup中的,不知道作者为什么把他放在了loop中。
因为这里在loop中实际只执行了一次,后面作者用了一个while(1)的死循环去执行了其他操作。

particleSensor.available()的作用是判断是否有新的数据写进来。

内部的数据是用数组实现的一个环形链表。
如果没有新的数据,那么就检查是否有新的数据传输进来。

particleSensor.check()就是起到检查是否有新数据的作用。
在函数内部是一直在判断读指针和写指针的数据,如果不相同则是有新的数据过来。

particleSensor.nextSample()就是看是不是有新数据,有新数据就把尾指针加一。

剩下的部分就是把数据打印出来了。

这里的redBufferirBuffer默认都是大小为100的数组。

从这里开始下面的篇幅就不逐一深入到每一个函数里去看了。

这段函数的主要功能就是把redBufferirBuffer里面写满数据,用于后面的误差统计计算。

MAX30102数据计算

  //calculate heart rate and SpO2 after first 100 samples (first 4 seconds of samples)
  maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);

这是一个引用了#include "spo2_algorithm.h"库的计算公式,这个公式内部的计算还是比较复杂的,而且使用起来不是特别稳定。

分析这个函数有些复杂,而且这篇文章到这里也太长了,挖个坑,有时间单独写出来吧。

MAX30102数据计算

和上一个小节题目都一样,因为上两个小节本质上是在做初始化的操作,从功能上看,并不能算作是loop中的内容,其实放在setup中更加合理。

while (1)
  {
    //dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
    for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }

    //take 25 sets of samples before calculating the heart rate.
    for (byte i = 75; i < 100; i++)
    {
      while (particleSensor.available() == false) //do we have new data?
        particleSensor.check(); //Check the sensor for new data

      digitalWrite(readLED, !digitalRead(readLED)); //Blink onboard LED with every data read

      redBuffer[i] = particleSensor.getRed();
      irBuffer[i] = particleSensor.getIR();
      particleSensor.nextSample(); //We're finished with this sample so move to next sample

      //send samples and calculation result to terminal program through UART
      Serial.print(F("red="));
      Serial.print(redBuffer[i], DEC);
      Serial.print(F(", ir="));
      Serial.print(irBuffer[i], DEC);

      Serial.print(F(", HR="));
      Serial.print(heartRate, DEC);

      Serial.print(F(", HRvalid="));
      Serial.print(validHeartRate, DEC);

      Serial.print(F(", SPO2="));
      Serial.print(spo2, DEC);

      Serial.print(F(", SPO2Valid="));
      Serial.println(validSPO2, DEC);
    }

    //After gathering 25 new samples recalculate HR and SP02
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
  }

这样看是挺多的,但是如果把串口输出部分,删除其实也没有多少,代码如下:

while (1)
  {
    //dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
    for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }

    //take 25 sets of samples before calculating the heart rate.
    for (byte i = 75; i < 100; i++)
    {
      while (particleSensor.available() == false) //do we have new data?
        particleSensor.check(); //Check the sensor for new data

      digitalWrite(readLED, !digitalRead(readLED)); //Blink onboard LED with every data read

      redBuffer[i] = particleSensor.getRed();
      irBuffer[i] = particleSensor.getIR();
      particleSensor.nextSample(); //We're finished with this sample so move to next sample
    }

    //After gathering 25 new samples recalculate HR and SP02
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
  }

首先是

for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }

这一部分,这里就是腾挪数据,想象redBufferirBuffer中下标为0是栈底,99是栈顶(因为数组的大小为100,所以在栈顶是99)。这里其实就是把栈底的数据(也就是旧数据,进行了删除),在栈顶空出来25个数据的空间。

for (byte i = 75; i < 100; i++)
    {
      while (particleSensor.available() == false) //do we have new data?
        particleSensor.check(); //Check the sensor for new data

      digitalWrite(readLED, !digitalRead(readLED)); //Blink onboard LED with every data read

      redBuffer[i] = particleSensor.getRed();
      irBuffer[i] = particleSensor.getIR();
      particleSensor.nextSample(); //We're finished with this sample so move to next sample
    }

这里做的是继续获取数据,把栈顶的25个空位填满,最后再送到maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);中去计算。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...