1.6 Heart Rate Sensor

Read pulse data from the MAX30102 heart rate sensor and calculate BPM.

Overview

This example interfaces with the MAX30102 optical heart rate sensor over I2C. It reads infrared (IR) reflection data from your fingertip, detects individual heartbeats, and computes a rolling average of beats per minute (BPM).

Note

This example is based on the open-source Example5_HeartRate example from the SparkFun MAX3010x sensor library. For the LAFVIN ESP32-S3 Multimedia Kit, we adapted the I2C pin mapping and sensor setup, then added finger-detection debounce, outlier filtering, status reporting, and state reset logic to make the readings more stable for learning and demonstration.

What You’ll Learn

  • Using I2C sensor libraries (SparkFun MAX3010x)

  • Real-time sensor data processing

  • Beat detection and BPM calculation with validation

Before You Begin

Complete the setup steps in Preparation before continuing.

Note

Use the recommended Arduino IDE settings in Recommended Board Settings unless this lesson says otherwise.

Open the example sketch from the code package:

LAFVIN-ESP32-S3-Multimedia-Kit-main/1.Arduino/6.HeartRate/6.HeartRate.ino

This example uses the onboard MAX30102 sensor. No external wiring is required.

Signal

ESP32-S3

SDA

GPIO 2

SCL

GPIO 1

Upload and Test

  1. Connect the board with a USB Type-C data cable.

  2. Open 6.HeartRate.ino in Arduino IDE.

  3. In the Tools menu, select ESP32S3 Dev Module as the board and choose the correct serial port.

  4. Click Upload.

  5. Open Serial Monitor at 115200 baud. You should see:

    Initializing MAX30102...
    MAX30102 ready.
    Place your finger gently on the sensor and keep still.
    
  6. Gently place your fingertip on the MAX30102 sensor. After a few seconds, you should see BPM readings:

    Finger detected. Measuring heart rate...
    BPM=72.5, Avg BPM=73, IR(raw)=155000
    

Note

Keep your finger still and apply gentle, consistent pressure. The sensor needs a few seconds to stabilize. If you see Signal too weak or No finger, adjust your finger position.

How the Code Works

Sensor Initialization

The sketch starts I2C on GPIO 2 and GPIO 1, then configures the MAX30102 with the sampling parameters used for this board.

Wire.begin(IIC_SDA, IIC_SCL);

if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
  Serial.println("MAX30102/MAX30105 was not found. Please check wiring and power.");
  while (1) {
    delay(10);
  }
}

particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
particleSensor.setPulseAmplitudeGreen(0);

Finger Detection

The sketch uses debounce counters so a finger is only considered present or removed after several stable samples.

if (lastIrValue >= FINGER_ON_THRESHOLD) {
  if (fingerOnCount < FINGER_DEBOUNCE_SAMPLES) {
    fingerOnCount++;
  }
  fingerOffCount = 0;
  if (!fingerPresent && fingerOnCount >= FINGER_DEBOUNCE_SAMPLES) {
    handleFingerDetected();
  }
} else if (lastIrValue <= FINGER_OFF_THRESHOLD) {
  if (fingerOffCount < FINGER_DEBOUNCE_SAMPLES) {
    fingerOffCount++;
  }
  fingerOnCount = 0;
  if (fingerPresent && fingerOffCount >= FINGER_DEBOUNCE_SAMPLES) {
    handleFingerRemoved();
  }
}

BPM Calculation

Each detected beat is converted into BPM, then checked before it is accepted into the rolling average.

void processHeartRate(uint32_t irValue) {
  if (!checkForBeat(irValue)) {
    return;
  }

  const long nowMs = millis();
  if (lastBeatMs == 0) {
    lastBeatMs = nowMs;
    return;
  }

  const float bpm = 60.0f / ((nowMs - lastBeatMs) / 1000.0f);
  lastBeatMs = nowMs;

  if (!isReasonableBeat(bpm)) {
    return;
  }

  beatsPerMinute = bpm;
  updateAverageRate((int)(bpm + 0.5f));
}

Status Reporting

The sketch prints different status messages depending on whether no finger is detected, the signal is weak, or stable BPM data is available.

void printStatus() {
  if (!fingerPresent) {
    Serial.println("status=No finger");
    return;
  }

  if (lastIrValue < FINGER_ON_THRESHOLD) {
    Serial.println("status=Signal too weak");
    return;
  }

In the provided sketch:

  • The green LED is disabled, so the sensor uses the IR channel for heart rate measurement

  • BPM values outside the valid range are discarded

  • Readings that differ too much from the rolling average are also rejected

  • Status information is reported once per second

Try This Next

  • Experiment with the FINGER_ON_THRESHOLD and FINGER_OFF_THRESHOLD values

  • Increase RATE_AVG_SIZE from 8 to 16 for smoother averaging

  • Graph the raw IR values in Arduino IDE’s Serial Plotter to visualize your pulse waveform