max30102程式碼分析總篇

一叁五發表於2024-05-09

前言

主要介紹的是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);中去計算。

相關文章