To build a reliable heart beat sensor Arduino project, use the MAX30102 or MAX30105 I2C breakout board rather than cheap analog optical sensors. The analog modules drown in 60Hz mains hum and ambient light noise, whereas the MAX3010x series features an onboard 18-bit ADC, programmable LED current drivers, and ambient light cancellation. This guide provides the exact hardware pinout, a complete compilable C++ sketch with signal filtering, and a debugging matrix for the most common I2C and signal-drop failures.

Component Selection: MAX30102 vs Analog Pulse Sensors

Photoplethysmography (PPG) measures blood volume changes via light absorption. Oxygenated hemoglobin absorbs infrared (IR) light, while deoxygenated blood absorbs red light. A dedicated PPG sensor pulses these LEDs at specific microsecond intervals and measures the reflected photons. According to research published in the National Institutes of Health (NIH), motion artifacts and ambient light are the primary failure modes in wearable PPG systems. The MAX3010x family solves this in silicon.

Table 1: Heart Rate Sensor Module Comparison for Arduino Projects
Sensor Module Interface ADC Resolution Ambient Light Rejection Typical Price (USD) Best Use Case
MAX30102 / MAX30105 (GY-Breakout) I2C (Up to 400kHz) 18-bit Yes (Onboard subtraction) $4.00 - $8.00 Accurate BPM, SpO2, IoT integration
Analog Pulse Sensor (LM393 based) Analog (0-5V) 10-bit (Arduino ADC) No (Hardware noise) $1.50 - $3.00 Basic educational demos, low-budget
MAX30101 (Official Eval) I2C / SPI 18-bit Yes $40.00+ Clinical prototyping, multi-LED arrays
DF Robot Heart Rate Sensor Analog / Digital 10-bit Partial (Optical filter) $15.00 - $20.00 Ruggedized hobbyist builds
Maker Tip: The MAX30102 (optimized for SpO2 with Red/IR LEDs) and MAX30105 (adds a Green LED for heart rate and smoke detection) share the same I2C register map. The SparkFun Arduino library supports both interchangeably. Most budget 'MAX30102' boards on AliExpress are actually populated with MAX30105 silicon.

Hardware Pinout and Wiring Guide

This build targets the Arduino Uno R3 (ATmega328P). If you are using a 3.3V board like the Arduino Nano 33 IoT or ESP32 DevKit V1, you can bypass the logic level shifting mentioned below.

Parts List

  • Microcontroller: Arduino Uno R3 (or ATmega328P-based clone)
  • Sensor: GY-MAX30102 or GY-MAX30105 Breakout Board
  • Display: 0.96-inch SSD1306 I2C OLED (128x64, 4-pin)
  • Wiring: 22 AWG solid core hookup wire
  • Pull-up Resistors: 2x 4.7kΩ (Required for many clone boards)

Pin Mapping Table

Module Pin Arduino Uno R3 Pin Notes & Gotchas
VIN / VCC 5V Use 3.3V if your specific breakout lacks an onboard LDO.
GND GND Ensure a common ground with the OLED display.
SDA A4 (SDA) Requires 4.7kΩ pull-up to 3.3V on cheap clone boards.
SCL A5 (SCL) Requires 4.7kΩ pull-up to 3.3V on cheap clone boards.
INT Pin 2 Used for hardware interrupt on FIFO data ready (optional).

Wiring Steps

  1. Power the Sensor: Connect the breakout VCC to the Uno's 5V pin. The genuine Maxim chips operate internally at 1.8V, but breakouts include an LDO. If your board gets hot, switch to the 3.3V pin.
  2. Route I2C Data: Connect SDA to A4 and SCL to A5. The Arduino Wire library handles the protocol, but the physical layer needs help.
  3. Fix the Pull-Up Issue (Crucial): Many imported GY-MAX3010x boards route the I2C pull-up resistors to 1.8V instead of 3.3V, causing the Uno's 5V logic to fail to register a HIGH state. Solder two 4.7kΩ resistors between the SDA/SCL lines and the 3.3V pin on the Arduino to guarantee clean square waves.
  4. Wire the OLED: Connect the SSD1306 VCC to 5V, GND to GND, SDA to A4, and SCL to A5. Both devices will share the I2C bus at different addresses (Sensor: 0x57, OLED: 0x3C).

Compilable Arduino Code with Signal Filtering

This sketch uses the SparkFun MAX3010x Sensor Library. It initializes the sensor, configures the IR LED pulse width and sample rate, and uses a moving average to calculate Beats Per Minute (BPM). Install the SparkFun MAX3010x Sensor Library and Adafruit SSD1306 via the Arduino Library Manager before compiling.

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

// --- Pin & Hardware Definitions ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define SENSOR_ADDRESS 0x57 // Default for MAX30102/30105

MAX30105 particleSensor;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const byte RATE_SIZE = 4; // Averaging window for BPM
byte rates[RATE_SIZE];
byte rateSpot = 0;
long lastBeat = 0;
float beatsPerMinute = 0.0;
int beatAvg = 0;

