The Verdict: Which Heart Rate Sensor Module to Pick

When building a heart rate sensor Arduino project, the market is flooded with optical sensor modules that look identical but perform vastly differently. Optical heart rate monitoring relies on photoplethysmography (PPG)—shining light into the skin and measuring the reflection changes as blood volume pulses. The quality of the analog-to-digital converter (ADC) and the LED driver circuitry dictates whether you get a clean waveform or useless noise.

Use this decision path to select the correct module for your workbench:

Condition / Requirement Sensor to Pick Why
Need clinical-grade SpO2 + HR with low motion artifact MAX30102 (GY-MAX30102) 18-bit ADC, 32-bit FIFO, dedicated ambient light cancellation.
Just need simple analog BPM for a school project, no I2C Pulse Sensor Amped Analog output, simple threshold code, but fails under motion.
Found a cheap MAX30100 on AliExpress / Amazon Do Not Buy Hardware design flaw: internal LED driver conflicts with I2C pull-ups.

Default Pick: The MAX30102. It is the current industry standard for wearable prototyping. We will use the GY-MAX30102 breakout board for this guide, paired with a logic level shifter to protect its sensitive 1.8V I2C bus.

Parts List and Build Specifications

To replicate this exact build, gather the following components. Do not substitute the microcontroller without adjusting the logic level shifting strategy.

Component Exact Variant / Model Notes & Pricing (Approx.)
Microcontroller Arduino Nano v3 (ATmega328P, 5V logic) The classic 5V board. Requires level shifting.
Sensor Module GY-MAX30102 Breakout Board Ensure it has the MAX30102 chip, not the MAX30100.
Logic Level Shifter BSS138 Bi-directional I2C Shifter Mandatory for 5V to 3.3V/1.8V I2C translation.
Wiring 22 AWG solid core jumper wires Keep I2C runs under 10cm to reduce capacitance.

Difficulty Rating: Intermediate. The primary challenge is not the C++ code, but the I2C voltage translation. The MAX30102 operates its internal logic at 1.8V, though many breakout boards include an onboard 3.3V LDO. Feeding 5V from an Arduino Nano directly into the sensor's SDA/SCL pins will forward-bias the internal ESD protection diodes, causing the I2C bus to lock up or permanently damaging the silicon.

Pin Mapping and Wiring Steps

The wiring routes the Nano's 5V I2C lines through the BSS138 level shifter before reaching the sensor. The sensor's interrupt pin is optional but highly recommended for precise timing without blocking the main loop.

Arduino Nano v3 Pin Level Shifter (HV Side) Level Shifter (LV Side) MAX30102 Breakout Pin
5V HV LV VIN (or 3V3 if no onboard LDO)
GND GND (HV) GND (LV) GND
A4 (SDA) HV1 LV1 SDA
A5 (SCL) HV2 LV2 SCL
D2 (INT0) Direct N/A INT (Optional, use voltage divider)

Wiring Steps:

  1. De-energize the circuit: Ensure the Nano is unplugged from USB before making I2C connections.
  2. Power the Level Shifter: Connect Nano 5V to the HV pin and Nano GND to the HV GND pin. Connect the MAX30102 3.3V/1.8V supply to the LV pin and LV GND.
  3. Route I2C Lines: Connect Nano A4 to HV1, and LV1 to Sensor SDA. Connect Nano A5 to HV2, and LV2 to Sensor SCL.
  4. Verify Pull-ups: Most GY-MAX30102 boards have 4.7kΩ pull-up resistors to 3.3V. This is correct. If your level shifter also has pull-ups, you may need to desolder the ones on the sensor board to keep the parallel resistance above 2kΩ, ensuring clean I2C rise times.

Complete Arduino Code with Error Handling

This code targets the Arduino Nano v3 (ATmega328P, 5V logic). Before compiling, open the Arduino Library Manager and install the SparkFun MAX3010x library. Note that the library uses the MAX30105 class for both the MAX30102 and MAX30105 chips because they share an identical I2C register map.

// Install "SparkFun MAX3010x" via Arduino Library Manager
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"

MAX30105 particleSensor;

// Pin definitions for Arduino Nano v3 (ATmega328P)
#define I2C_SDA_PIN A4
#define I2C_SCL_PIN A5
#define SENSOR_INT_PIN 2 // Hardware interrupt 0

const byte RATE_SIZE = 4; // Average of last 4 heart rates
byte rates[RATE_SIZE];
byte rateSpot = 0;
long lastBeat = 0;
float beatsPerMinute;
int beatAvg;

