ESP32 и MAX30102: пульсоксиметр и датчик пульса в Arduino IDE

Перевод статьи ESP32 with MAX30102: Pulse Oximeter and Heart Rate Sensor (Arduino IDE) — Rui Santos & Sara Santos, Random Nerd Tutorials.
Это вводное руководство по работе с датчиком пульса и пульсоксиметром MAX30102 на ESP32 в Arduino IDE. Датчик умеет измерять частоту сердечных сокращений, насыщение крови кислородом (SpO₂) и температуру тела, а общается по протоколу I²C.
Разберём, как подключить датчик, и приведём три примера кода: для пульса, для сатурации и для температуры.
Знакомимся с датчиком MAX30102#
Модуль построен вокруг микросхемы MAX30102 — оптического датчика, который неинвазивно измеряет насыщение крови кислородом (SpO₂) и частоту пульса. Внутри есть и датчик температуры: он нужен для температурной компенсации и калибровки.

Модуль MAX30102
В MAX30102 стоят красный светодиод, инфракрасный светодиод и фотодетектор. Светодиоды светят в кожу, а фотодетектор измеряет отражённый свет. Количество отражённого света меняется вместе с кровотоком при каждом ударе сердца — обрабатывая эти изменения, можно вычислить пульс и SpO₂.
Как работает MAX30102#

Принцип измерения пульса и SpO₂
- Излучение света. Красный и инфракрасный светодиоды светят в ткани тела, каждый на своей длине волны.
- Приём отражённого света. Свет проникает в ткани, часть его отражается. Доля отражённого света зависит от объёма крови и уровня её насыщения кислородом; фотодетектор измеряет её.
- Изменения интенсивности. Измеряя вариации интенсивности отражённого света на двух длинах волн — красной и инфракрасной, — датчик различает оксигенированный и дезоксигенированный гемоглобин:
- оксигенированный гемоглобин сильнее поглощает инфракрасный свет;
- дезоксигенированный гемоглобин сильнее поглощает красный свет.
По соотношению интенсивностей отражённого красного и инфракрасного света вычисляется SpO₂, а по изменениям объёма крови с каждым ударом сердца — частота пульса (BPM).
Подключение MAX30102 к ESP32#
Датчик работает по I²C, так что используем стандартные для платы выводы I²C.
| Плата | SDA | SCL |
|---|---|---|
| ESP32 | GPIO 21 | GPIO 22 |
| ESP32-S3 | GPIO 8 | GPIO 9 |
Питание подаётся на вывод VIN — подойдут и 5 В, и 3,3 В.

Схема подключения (подправьте под свою модель платы)
Подготовка Arduino IDE#
Программировать будем в Arduino IDE, поэтому убедитесь, что установлена поддержка плат ESP32:
Если предпочитаете VS Code с PlatformIO — можно работать и там:
Устанавливаем библиотеку SparkFun MAX3010x#
Библиотек для MAX30102 существует несколько; мы возьмём SparkFun MAX3010x — она хорошо работает с этим датчиком и проста в использовании.
Откройте менеджер библиотек Arduino IDE, найдите SparkFun MAX3010x и установите библиотеку авторства SparkFun.

Менеджер библиотек Arduino IDE
1. Измеряем пульс#
Следующий скетч получает частоту сердечных сокращений и выводит её в монитор порта.
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
MAX30105 sensor;
// Настройки
const byte RATE_SIZE = 5; // сколько ударов усредняем
byte rates[RATE_SIZE]; // последние корректные значения BPM
byte rateSpot = 0;
byte validBeats = 0;
long lastBeat = 0;
float currentBPM = 0;
int averageBPM = 0;
unsigned long lastPrint = 0;
// Принимаем только правдоподобные значения пульса (подстройте под себя)
const float MIN_BPM = 45;
const float MAX_BPM = 180;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Heart Rate Sensor");
Serial.println();
Serial.println("Place your finger on the sensor...");
Serial.println();
// Инициализация датчика
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
sensor.setup(); // настройки по умолчанию
sensor.setPulseAmplitudeRed(0); // красный светодиод не нужен — гасим
}
void loop() {
long irValue = sensor.getIR(); // читаем инфракрасный канал
// Проверяем, был ли удар сердца
if (checkForBeat(irValue)) {
long timeBetweenBeats = millis() - lastBeat;
lastBeat = millis();
currentBPM = 60.0 / (timeBetweenBeats / 1000.0);
// Оставляем только правдоподобные значения
if (currentBPM >= MIN_BPM && currentBPM <= MAX_BPM) {
rates[rateSpot] = (byte)currentBPM;
rateSpot++;
if (rateSpot >= RATE_SIZE){
rateSpot = 0;
}
// Считаем корректные удары, пока буфер не заполнится
if (validBeats < RATE_SIZE) {
validBeats++;
}
// Среднее считаем только по полному буферу
if (validBeats >= RATE_SIZE) {
averageBPM = 0;
for (byte i = 0; i < RATE_SIZE; i++) {
averageBPM += rates[i];
}
averageBPM /= RATE_SIZE;
}
}
}
// Печатаем раз в секунду
if (millis() - lastPrint >= 1000) {
lastPrint = millis();
if (irValue < 50000) {
// Палец не приложен
Serial.println("Waiting for finger...");
}
else if (averageBPM == 0) {
// Палец есть, но корректных ударов пока мало
Serial.println("Measuring... keep your finger still");
}
else {
Serial.print("Heart Rate: ");
Serial.print(averageBPM);
Serial.println(" BPM");
}
}
}
Как датчик определяет пульс#

