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
Connect the board with a USB Type-C data cable.
Open
6.HeartRate.inoin Arduino IDE.In the Tools menu, select
ESP32S3 Dev Moduleas the board and choose the correct serial port.Click Upload.
Open Serial Monitor at
115200baud. You should see:Initializing MAX30102... MAX30102 ready. Place your finger gently on the sensor and keep still.
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_THRESHOLDandFINGER_OFF_THRESHOLDvaluesIncrease
RATE_AVG_SIZEfrom 8 to 16 for smoother averagingGraph the raw IR values in Arduino IDE’s Serial Plotter to visualize your pulse waveform