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. |
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
- 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.
- 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.
- Install the Library: In the Arduino IDE, go to Sketch > Include Library > Manage Libraries and install the
SparkFun MAX3010x Pulse and Proximity Sensor Library. - 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
- 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.
- 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.
- 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 at0x5E, you have a counterfeit MAX30100 chip disguised as a MAX30102 (a common issue with cheap clones). The SparkFun library will reject it.
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.






