MAX30102传感器接Arduino:读BPM与SpO2

2026-11-12 20:28:47 7阅读 所属项目:ESP32 开发板

MAX30102心率血氧模块实拍图

手腕上的智能手表能测心率、测血氧,靠的大多是一颗指甲盖大小的光学传感器。这颗叫 MAX30102 的芯片来自 Analog Devices,是老款 MAX30100 的升级版,两颗 LED 加一个光电探测器,走 I2C 总线,接 Arduino 就能把 BPM 和 SpO2 数据读出来。这篇按接线、装库、逐级验证的顺序把流程理一遍,代码全部来自 SparkFun 官方库的例程,先摆数据再动手,新手照着做一遍就能跑通。

先认识模块:一颗芯片管两束光

MAX30102 内部有红色和红外两颗 LED,波长分别是 660nm 和 880nm,配合光电探测器把反射回来的光强转成数字量。芯片供电要 1.8V、LED 要 3.3V,模块上已经带了稳压器,所以只需要给模块一路 3.3V 到 5.5V 的电源。正常测量电流不到 600μA,待机只有 0.7μA,做电池供电的可穿戴实验很合适。

几个关键规格列一下:供电 3.3V 到 5.5V;红光 660nm、红外 880nm;板载温度传感器量程 -40°C 到 +85°C,精度 ±1°C;片上 ADC 是 18 位,共 262144 级;FIFO 缓存最多存 32 组测量数据,主控忙别的也不会丢样本。

通信是标准 I2C:写地址 0xAE,读地址 0xAF,固定不可改,所以一条总线上挂别的器件要注意地址冲突。芯片还支持 5 类中断(上电就绪、新数据就绪、环境光过强、温度就绪、FIFO 将满),都从 INT 引脚输出,平时高电平,触发时拉低。这篇的例程用轮询方式读数据,INT 可以先不接。

MAX30102光电容积脉搏波原理示意图

接线:只用四根线,SCL 对 A5、SDA 对 A4

模块引脚从电源到地一共 7 个,逐个说清楚:

1. VIN:电源输入,接 3.3V 或 5V,跟主控逻辑电平保持一致,Arduino Uno 这类 5V 板就接 5V;
2. GND:地,接 Arduino 的 GND;
3. SCL:I2C 时钟线,接 Uno 的 SCL(即 A5);
4. SDA:I2C 数据线,接 Uno 的 SDA(即 A4);
5. INT:中断输出,本篇例程不接;
6. IRD 和 RD:分别是红外 LED 和红光 LED 的内部接地端,芯片内部已经处理,悬空即可。

背面的焊接跳线默认选 3.3V 逻辑,接 Arduino 和 ESP32 都不用动;只有接 1.8V 逻辑的主控才需要改。上拉电阻模块已经带好,总线直接挂就行。

MAX30102模块引脚定义图

MAX30102接Arduino Uno的接线图

装库:认准 SparkFun 那一个

自己写寄存器配置太折腾,直接用 SparkFun Electronics 的官方库。打开 Arduino IDE,点左侧 Library Manager 图标,搜索框输入 MAX3010x,找到 SparkFun MAX3010x Pulse and Proximity Sensor Library,点 Install。装完就能跑下面的例程。另外说明一下:例程里的 #include "Wire.h" 原写法是尖括号包 Wire.h,两种写法在 Arduino 工具链下等价,这里统一用引号写法。

第一步:先读红光和红外的原始值

拿到传感器别急着测心率,先确认它活着、数据在动。下面这段把红光和红外两路原始值打到串口:

#include "Wire.h"
#include "MAX30105.h"

MAX30105 particleSensor;

void setup() {
  Serial.begin(9600);

  // Initialize sensor
  if (particleSensor.begin() == false) {
    Serial.println("MAX30102 was not found. Please check wiring/power.");
    while (1)
      ;
  }

  particleSensor.setup();  //Configure sensor. Use 6.4mA for LED drive
}

void loop() {
  Serial.print(" R[");
  Serial.print(particleSensor.getRed());
  Serial.print("] IR[");
  Serial.print(particleSensor.getIR());
  Serial.println("]");
}

上传后打开串口监视器,传感器朝上,手掌在上方晃动,两组数字会跟着变化;把指腹轻轻贴上去,数字会出现规律起伏。想看得更直观,把 loop() 换成下面两行,再用 Tools 菜单里的 Serial Plotter 画出来:

void loop() {
  Serial.print(particleSensor.getRed());
  Serial.print(", ");
  Serial.println(particleSensor.getIR());
}

绘图仪上会出现两条曲线,手指贴稳之后,红外那一路的波形就是你的脉搏波,一个波峰对应一次心跳。

第二步:算心率 BPM

确认波形正常后,用库自带的 heartRate.h 做峰值检测,滑动平均 4 次读数输出 BPM。代码原样贴上:

#include "Wire.h"
#include "MAX30105.h"
#include "heartRate.h"

MAX30105 particleSensor;

const byte RATE_SIZE = 4;  //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE];     //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0;  //Time at which the last beat occurred

float beatsPerMinute;
int beatAvg;

void setup() {
  Serial.begin(9600);
  Serial.println("Initializing...");

  // Initialize sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("MAX30102 was not found. Please check wiring/power. ");
    while (1)
      ;
  }
  Serial.println("Place your index finger on the sensor with steady pressure.");

  particleSensor.setup();                     //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);  //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);   //Turn off Green LED
}