Красный и инфракрасный светодиоды плюс фотодетектор
Светодиоды светят сквозь кончик пальца, а текущая кровь поглощает свет. Когда сердце сокращается, в сосуды пальца выталкивается больше крови — и света поглощается больше.
Для пульса нам важен именно инфракрасный канал:
- удар сердца — крови больше, инфракрасного света поглощается больше, до фотодетектора доходит меньше;
- между ударами — крови меньше, поглощается меньше, до фотодетектора доходит больше.
Изменения интенсивности складываются в форму волны. Замеряя время между её пиками, мы и получаем частоту пульса.
Как работает код#
Подключаем библиотеки: Wire.h для I²C, MAX30105.h для самого датчика и heartRate.h с алгоритмом детекции ударов. Создаём объект sensor.
В setup() датчик инициализируется вызовом sensor.begin(Wire, I2C_SPEED_FAST), затем sensor.setup() применяет настройки по умолчанию, а setPulseAmplitudeRed(0) гасит красный светодиод — для пульса он не нужен.
В loop() читается инфракрасный канал sensor.getIR(), и функция checkForBeat() сообщает, был ли удар. По интервалу между ударами считается мгновенное значение BPM. Оно попадает в кольцевой буфер только если укладывается в разумный диапазон (45–180 ударов) — это простейший фильтр от артефактов. Среднее по буферу считается лишь тогда, когда набралось пять корректных ударов. Отдельно проверяется значение инфракрасного канала: если оно меньше 50 000, значит, палец к датчику не приложен.
Проверка#
Загрузите скетч на плату и откройте монитор порта на скорости 115200.
Приложите палец к датчику. Для точных показаний палец должен лежать неподвижно и с постоянным нажимом — удобно прихватить датчик к пальцу резинкой.

Палец должен лежать неподвижно
В мониторе порта пойдут значения BPM.

Измеренный пульс
2. Измеряем насыщение крови кислородом (SpO₂)#
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"
MAX30105 sensor;
// Буферы, нужные алгоритму
#define BUFFER_SIZE 100
uint32_t irBuffer[BUFFER_SIZE];
uint32_t redBuffer[BUFFER_SIZE];
int32_t spo2; // значение сатурации
int8_t validSPO2; // 1 — измерение корректно, 0 — пока нет
int32_t heartRate; // не используется, но функция его требует
int8_t validHeartRate;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Oxygen Saturation");
Serial.println();
Serial.println("Place your finger on the sensor...");
Serial.println();
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
byte ledBrightness = 60; // 0 = выкл, 255 = 50 мА
byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32
byte ledMode = 2; // 1 = только красный, 2 = красный + ИК, 3 = + зелёный
byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; // 69, 118, 215, 411
int adcRange = 4096; // 2048, 4096, 8192, 16384
sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
}
void loop() {
// Набираем 100 отсчётов (занимает несколько секунд)
for (byte i = 0; i < BUFFER_SIZE; i++) {
while (sensor.available() == false) {
sensor.check();
}
redBuffer[i] = sensor.getRed();
irBuffer[i] = sensor.getIR();
sensor.nextSample();
}
// Считаем SpO2
maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_SIZE, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
if (validSPO2) {
Serial.print("Oxygen Saturation: ");
Serial.print(spo2);
Serial.println(" %");
} else {
Serial.println("Measuring... keep your finger still");
}
Serial.println();
}
Здесь вместо heartRate.h подключается spo2_algorithm.h, а датчик настраивается вручную: яркость светодиодов, усреднение выборок, режим светодиодов (нужны оба — красный и инфракрасный), частота выборки, длительность импульса и диапазон АЦП.
В loop() программа набирает по сто отсчётов красного и инфракрасного каналов, а затем передаёт оба буфера в maxim_heart_rate_and_oxygen_saturation(). Функция возвращает и пульс, и сатурацию, но нас интересует только spo2; флаг validSPO2 говорит, можно ли доверять результату.

Насыщение крови кислородом в процентах
3. Измеряем температуру#
В MAX30102 есть датчик температуры — он нужен для калибровки значений BPM и SpO₂, но его можно использовать и напрямую, чтобы получить температуру тела (точнее, кончика пальца).
#include <Wire.h>
#include "MAX30105.h"
MAX30105 sensor;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Temperature Sensor");
Serial.println();
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
// Гасим светодиоды, чтобы они не грели датчик
sensor.setup(0); // 0 = светодиоды выключены
sensor.enableDIETEMPRDY(); // включаем сигнал готовности измерения температуры
}
void loop() {
float tempC = sensor.readTemperature();
float tempF = tempC * 1.8 + 32.0;
Serial.print("Temperature: ");
Serial.print(tempC, 2);
Serial.print(" °C | ");
Serial.print(tempF, 2);
Serial.println(" °F");
delay(1000);
}
Обратите внимание на две строчки в setup(): sensor.setup(0) полностью гасит светодиоды, чтобы они не подогревали кристалл и не портили измерение, а enableDIETEMPRDY() включает сигнал готовности температурного канала.
Закрепите датчик на пальце резинкой и откройте монитор порта на 115200. Показания пойдут раз в секунду — дайте им несколько секунд, чтобы стабилизироваться.

Температура кончика пальца
Итог#
Мы подключили MAX30102 к ESP32 и сняли с него три показателя: пульс, насыщение крови кислородом и температуру. Датчик хорошо подходит для носимых и «медицинских» самоделок — с оговоркой, что это не сертифицированный прибор и полагаться на его показания в диагностике нельзя.
Дальше проект логично развивать в сторону отображения данных — например, собрать асинхронный веб-сервер, который покажет показания в браузере, или вывести их на графики со статистикой.