前言
主要介绍的是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);
}
操作流程如下:
- 读取该地址的值
- 把读取到的数据和mask做与操作
- 写入第二步得到的数值与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()
就是看是不是有新数据,有新数据就把尾指针加一。
剩下的部分就是把数据打印出来了。
这里的redBuffer
和irBuffer
默认都是大小为100的数组。
从这里开始下面的篇幅就不逐一深入到每一个函数里去看了。
这段函数的主要功能就是把redBuffer
和irBuffer
里面写满数据,用于后面的误差统计计算。
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];
}
这一部分,这里就是腾挪数据,想象redBuffer
和irBuffer
中下标为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);
中去计算。