void setup() {
  Serial.begin(115200);
  
  // Initialize I2C and OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Halt on display failure
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  
  // Initialize Sensor with Error Handling
  // Wire.setClock(400000); // Optional: Fast I2C
  if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD, SENSOR_ADDRESS)) {
    display.setCursor(0, 0);
    display.println("Sensor");
    display.println("Not Found!");
    display.display();
    Serial.println("MAX30105 was not found. Please check wiring/power.");
    while (1); // Halt execution
  }
  
  // Sensor Configuration for Heart Rate
  particleSensor.setup(0x2F); // IR LED current ~7.6mA
  particleSensor.setPulseAmplitudeRed(0x00); // Turn off Red LED
  particleSensor.setPulseAmplitudeIR(0x2F);  // Set IR LED
  particleSensor.setPulseAmplitudeGreen(0x00); // Turn off Green
  
  particleSensor.setSampleRate(100); // 100 SPS
  particleSensor.setPulseWidth(411); // 411us pulse width (18-bit resolution)
  particleSensor.setFIFOAverage(4);  // Average 4 samples
}

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

  // Check for finger presence (threshold depends on setup)
  if (irValue < 50000) {
    display.clearDisplay();
    display.setCursor(0, 20);
    display.println("Place");
    display.println("Finger");
    display.display();
    beatAvg = 0; // Reset average when finger removed
    return;
  }

  // Beat Detection Algorithm
  if (checkForBeat(irValue) == true) {
    long delta = millis() - lastBeat;
    lastBeat = millis();
    beatsPerMinute = 60.0 / (delta / 1000.0);

    if (beatsPerMinute > 20 && beatsPerMinute < 255) {
      rates[rateSpot++] = (byte)beatsPerMinute;
      rateSpot %= RATE_SIZE;
      
      beatAvg = 0;
      for (byte x = 0 ; x < RATE_SIZE ; x++) {
        beatAvg += rates[x];
      }
      beatAvg /= RATE_SIZE;
    }
  }

  // Update OLED Display
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("BPM: ");
  display.println(beatAvg);
  display.setCursor(0, 40);
  display.setTextSize(1);
  display.print("Raw IR: ");
  display.println(irValue);
  display.display();
  
  // Serial output for Arduino Plotter
  Serial.print(irValue);
  Serial.print(",");
  Serial.println(beatAvg);
}

Debugging: First Three Things to Check When It Fails

Optical biometric sensors are notoriously finicky on the bench. If your serial monitor is throwing errors or the BPM is erratic, follow this ranked troubleshooting matrix.

Error String: MAX30105 was not found. Please check wiring/power.
Ranked Causes:
  1. Missing I2C Pull-ups: As noted in the wiring section, clone boards often have 1.8V pull-ups. The Uno's I2C pins require a solid 3.3V or 5V HIGH threshold. Add external 4.7kΩ pull-ups to 3.3V.
  2. Address Mismatch: The library defaults to 0x57. Some rare batches use 0x56. Run the standard Arduino I2C_Scanner sketch to verify the hex address.
  3. Fried LDO: If you accidentally wired VCC to the RAW pin or applied >6V, the onboard 1.8V LDO is likely shorted. The chip will not respond to I2C polling.
Error Symptom: irValue is stuck at 0 or maxed at 262143 (18-bit saturation). Ranked Causes:
  1. Sensor Saturation: The IR LED current (set to 0x2F in the code) is too high for your skin tone or the sensor is pressed too hard against the skin, causing the photodiode to rail out. Lower the amplitude to 0x15.
  2. Ambient IR Flood: Direct sunlight or incandescent desk lamps contain massive amounts of IR. The sensor's ambient cancellation register overflows. Shield the sensor with your hand or move away from the window.
Error Symptom: BPM jumps wildly (e.g., 40 → 180 → 65) or drops out entirely. Ranked Causes:
  1. Motion Artifacts: PPG relies on micro-vascular expansion. Even slight finger twitching creates voltage spikes larger than the pulse wave. Rest your hand flat on the desk.
  2. Incorrect Threshold: The if (irValue < 50000) finger-detection threshold in the code is hardcoded. If your baseline IR reflection is lower, the code thinks you keep removing your finger. Open the Serial Plotter, note your baseline with no finger, and adjust the threshold to baseline + 10000.

Extending and Simplifying the Build

Once you have the baseline I2C communication and beat detection working, you can scale the project up or down depending on your enclosure and power constraints.

How to Simplify the Build

If you are building a temporary bench test or want to reduce the BOM cost, drop the SSD1306 OLED entirely. Delete the Adafruit GFX and SSD1306 library includes, remove the display I2C initialization, and rely purely on the Arduino IDE's Serial Plotter (Tools > Serial Plotter). Set the baud rate to 115200. The Serial.print(irValue); Serial.print(","); Serial.println(beatAvg); lines already in the code will render a real-time ECG-style waveform and a flatline BPM tracker, giving you visual feedback without extra hardware.

How to Extend the Build

To turn this into a wearable or remote monitor, swap the Arduino Uno R3 for an ESP32 DevKit V1. The ESP32 operates natively at 3.3V, eliminating the I2C pull-up voltage mismatch issues entirely.

  • Add BLE: Use the NimBLE-Arduino library to broadcast the beatAvg variable over Bluetooth Low Energy using the standard Heart Rate Service UUID (0x180D). This allows the sensor to push data directly to generic fitness apps on iOS or Android.
  • Add WiFi/MQTT: Use the ESP32's WiFi radio to publish the BPM data to an MQTT broker (like Mosquitto) every 5 seconds. This integrates the heart beat sensor Arduino project directly into Home Assistant for automated alerts if a user's heart rate exceeds a safe threshold during exercise.

When migrating to the ESP32, ensure you change the I2C pin definitions in the Wire.begin(SDA_PIN, SCL_PIN) call, as the ESP32's default I2C pins differ from the ATmega328P's A4/A5 hardware mapping.