Project Overview & Difficulty Rating

Optical heart rate sensing relies on photoplethysmography (PPG). The sensor fires light into the skin and measures the refraction changes caused by blood volume pulsing through capillaries. Building a heart rate monitor Arduino project seems straightforward until you hit the two most common bench hurdles: I2C voltage mismatch and motion artifact noise. This guide walks through a robust, electrically safe implementation using the MAX30102, bypassing the fried-sensor traps that plague generic tutorials.

Difficulty Rating: Intermediate
Time to Build: 45 minutes
Core Concept: I2C logic level translation and PPG signal filtering.

Hardware Spec Sheet & Parts List

The MAX30102 (originally Maxim Integrated, now Analog Devices) is a 3.3V device. The standard Arduino Nano outputs 5V on its I2C lines. Connecting them directly will degrade or destroy the sensor's internal pull-ups. We use a BSS138 logic level converter to bridge this gap safely.

Component Exact Variant / Model Operating Voltage Interface Approx. Cost (2026)
Microcontroller Arduino Nano V3.0 (ATmega328P, 5V logic) 5V (USB/VIN) I2C / UART $18.00
PPG Sensor MAX30102 Breakout Module (Generic or Adafruit) 3.3V (Strict) I2C (Up to 400kHz) $12.00
Level Shifter BSS138 Bi-directional Logic Level Converter (4-channel) 3.3V / 5V I2C Safe $2.50
Display (Optional) 0.96 inch SSD1306 OLED (I2C, 128x64) 3.3V - 5V tolerant I2C $6.00

Reference: For detailed electrical characteristics and I2C timing diagrams, consult the Analog Devices MAX30102 Datasheet.

Wiring the MAX30102 to the Arduino Nano

Proper I2C wiring requires separating the high-voltage (HV) and low-voltage (LV) sides of the BSS138 converter. The Nano provides the 5V reference and signals, while the MAX30102 operates strictly on the 3.3V side.

Arduino Nano (5V) BSS138 Level Shifter MAX30102 Sensor (3.3V)
5V Pin HV (High Voltage) -
3.3V Pin LV (Low Voltage) VIN / VCC
GND GND (Both sides) GND
A4 (SDA) HV1 -
- LV1 SDA
A5 (SCL) HV2 -
- LV2 SCL
Safety Warning: Never wire the Nano's A4/A5 pins directly to the MAX30102 SDA/SCL pins. The Nano's internal 5V pull-ups will backfeed into the 3.3V sensor, causing thermal throttling or permanent silicon damage. For a deeper understanding of why this happens, review the SparkFun Logic Levels Tutorial.
  1. Power the LV Side: Connect the Nano's 3.3V output to the LV pin on the BSS138. This powers the MAX30102.
  2. Power the HV Side: Connect the Nano's 5V pin to the HV pin on the BSS138.
  3. Common Ground: Tie the Nano GND to both GND pins on the level shifter, and the sensor GND.
  4. Route I2C: Run Nano A4 to HV1, and HV1's corresponding LV1 to the Sensor SDA. Repeat for A5/SCL.
  5. Verify with Multimeter: Before plugging in USB, check continuity. Ensure no shorts between 5V and 3.3V rails. Measure the LV1 and LV2 pins; they should read ~3.3V when pulled high.

Complete I2C Code with Error Handling

Target Board Variant: This code is compiled and tested for the Arduino Nano V3.0 (ATmega328P). If using an ESP32 or Nano 33 IoT, adjust the Wire library initialization and pin definitions accordingly.

We use the SparkFun MAX3010x library, which handles the complex FIFO buffer parsing and ambient light cancellation. The code below outputs to the Serial Monitor to guarantee compilation without display library conflicts. Use the Arduino IDE Serial Plotter (Ctrl+Shift+L) to visualize the raw IR data.

#include <Wire.h>
#include <MAX30105.h>
#include <heartRate.h>

MAX30105 particleSensor;

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

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Wire.setClock(400000); // 400kHz I2C Fast Mode

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

  // Configuration: LED Pulse Amplitude (0x2F), Sample Rate (400), Pulse Width (4100), ADC Range (250)
  particleSensor.setup(0x2F, 4, 0, 400, 4100, 250);
}

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

  if (checkForBeat(irValue) == true) {
    long delta = millis() - lastBeat;
    lastBeat = millis();
    
    beatsPerMinute = 60 / (delta / 1000.0);

    // Sanity check for human heart rates (20 to 255 BPM)
    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;
    }
  }

  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: 'MAX30105 was not found' & Noisy Data

