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.
| 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 |
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
- 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.
- Route I2C Data: Connect SDA to A4 and SCL to A5. The Arduino Wire library handles the protocol, but the physical layer needs help.
- 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.
- 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.
MAX30105 was not found. Please check wiring/power.Ranked Causes:
- 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.
- Address Mismatch: The library defaults to
0x57. Some rare batches use0x56. Run the standard ArduinoI2C_Scannersketch to verify the hex address. - 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.
irValue is stuck at 0 or maxed at 262143 (18-bit saturation).
Ranked Causes:
- Sensor Saturation: The IR LED current (set to
0x2Fin 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 to0x15. - 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.
- 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.
- 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 tobaseline + 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-Arduinolibrary to broadcast thebeatAvgvariable 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.






