Difficulty: Beginner-Intermediate | Time: 45 minutes | Target Board: Arduino Uno R3 (AVR)

Building an Arduino heart monitor relies on photoplethysmography (PPG)—shining a green LED into the skin and measuring the light reflected back as blood volume changes with each heartbeat. While the concept is simple, the analog signal is notoriously noisy. A floating ground, ambient 60Hz room lighting, or a missing pull-down resistor will turn your serial plotter into a flatline or a chaotic mess.

This guide provides the exact hardware specifications, a robust pin mapping, and complete, compilable C++ code with built-in error handling to catch disconnected sensors before they ruin your data logging. We are targeting the Arduino Uno R3 due to its stable 5V logic and reliable analog-to-digital converter (ADC) reference, which is critical for reading the millivolt-level swings from the PPG sensor.

Parts List & Hardware Specifications

The market is flooded with cheap PPG clones. The table below specifies the exact variants you need to ensure the op-amp circuit actually functions. The official PulseSensor Amped includes an MCP6001 op-amp, while the common purple clones use an LM358. Both work, but the clone requires slightly different thresholding in code.

Component Exact Model / Variant Est. Price (2026) Notes & Bench Tips
Microcontroller Arduino Uno R3 (Rev3, ATmega328P) $24.00 - $28.00 Avoid the Uno R4 Minima for this specific build; the R4's ADC behaves differently with the legacy PulseSensor library timer interrupts.
PPG Sensor PulseSensor Amped (Official) OR Purple LM358 Clone $25.00 / $6.00 The official board has better noise filtering. If using the $6 clone, ensure the back of the board has the LM358 chip and two potentiometers.
Wiring 22 AWG Solid Core Jumper Wires (M-F) $5.00 Use shielded cable if routing the signal wire more than 6 inches to prevent EMI pickup.
Feedback 5mm Red LED + 330Ω Resistor $0.50 For visual heartbeat confirmation without relying on the serial monitor.

Pin Mapping and Wiring Procedure

The PulseSensor outputs an analog voltage between 0V and 5V. The signal swings by roughly 50mV to 200mV around a DC bias point (usually ~2.5V). Because the swing is so small, a solid ground reference is non-negotiable.

Bench Tip: Never share the Arduino's ground rail with a high-current load (like a motor or relay) while reading the PPG sensor. The ground bounce will completely drown out the heartbeat signal.
PulseSensor Pin Arduino Uno R3 Pin Wire Color (Standard) Function
VCC (or +) 5V Red Power for the LED and LM358 op-amp. Do NOT use 3.3V.
GND (or -) GND Black Common ground reference. Keep this wire short.
SIG (or S) A0 Orange/Yellow Analog signal output. Must connect to an ADC pin.
  1. Prepare the Sensor: If using the purple clone, locate the small potentiometer on the back. Use a precision screwdriver to set it to the middle position before connecting power.
  2. Connect Power: Wire VCC to 5V and GND to GND. The green LED on the sensor should illuminate brightly.
  3. Connect Signal: Wire SIG to A0. Do not connect it to a digital pin; the library requires hardware ADC sampling.
  4. Attach to Body: Clip the sensor to your earlobe or tape it firmly to your index finger. The photodiode (dark square) must face the skin, and the green LED must shine directly into the capillary bed.

Complete Compilable Code with Error Handling

The following code targets the Arduino Uno R3 and utilizes the official PulseSensorPlayground library, which handles the interrupt-driven ADC sampling required for accurate BPM calculation. We have added a custom initialization check to catch disconnected or shorted sensors immediately.

Prerequisite: Install the "PulseSensor Playground" library via the Arduino Library Manager before compiling.

#include <PulseSensorPlayground.h>

// --- PIN DEFINITIONS ---
#define PULSE_INPUT A0
#define PULSE_BLINK 13    // Built-in LED on Uno R3
#define PULSE_FADE 5      // Optional PWM pin for fading LED

// --- LIBRARY CONFIGURATION ---
const int OUTPUT_TYPE = SERIAL_PLOTTER;
const int THRESHOLD = 530; // Adjust based on your sensor's DC bias

PulseSensorPlayground pulseSensor;

void setup() {
  Serial.begin(115200);
  
  // Configure the PulseSensor manager
  pulseSensor.analogInput(PULSE_INPUT);
  pulseSensor.blinkOnPulse(PULSE_BLINK);
  pulseSensor.fadeOnPulse(PULSE_FADE);
  pulseSensor.setThreshold(THRESHOLD);

  // Custom Hardware Error Check
  // Read the raw pin to ensure it's not floating at the rails (0 or 1023)
  int rawCheck = analogRead(PULSE_INPUT);
  if (rawCheck < 15 || rawCheck > 1010) {
    Serial.println("ERROR: PulseSensor signal stuck at rail. Check VCC/GND wiring.");
    Serial.println("Raw ADC Value: " + String(rawCheck));
    while(true) { 
      // Halt execution to prevent garbage data logging
      digitalWrite(PULSE_BLINK, HIGH); 
      delay(100); 
      digitalWrite(PULSE_BLINK, LOW); 
      delay(100);
    }
  }

  // Initialize the library (sets up timer interrupts)
  if (!pulseSensor.begin()) {
    Serial.println("ERROR: PulseSensor library failed to initialize. Check timer conflicts.");
    while(true);
  }
  
  Serial.println("PulseSensor initialized successfully. Awaiting heartbeat...");
}

