
手腕上的智能手表能测心率、测血氧,靠的大多是一颗指甲盖大小的光学传感器。这颗叫 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 可以先不接。

接线:只用四根线,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 逻辑的主控才需要改。上拉电阻模块已经带好,总线直接挂就行。


装库:认准 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?,这是判断有没有人把手指放上去的粗门槛,顺手就能改成检测手指离开自动暂停的功能。

放手指的姿势,决定读数能不能看
光学测量对接触方式很敏感,读数飘的时候按下面几条排查:
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 的那轮数据说明信号质量不够,别当真值用。

把丑话说在前面:它不是医疗设备
MAX30102 靠光反射取脉搏,手指晃动、按压力度变化、环境光干扰、指甲油、低灌注状态都会把读数带偏,原厂定位就是学习和实验工具,官方文档也明确提醒它不适合医疗场景。这个模块适合做桌面监测实验、课程设计、给机器人加个人体存在检测,所有 BPM 和 SpO2 数据只能作实验参考,不能用于任何健康判断。
玩到后面还可以解锁两个附赠功能:芯片内置的温度传感器能当快速温度计用(读芯片结温),把 LED 全关后调用 readTemperature() 即可;另外把红外通道当接近传感器,开机先取 32 次平均值做基线,之后检测到偏差超过阈值就报"有东西",手在上方挥一下就能触发,给桌面装置做无接触交互挺方便。18 位 ADC 的灵敏度摆在那,几十厘米内的小动作都逃得过。











血氧那段提醒得对,这东西当实验玩具就行,真要测还得看医疗认证的设备
这段代码我直接抄下来试了,例程1读原始值那步很关键,先看波形再上算法能少走很多弯路。之前我就是跳过这步直接算BPM,读数飘得没法看,后来发现是指腹压太重把血流掐了,调整力度之后Avg BPM基本和手环对得上,差两三下以内。
IR
刚到手的模块一直报 not found,原来是SDA接成A5了,看完接线图才反应过来,感谢