void setup() {
  Serial.begin(115200);
  
  // Initialize I2C with explicit pins for Nano
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setClock(400000); // 400kHz I2C

  // Initialize sensor with error handling
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("MAX30105 was not found. Please check wiring/power.");
    while (1); // Halt execution
  }

  // Configure sensor parameters for MAX30102
  byte ledMode = 2; // 2 = Red + IR (Heart Rate only)
  int sampleRate = 400; // Samples per second
  int pulseWidth = 411; // ADC resolution / integration time
  int sampleAvg = 4; // Hardware averaging
  
  particleSensor.setup(0x1F, 4, 2, sampleRate, pulseWidth, sampleAvg);
  
  // Fine-tune LED power (adjust based on skin tone/thickness)
  particleSensor.setPulseAmplitudeRed(0x1F);
  particleSensor.setPulseAmplitudeIR(0x1F);
  
  Serial.println("Place your finger on the sensor. Steady pressure.");
}

void loop() {
  long irValue = particleSensor.getIR();

  if (checkForBeat(irValue) == true) {
    long delta = millis() - lastBeat;
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);

    if (beatsPerMinute < 255 && beatsPerMinute > 20) {
      rates[rateSpot++] = (byte)beatsPerMinute;
      rateSpot %= RATE_SIZE;

      beatAvg = 0;
      for (byte x = 0 ; x < RATE_SIZE ; x++) {
        beatAvg += rates[x];
      }
      beatAvg /= RATE_SIZE;
    }
  }

  // Output data for Serial Plotter
  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();
}

Debugging: I2C Failures and Sensor Detection

If your serial monitor outputs the exact error string: "MAX30105 was not found. Please check wiring/power.", do not assume the sensor is dead. The Arduino Wire library returns this when the sensor NACKs its I2C address. Follow these ranked causes to isolate the fault.

The First Three Things to Check:

  1. Logic Level Overvoltage (Most Likely): If you wired the Nano's 5V SDA/SCL directly to the sensor without a BSS138 level shifter, the 5V high logic pushes current backward through the sensor's internal ESD diodes. This clamps the I2C line low, causing the 0x57 address to NACK. Fix: Disconnect power, measure resistance between SDA and GND. If it reads near 0 ohms, the sensor is permanently damaged. Replace it and use a level shifter.
  2. Clone Chip I2C Address Mismatch: Many cheap GY-MAX30102 clones use unauthorized silicon that defaults to I2C address 0x56 instead of the official Analog Devices 0x57 address. Fix: Run an I2C Scanner sketch. If it finds a device at 0x56, open the SparkFun library's MAX30105.cpp file and change #define MAX30105_ADDRESS 0x57 to 0x56.
  3. I2C Pull-up Resistor Conflict: If you have multiple I2C devices on the bus (like an OLED display), the parallel combination of their pull-up resistors might drop below 1kΩ. This prevents the BSS138 MOSFETs from pulling the line low fast enough. Fix: Use an I2C pull-up resistor calculator. For a 400kHz bus, keep total parallel resistance between 2.2kΩ and 4.7kΩ.

For deeper hardware analysis, refer to the official MAX30102 datasheet from Analog Devices, specifically Section 9 regarding the I2C interface timing requirements.

Extending and Simplifying the Build

Once you have a stable BPM reading on the Serial Plotter, you will likely want to adapt the hardware for a specific deployment. Here is how to scale the build up or down without rewriting the core algorithm.

How to Simplify: Drop the Level Shifter

The BSS138 level shifter adds wiring complexity and parasitic capacitance. If you want a cleaner breadboard layout, swap the 5V Arduino Nano v3 for a native 3.3V microcontroller. The Seeed Studio XIAO ESP32-C3 or the Arduino Nano 33 IoT operate their I2C buses at 3.3V natively. You can wire the SDA/SCL pins directly to the MAX30102, eliminating the level shifter entirely while gaining WiFi/BLE capabilities for data logging.

How to Extend: Add SpO2 and Wireless Telemetry

To calculate Blood Oxygen Saturation (SpO2), you must enable the Red LED channel alongside the IR channel (change ledMode = 2 to ledMode = 3 in the setup function). SpO2 requires calculating the ratio of the AC to DC components of both the Red and IR PPG waveforms. The SparkFun MAX3010x Hookup Guide provides the specific FIFO buffer reading sequence required for the ratio calculation.

Final Recommendation: For any new heart rate sensor Arduino build in 2026, default to the MAX30102 paired with a BSS138 level shifter if using 5V logic. The MAX30100 is obsolete and prone to I2C lockups, and analog pulse sensors lack the ambient light rejection required for reliable readings outside a dark room. Stick to the digital I2C standard, respect the voltage domains, and your PPG waveforms will be clean enough for clinical-grade algorithm testing.