void loop() {
  // Check if a new heartbeat has been detected
  if (pulseSensor.sawNewSample()) {
    int myBPM = pulseSensor.getBeatsPerMinute();
    
    // Filter out physiological impossibilities (noise artifacts)
    if (myBPM > 30 && myBPM < 220) {
      Serial.print("BPM: ");
      Serial.println(myBPM);
    }
  }
  
  // Small delay to prevent flooding the serial buffer
  delay(20);
}

Debugging: First Three Things to Check When It Fails

When your serial monitor outputs erratic data or flatlines, do not immediately rewrite the code. PPG sensors fail at the physical layer 95% of the time. If you see the exact error string "ERROR: PulseSensor signal stuck at rail. Check VCC/GND wiring." or a BPM reading of exactly 60 or 120, follow this ranked troubleshooting path.

  1. VCC/GND Swap or Floating Ground (Most Likely): The analogRead() returns 0 or 1023. This means the signal pin is pulled to ground or 5V. Use your multimeter to measure DC voltage between the sensor's VCC and GND pads. You must read exactly 4.8V to 5.1V. If you read 0V, your jumper wire is broken or seated in the wrong breadboard row.
  2. Ambient 50/60Hz Mains Hum: If your BPM reads exactly 60, 120, 50, or 100, your sensor is blinded by room lighting. Fluorescent and LED bulbs flicker at the AC mains frequency. The photodiode picks up this flicker and the library interprets it as a heartbeat. Fix: Cup your hand over the sensor to block ambient light, or apply a piece of dark electrical tape over the top of the sensor module, leaving only the skin-facing side exposed.
  3. Motion Artifacts and Poor Skin Contact: If the BPM jumps wildly between 40 and 180, the sensor is shifting against the skin. PPG requires the LED and photodiode to remain perfectly still relative to the capillaries. Fix: Use medical tape to secure the sensor flat against the pad of your finger. Do not use the spring-clip on your finger; the spring tension causes micro-movements. The earlobe is mechanically much more stable.

Extending and Simplifying the Build

Depending on your end goal, you may need to strip this project down to its bare essentials or scale it up for a display enclosure.

To Simplify (Standalone Feedback):
If you just want a visual metronome of your pulse without a PC attached, delete the Serial commands in the loop(). The pulseSensor.blinkOnPulse(PULSE_BLINK); function already handles flashing the Pin 13 LED in hardware-timed sync with your heart. You can power the Uno R3 via a 9V battery or a 5V USB power bank, making it a standalone bio-feedback device.

To Extend (I2C OLED Integration):
To plot the waveform and display BPM without a serial connection, add a 0.96-inch SSD1306 I2C OLED display. Wire SDA to A4 and SCL to A5 on the Uno R3. Use the Adafruit_SSD1306 library. In the loop(), map the raw analog signal (0-1023) to the OLED's Y-axis (0-64 pixels) and draw a rolling line graph. This requires shifting the display buffer array left by one pixel every time pulseSensor.sawNewSample() returns true.

Frequently Asked Questions

Can I use an Arduino Nano instead of the Uno for this heart monitor?

Yes, the Arduino Nano (ATmega328P variant) uses the exact same silicon and pinout for A0, 5V, and GND. The code provided above will compile and run without modification. However, avoid the Arduino Nano 33 IoT or Nano RP2040 Connect for this specific library, as their 3.3V logic and different ADC architectures require voltage dividers and library modifications to prevent damaging the sensor or getting inaccurate readings.

Why does my Arduino heart monitor BPM double when I move my finger?

This is caused by the "dicrotic notch" in the arterial pressure waveform. A single heartbeat actually produces a primary peak (systole) and a smaller secondary peak (diastole reflection). If your sensor gain is too high, or the THRESHOLD variable in the code is set too low, the library will detect both peaks as separate heartbeats, exactly doubling your BPM. Increase the THRESHOLD value in the code (e.g., from 530 to 550) or turn down the gain potentiometer on the back of the purple clone sensor.

Is the Arduino heart monitor accurate enough for medical diagnostics?

No. While the PulseSensor hardware is excellent for educational and hobbyist bio-feedback, it lacks the medical-grade isolation, multi-wavelength calibration, and FDA-cleared algorithms required for clinical diagnostics. It cannot detect arrhythmias like atrial fibrillation reliably, nor can it measure blood oxygen saturation (SpO2) because it only uses a single green LED, whereas medical pulse oximeters use both red and infrared light.

How do I power the Arduino heart monitor with a battery for portable use?

For portable use, power the Arduino Uno R3 via the Vin pin using a 7.4V (2S) LiPo battery pack, or plug a standard 5V USB power bank into the micro-USB port. The USB power bank route is highly recommended because switching regulators on the Uno's barrel jack can introduce high-frequency switching noise into the 5V rail, which will couple directly into your analog PPG signal and degrade the ADC resolution.