If you are building an arduino pulse sensor project, skip the generic analog green-LED phototransistor modules and use the MAX30102 I2C breakout. The classic analog "Pulse Sensor Amped" modules are notoriously susceptible to 60Hz mains hum, ambient light washout, and motion artifacts. The MAX30102 uses infrared (IR) photoplethysmography (PPG) with built-in ambient light cancellation and a 15-bit ADC, giving you clinical-grade signal fidelity on a noisy workbench. Below is the exact decision framework, hardware mapping, and compilable code to get it running.

The Core Decision: Analog PPG vs. I2C MAX30102

Not all pulse sensors are created equal. Before you order parts, run your project requirements through this decision path to determine the right module.

Project Condition Analog "Pulse Sensor Amped" (Green LED) MAX30102 I2C Breakout (IR/Red LED)
Power Supply Requires 5V analog reference for full resolution. Strictly 3.3V logic and power. 5V will fry the LDO.
Ambient Light Fails under fluorescent/LED room lighting (requires shielding). Excellent. Hardware ambient light cancellation register.
Data Output Raw analog voltage (0-1023). Requires heavy software filtering. Digital I2C stream. 15-bit IR/Red values with FIFO buffer.
SpO2 Capability Impossible. Single wavelength (green). Possible. Dual wavelength (Red + IR) allows oxygen estimation.
The Concrete Pick: For 95% of embedded telemetry, data-logging, and IoT projects, buy the MAX30102 I2C Breakout. If you are on a strict budget, the generic $4 MAX30102 modules from overseas distributors work perfectly, provided you add external I2C pull-up resistors. If you need guaranteed level-shifting and onboard pull-ups, spend $15 on the SparkFun MAX30105 Particle Sensor Breakout (SEN-17341), which is fully backward-compatible with MAX30102 libraries.

Hardware Spec Sheet and Pin Mapping

The code and wiring below specifically target the Arduino Uno R3 (ATmega328P) or the Arduino Nano v3. If you are using a 3.3V board like the ESP32 or Arduino Nano 33 IoT, you can skip the logic-level warnings, but the I2C pins will change.

Component Exact Variant / Part Number Estimated Cost (2026) Notes
Microcontroller Arduino Uno R3 (ATmega328P DIP) $22 - $28 5V logic board. Requires care with 3.3V sensors.
Pulse Sensor MAX30102 Generic Breakout or SparkFun SEN-17341 $4 - $16 I2C Address: 0x57. Max I2C speed: 400kHz.
Pull-up Resistors 4.7kΩ Through-hole (x2) $0.10 Mandatory for generic modules lacking onboard pull-ups.
Wiring 22 AWG Solid Core or Dupont Jumpers $3 Keep I2C runs under 6 inches to prevent capacitance issues.

Pin Mapping Table (Arduino Uno R3 to MAX30102)

MAX30102 Pin Arduino Uno R3 Pin Wire Color (Standard) Critical Notes
VIN / VCC 3.3V Red WARNING: Do NOT connect to 5V. You will destroy the sensor.
GND GND Black Ensure a solid common ground.
SDA A4 Blue Requires 4.7kΩ pull-up to 3.3V on generic boards.
SCL A5 Yellow Requires 4.7kΩ pull-up to 3.3V on generic boards.
INT D2 Green Optional. Used for hardware interrupt data-ready signaling.

Step-by-Step Wiring and Compilable Code

Difficulty: Intermediate | Time: 25 Minutes | Library: SparkFun_MAX3010x
  1. Wire the I2C Bus: Connect SDA to A4 and SCL to A5. If using a generic MAX30102 board, solder a 4.7kΩ resistor between the SDA line and the 3.3V line, and another 4.7kΩ between SCL and 3.3V.
  2. Power the Sensor: Connect VIN to the Arduino's 3.3V output. Verify with a multimeter that the 3.3V rail is reading between 3.25V and 3.35V before connecting the sensor.
  3. Install the Library: In the Arduino IDE, go to Sketch > Include Library > Manage Libraries and install the SparkFun MAX3010x Pulse and Proximity Sensor Library.
  4. Upload the Code: Copy the complete, compilable C++ code below. This script implements a basic threshold-crossing beat detector with a refractory period to prevent double-counting the dicrotic notch.
#include <Wire.h>
#include <SparkFun_MAX3010x.h>

// --- PIN DEFINITIONS ---
// I2C pins are hardware-fixed on Uno R3, but explicitly documented here
const byte I2C_SDA = A4;
const byte I2C_SCL = A5;
const byte INT_PIN = 2; // Hardware interrupt pin (optional, unused in polling loop)

// --- SENSOR OBJECT ---
// The library uses the MAX30105 class to handle both MAX30102 and MAX30105 chips
MAX30105 particleSensor;

// --- BEAT DETECTION VARIABLES ---
long lastBeatTime = 0;
long previousBeatTime = 0;
float bpm = 0.0;
const long IR_THRESHOLD = 50000; // Baseline trigger; adjust based on Serial Plotter
const long REFRACTORY_PERIOD = 300; // ms, prevents double-triggering on single pulse

