Перевод статьи 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

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

Как работает MAX30102#

Схема работы датчика MAX30102

Принцип измерения пульса и SpO₂

  1. Излучение света. Красный и инфракрасный светодиоды светят в ткани тела, каждый на своей длине волны.
  2. Приём отражённого света. Свет проникает в ткани, часть его отражается. Доля отражённого света зависит от объёма крови и уровня её насыщения кислородом; фотодетектор измеряет её.
  3. Изменения интенсивности. Измеряя вариации интенсивности отражённого света на двух длинах волн — красной и инфракрасной, — датчик различает оксигенированный и дезоксигенированный гемоглобин:
    • оксигенированный гемоглобин сильнее поглощает инфракрасный свет;
    • дезоксигенированный гемоглобин сильнее поглощает красный свет.

По соотношению интенсивностей отражённого красного и инфракрасного света вычисляется SpO₂, а по изменениям объёма крови с каждым ударом сердца — частота пульса (BPM).

Подключение MAX30102 к ESP32#

Датчик работает по I²C, так что используем стандартные для платы выводы I²C.

ПлатаSDASCL
ESP32GPIO 21GPIO 22
ESP32-S3GPIO 8GPIO 9

Питание подаётся на вывод VIN — подойдут и 5 В, и 3,3 В.

Схема подключения MAX30102 к ESP32

Схема подключения (подправьте под свою модель платы)

Подготовка Arduino IDE#

Программировать будем в Arduino IDE, поэтому убедитесь, что установлена поддержка плат ESP32:

Если предпочитаете VS Code с PlatformIO — можно работать и там:

Устанавливаем библиотеку SparkFun MAX3010x#

Библиотек для MAX30102 существует несколько; мы возьмём SparkFun MAX3010x — она хорошо работает с этим датчиком и проста в использовании.

Откройте менеджер библиотек Arduino IDE, найдите SparkFun MAX3010x и установите библиотеку авторства SparkFun.

Установка библиотеки SparkFun MAX3010x в Arduino IDE

Менеджер библиотек 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");
    }
  }
}

Как датчик определяет пульс#

Микросхема MAX30102 крупным планом

Красный и инфракрасный светодиоды плюс фотодетектор

Светодиоды светят сквозь кончик пальца, а текущая кровь поглощает свет. Когда сердце сокращается, в сосуды пальца выталкивается больше крови — и света поглощается больше.

Для пульса нам важен именно инфракрасный канал:

  • удар сердца — крови больше, инфракрасного света поглощается больше, до фотодетектора доходит меньше;
  • между ударами — крови меньше, поглощается меньше, до фотодетектора доходит больше.

Изменения интенсивности складываются в форму волны. Замеряя время между её пиками, мы и получаем частоту пульса.

Как работает код#

Подключаем библиотеки: 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.

Приложите палец к датчику. Для точных показаний палец должен лежать неподвижно и с постоянным нажимом — удобно прихватить датчик к пальцу резинкой.

Датчик MAX30102, закреплённый на пальце

Палец должен лежать неподвижно

В мониторе порта пойдут значения 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 и сняли с него три показателя: пульс, насыщение крови кислородом и температуру. Датчик хорошо подходит для носимых и «медицинских» самоделок — с оговоркой, что это не сертифицированный прибор и полагаться на его показания в диагностике нельзя.

Дальше проект логично развивать в сторону отображения данных — например, собрать асинхронный веб-сервер, который покажет показания в браузере, или вывести их на графики со статистикой.