Heart rate monitor: counting beats in a very noisy signal
- Build Logs
- 30 Jul, 2026
This project is a lesson in signal processing wearing a medical costume. The sensor is trivial to wire — three pins — and the entire difficulty is deciding which wiggles in a noisy trace are heartbeats. Get that right and you have a solid introduction to peak detection, which is a genuinely transferable skill.
What you will need
| Part | Qty | Approx. |
|---|---|---|
| Pulse Sensor / Heart Rate Detector | 1 | ₹188 |
| Arduino Uno R3 (compatible) | 1 | ₹418 |
| 16×2 LCD with I2C | 1 | ₹149 |
Roughly ₹755. A strip of velcro to hold the sensor to a fingertip is worth more to this build than any component.
How it works, and why it is fussy
This is photoplethysmography. A green LED shines into your fingertip and a photodiode measures how much light comes back. Blood absorbs green light, so with each heartbeat the pulse of blood through the capillaries absorbs slightly more, and the reflected light dips.
The word to sit with is slightly. The pulsatile component is a small fraction of the total signal. Everything else — skin tone, how hard you press, ambient light, the sensor shifting a millimetre — is larger than the thing you are trying to measure.
Which gives three practical rules:
- Press gently. Firm pressure restricts capillary flow and flattens the very signal you want. This is the opposite of what everyone instinctively does.
- Hold still. Motion artefacts are far bigger than the pulse. Any reading taken while moving your hand is fiction.
- Block ambient light. Mains lighting flickers at 100 Hz and lands directly in your signal. Wrap the sensor and finger together.
Fingertip and earlobe work well. The wrist does not — too far from the capillary bed, which is why wrist wearables use several LEDs and much cleverer processing.
Wiring
| From | To |
|---|---|
| Pulse sensor S (purple) | Uno A0 |
| Pulse sensor + (red) | Uno 5V |
| Pulse sensor − (black) | Uno GND |
| LCD SDA / SCL | Uno A4 / A5 |
That is the whole circuit. Everything else is software.
Look at the waveform first
Before writing any beat detection, plot the raw signal. Upload this, open Tools → Serial Plotter, and put your finger on the sensor.
void setup() { Serial.begin(115200); }
void loop() {
Serial.println(analogRead(A0));
delay(20); // 50 Hz sampling
}
You are looking for a repeating wave with a sharp rise and a slower fall, riding on a slowly wandering baseline. If you see a flat line, or noise with no rhythm, fix that now — adjust pressure, wrap the finger, try the other hand. No algorithm rescues a signal that is not there, and this is where people give up on this project.
Beat detection with a moving threshold
The naive approach — "count crossings above 550" — fails immediately, because the baseline moves. Warm hands, cold hands and different fingers all sit at different levels, so a fixed threshold either catches everything or nothing.
The fix is a threshold that tracks the signal: keep a running peak and trough, and trigger at a point partway between them.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
#define PULSE_PIN A0
int peak = 512, trough = 512;
bool above = false;
unsigned long lastBeat = 0;
int bpmHistory[5];
byte historyIndex = 0;
byte validBeats = 0;
// A heart rate outside this range is a detection error, not a person.
const int MIN_BPM = 40, MAX_BPM = 200;
void setup() {
Serial.begin(115200);
lcd.init(); lcd.backlight();
lcd.print("Place finger...");
}
void loop() {
int signal = analogRead(PULSE_PIN);
// Track the envelope, decaying slowly so it follows the baseline
// without being dragged around by a single sample.
if (signal > peak) peak = signal;
if (signal < trough) trough = signal;
peak -= 1;
trough += 1;
int amplitude = peak - trough;
// Too little swing means no finger, or a hopeless signal.
if (amplitude < 40) {
lcd.setCursor(0, 0); lcd.print("Place finger... ");
lcd.setCursor(0, 1); lcd.print(" ");
validBeats = 0;
delay(20);
return;
}
// Trigger high up the rising edge, and require the signal to fall
// back below a LOWER level before re-arming. That gap is hysteresis,
// and without it one beat's noisy peak registers as three beats.
int triggerHigh = trough + (amplitude * 3) / 4;
int triggerLow = trough + amplitude / 2;
if (!above && signal > triggerHigh) {
above = true;
unsigned long now = millis();
unsigned long interval = now - lastBeat;
lastBeat = now;
int bpm = 60000L / interval;
if (bpm >= MIN_BPM && bpm <= MAX_BPM) {
bpmHistory[historyIndex] = bpm;
historyIndex = (historyIndex + 1) % 5;
if (validBeats < 5) validBeats++;
if (validBeats == 5) {
long sum = 0;
for (byte i = 0; i < 5; i++) sum += bpmHistory[i];
lcd.setCursor(0, 0);
lcd.print("BPM: "); lcd.print(sum / 5); lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("Signal OK ");
} else {
lcd.setCursor(0, 1);
lcd.print("Measuring... ");
}
}
} else if (above && signal < triggerLow) {
above = false;
}
delay(20);
}
The three ideas doing the work
Hysteresis. Arming at 75% of the amplitude and re-arming only below 50% means a single beat cannot trigger repeatedly on its own noise. Using one threshold for both directions is the most common cause of a monitor reporting 180 BPM at rest.
A decaying envelope. Nudging peak down and trough up by one each sample lets the detector adapt as your baseline wanders, while still ignoring individual outliers. It is a one-line adaptive filter.
Physiological gating. Rejecting anything outside 40–200 BPM throws away detection errors cheaply. A 600 BPM reading is not a fast heart; it is two triggers on one beat.
Averaging five accepted beats is what stops the display flickering. It also means the number takes a few seconds to appear, which is honest — that is genuinely how long it takes to know.
Things that go wrong
Signal is flat. Pressing too hard. Ease off until it is barely touching.
BPM reads roughly double. Detecting both the main peak and the smaller secondary bump (the dicrotic notch, the aortic valve closing). Raise triggerHigh toward 85% of amplitude.
Wildly unstable numbers. Motion, or ambient light. Wrap the sensor and finger and keep the hand on the table.
Works on one finger, not another. Perfectly normal. Capillary density varies; index and middle fingertips are usually best.
Nothing at all through the day, fine at night. Sunlight. It swamps the photodiode completely.
Reads only when you hold your breath. You are moving more than you think. That is a good sign the detector is working.
What this is not
It is not a medical device, and the gap is not small. It has no clinical validation, no calibration traceability, no artefact rejection worth the name, and it will happily display a confident number derived from noise. Do not use it to make any decision about anyone's health, and be careful about how you describe it if you demonstrate it — "a heart rate monitor" invites a trust it has not earned. "A pulse detector" is more honest.
As a way to learn peak detection, adaptive thresholds and hysteresis, though, it is excellent, and those ideas turn up everywhere from step counters to seismographs.
Where to take it next
Plot the waveform on an OLED rather than printing a number — seeing the trace makes the algorithm's behaviour obvious, and it is far more interesting to demonstrate. Beat-to-beat interval variation is the more advanced direction, and the AD8232 ECG module measures electrical activity instead of optical, which is a genuinely different and better signal.
Ask our engineers if you cannot get a clean trace — it is nearly always pressure or ambient light, and we can usually tell from a photo of your setup.