void setup() {
  Serial.begin(115200);
  while (!Serial); // Wait for serial monitor (optional)
  
  Wire.begin();
  Wire.setClock(400000); // Set I2C bus to 400kHz Fast Mode

  // ERROR HANDLING: Initialize sensor and halt if missing
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("FATAL: MAX30102 was not found. Please check wiring/power.");
    while (1) {
      delay(1000); // Halt execution in infinite loop
    }
  }

  // --- SENSOR CONFIGURATION ---
  byte ledBrightness = 50; // 0=Off, 255=50mA (Keep low to save power/reduce heat)
  byte sampleAverage = 4;  // Average 4 samples to reduce noise
  byte ledMode = 2;        // 1=Red only, 2=Red+IR, 3=Red+IR+Green
  int sampleRate = 400;    // 50, 100, 200, 400, 800, 1000, 1600, 3200
  int pulseWidth = 411;    // 69, 118, 215, 411
  int adcRange = 4096;     // 2048, 4096, 8192, 16384

  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
  Serial.println("Sensor initialized. Place finger on sensor.");
}

void loop() {
  // Read the IR photodiode value (15-bit resolution, up to ~131000)
  long irValue = particleSensor.getIR();

  // Basic threshold-crossing beat detection logic
  if (irValue > IR_THRESHOLD && (millis() - lastBeatTime) > REFRACTORY_PERIOD) {
    previousBeatTime = lastBeatTime;
    lastBeatTime = millis();

    long beatInterval = lastBeatTime - previousBeatTime;
    
    // Calculate BPM if we have a valid previous beat
    if (beatInterval > 0 && previousBeatTime > 0) {
      float instantBpm = 60000.0 / (float)beatInterval;
      
      // Sanity check: Human heart rate is typically 40 - 200 BPM
      if (instantBpm > 40.0 && instantBpm < 200.0) {
        // Simple low-pass IIR filter for display stability
        bpm = (bpm * 0.75) + (instantBpm * 0.25);
      }
    }
  }

  // Output formatted data for Serial Plotter and Monitor
  Serial.print("IR:");
  Serial.print(irValue);
  Serial.print("\tBPM:");
  Serial.println(bpm, 1);

  delay(10); // Loop rate ~100Hz, sufficient for 400 SPS sensor read
}

Debugging: Exact Error Strings and the First Three Checks

When working with I2C sensors on 5V microcontrollers, things go wrong. If your serial monitor outputs the exact string: FATAL: MAX30102 was not found. Please check wiring/power., do not just rewrite the code. The hardware is failing to acknowledge its I2C address (0x57).

The First Three Things to Check When It Fails

  1. Verify the 3.3V Logic Level: The MAX30102 is strictly a 3.3V device. If you accidentally wired VIN to the Arduino's 5V pin, you have likely burned out the onboard LDO voltage regulator. Check the sensor temperature with your finger—if it's hot to the touch, the chip is dead. Replace the module.
  2. Check for I2C Pull-Up Resistors: The Arduino Uno's internal pull-ups are tied to 5V and are too weak (approx. 20kΩ) for reliable 400kHz I2C communication. If you are using a generic bare-bones MAX30102 breakout, you must solder external 4.7kΩ resistors from SDA to 3.3V and SCL to 3.3V. Without them, the signal lines float, and the Arduino Wire library will timeout.
  3. Run an I2C Scanner: Upload the standard Arduino "I2C Scanner" sketch. If the sensor shows up at address 0x57, your wiring is fine, and the issue is a library mismatch. If it shows up at 0x5E, you have a counterfeit MAX30100 chip disguised as a MAX30102 (a common issue with cheap clones). The SparkFun library will reject it.
Signal Noise & Finger Pressure: If the code compiles but BPM reads erratically (e.g., jumping from 60 to 180), you are pressing your finger too hard against the sensor. Excessive pressure compresses the capillary beds and restricts blood flow, flattening the AC component of the PPG signal. Rest your finger gently on the module.

Extending or Simplifying the Build

Once the core I2C communication and beat detection are stable, you can adapt the hardware footprint to your specific enclosure or power constraints.

How to Simplify the Build

If you are building a standalone wearable and don't need serial debugging, strip the Serial.print() statements from the loop() to save processing cycles. Replace the serial output with a direct write to a 16x2 I2C LCD (address 0x27). Because both the LCD and the MAX30102 share the I2C bus, you won't need any additional GPIO pins—just ensure your 4.7kΩ pull-ups are robust enough to handle the capacitance of two devices.

How to Extend the Build

To turn this into a wireless telemetry node, swap the Arduino Uno R3 for an ESP32-DevKitC V4. The ESP32 operates natively at 3.3V, eliminating the logic-level and pull-up headaches entirely. Wire SDA to GPIO 21 and SCL to GPIO 22. You can then use the BLEDevice library to broadcast the BPM variable via Bluetooth Low Energy (BLE) to a mobile dashboard like nRF Connect or a custom Python script, transforming your bench project into a medical-grade IoT prototype.