void loop() {
  long irValue = particleSensor.getIR();

  if (checkForBeat(irValue) == true) {
    //We sensed a beat!
    long delta = millis() - lastBeat;
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);

    if (beatsPerMinute < 255 && beatsPerMinute>  20) {
      rates[rateSpot++] = (byte)beatsPerMinute;  //Store this reading in the array
      rateSpot %= RATE_SIZE;                     //Wrap variable

      //Take average of readings
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }

  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);

  if (irValue < 50000)
    Serial.print(" No finger?");

  Serial.println();
}

手指轻贴传感器、保持不动,等几秒钟 Avg BPM 就会稳定下来。程序里还有个小细节:IR 值低于 50000 时提示 No finger?,这是判断有没有人把手指放上去的粗门槛,顺手就能改成检测手指离开自动暂停的功能。

串口监视器输出的心率BPM数据

放手指的姿势,决定读数能不能看

光学测量对接触方式很敏感,读数飘的时候按下面几条排查:

1. 不能按太重:指腹压太狠会掐断皮下血流,脉搏波直接变形;
2. 也不能太轻:漏进环境光会引入噪声,波形抖成毛刺;
3. 力度找个中间值,感觉像手指间轻轻捏着一枚硬币;
4. 手指容易酸的话,用橡皮筋或胶带把手指固定在传感器上,比悬空稳得多;
5. 换别的部位也行,耳垂、下唇这类皮肤薄、血流丰富的地方同样能测。

第三步:算血氧 SpO2

血氧的原理是把两束光的吸收差算成一个百分比:含氧血红蛋白吸收 880nm 红外更多,脱氧血红蛋白吸收 660nm 红光更多,对比两种光的吸收量就得到 SpO2。健康人一般在 95% 到 100% 之间。下面是库自带的 spo2_algorithm 例程,逻辑是先攒 100 个样本(约 4 秒),之后每秒滚动计算一次心率和血氧:

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

MAX30105 particleSensor;

#define MAX_BRIGHTNESS 255

#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
//Arduino Uno doesn't have enough SRAM to store 100 samples of IR led data and red led data in 32-bit format
//To solve this problem, 16-bit MSB of the sampled data will be truncated. Samples become 16-bit data.
uint16_t irBuffer[100];   //infrared LED sensor data
uint16_t redBuffer[100];  //red LED sensor data
#else
uint32_t irBuffer[100];   //infrared LED sensor data
uint32_t redBuffer[100];  //red LED sensor data
#endif

int32_t bufferLength;   //data length
int32_t spo2;           //SPO2 value
int8_t validSPO2;       //indicator to show if the SPO2 calculation is valid
int32_t heartRate;      //heart rate value
int8_t validHeartRate;  //indicator to show if the heart rate calculation is valid

byte pulseLED = 11;  //Must be on PWM pin
byte readLED = 13;   //Blinks with each data read

void setup() {
  Serial.begin(9600);  // initialize serial communication at 9600 bits per second:

  pinMode(pulseLED, OUTPUT);
  pinMode(readLED, OUTPUT);

  // Initialize sensor
  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)
      ;
  }

  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();

  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
}

void loop() {
  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);
  }

  //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);

  //Continuously taking samples from MAX30102.  Heart rate and SpO2 are calculated every 1 second
  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);
  }
}

程序用橡皮筋把手指固定后按任意键开始,串口会输出 red、ir、HR、SPO2 和各自的 valid 标志位。注意两点:一是 Uno 的 SRAM 只有 2KB,例程里专门用条件编译把缓冲区降成 16 位才放得下,ESP32 这类大内存板走 32 位分支;二是看 SPO2Valid 位,为 0 的那轮数据说明信号质量不够,别当真值用。

串口输出的心率与血氧SpO2数据

把丑话说在前面:它不是医疗设备

MAX30102 靠光反射取脉搏,手指晃动、按压力度变化、环境光干扰、指甲油、低灌注状态都会把读数带偏,原厂定位就是学习和实验工具,官方文档也明确提醒它不适合医疗场景。这个模块适合做桌面监测实验、课程设计、给机器人加个人体存在检测,所有 BPM 和 SpO2 数据只能作实验参考,不能用于任何健康判断。

玩到后面还可以解锁两个附赠功能:芯片内置的温度传感器能当快速温度计用(读芯片结温),把 LED 全关后调用 readTemperature() 即可;另外把红外通道当接近传感器,开机先取 32 次平均值做基线,之后检测到偏差超过阈值就报"有东西",手在上方挥一下就能触发,给桌面装置做无接触交互挺方便。18 位 ADC 的灵敏度摆在那,几十厘米内的小动作都逃得过。


作者:数据媛小满
链接:https://www.aiddithome.com/p/1df1504dca50e.html
本作版权归作者所有,未经允许请勿转载。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
4条评论
只看不说话
1楼 · 19分钟前

血氧那段提醒得对,这东西当实验玩具就行,真要测还得看医疗认证的设备

板砖搬不动
2楼 · 19分钟前

这段代码我直接抄下来试了,例程1读原始值那步很关键,先看波形再上算法能少走很多弯路。之前我就是跳过这步直接算BPM,读数飘得没法看,后来发现是指腹压太重把血流掐了,调整力度之后Avg BPM基本和手环对得上,差两三下以内。

线比代码多
4楼 · 19分钟前

刚到手的模块一直报 not found,原来是SDA接成A5了,看完接线图才反应过来,感谢