When the sensor fails to initialize, the Serial Monitor will output the exact error string: "MAX30105 was not found. Please check wiring/power." (Note: The SparkFun library uses the MAX30105 header name for the entire MAX3010x family, including the MAX30102).

The First 3 Things to Check When It Fails:
  1. I2C Pull-up Voltage: Measure the LV side of your level shifter. If it reads 0V, your Nano's 3.3V regulator is dead or unconnected. The MAX30102 requires 3.3V to power its internal I2C pull-ups.
  2. SDA/SCL Swap: I2C lines are not interchangeable. Verify A4 goes to SDA and A5 goes to SCL. Swap them if using a non-standard Nano clone.
  3. Address Collision: Run an I2C scanner sketch. The MAX30102 default address is 0x57. If you see 0x57 but the library fails, your sensor might be a counterfeit chip with a different internal register map.

Fixing Noisy or Erratic BPM Readings

If the sensor initializes but the BPM jumps wildly (e.g., 40 to 180 to 65), you are experiencing motion artifacts or pressure-induced ischemia.

  • Finger Pressure: Pressing too hard compresses the capillaries, cutting off the pulsatile AC blood flow. The sensor only sees the static DC tissue reflection. Rest your finger lightly on the module.
  • Ambient Light Rejection: The MAX30102 modulates its LEDs to reject 50/60Hz mains flicker, but direct sunlight will saturate the photodiode. Shield the sensor with your other hand or a small piece of felt.
  • Sample Averaging: In the particleSensor.setup() function, the second parameter is sample averaging. Increase it from 4 to 8 or 16 to smooth out high-frequency noise at the cost of slightly higher power draw.

Extending and Simplifying the Build

How to Simplify: If you want to eliminate the BSS138 level shifter and reduce wiring complexity, swap the 5V Arduino Nano for a native 3.3V board like the Adafruit Feather M0 or the Arduino Nano 33 IoT. This allows direct I2C connection, provided you verify the board's 3.3V regulator can supply at least 50mA (the MAX30102 peak LED current draw).

How to Extend: To make this a wearable, wireless heart rate monitor Arduino project, replace the Nano with an ESP32 DevKit V1. The ESP32 operates natively at 3.3V (solving the logic level issue) and includes Bluetooth Low Energy (BLE). You can use the NimBLE-Arduino library to broadcast the BPM as a standard BLE Heart Rate Service (UUID 0x180D), allowing it to connect directly to smartphone fitness apps or smartwatches.

Heart Rate Monitor Arduino FAQ

Why is my heart rate monitor Arduino project giving erratic BPM readings?

Erratic readings usually stem from three physical issues: pressing too hard on the sensor (which restricts capillary blood flow), ambient light leaking into the photodiode, or motion artifacts from talking or moving. Ensure your finger is resting lightly and completely covering the LEDs. If the issue persists, increase the sample averaging in the code and lower the LED pulse amplitude to prevent photodiode saturation.

Can I use the older MAX30100 sensor instead of the MAX30102?

It is highly discouraged. The MAX30100 is an obsolete, first-generation part with known hardware flaws, specifically regarding I2C bus locking and poor ambient light rejection. Furthermore, many cheap MAX30100 breakouts on the market have a design flaw where the I2C pull-up resistors are tied to the 1.8V internal regulator instead of the 3.3V VCC pin, causing widespread communication failures. Always use the MAX30102 or MAX30101.

How do I calibrate the MAX30102 for different skin tones?

The MAX30102 does not require manual calibration for skin tone. The sensor's Automatic LED Pulse Amplitude adjustment (handled by the library's setup() parameters) dynamically scales the IR and Red LED current based on the reflected light intensity. Darker skin tones absorb more light, prompting the sensor to automatically increase the LED drive current to maintain a readable signal-to-noise ratio.

What is the difference between SpO2 and heart rate on this sensor?

Heart rate measures the time delta between peak blood volume pulses (systole) using the IR LED alone. SpO2 (blood oxygen saturation) requires calculating the ratio of light absorption between the Red LED (660nm) and the IR LED (880nm) during both the pulsatile (AC) and non-pulsatile (DC) phases. Calculating accurate SpO2 requires a complex multi-stage calibration curve that is difficult to implement reliably outside of clinical environments.