Heart rate monitor 3.0
Note
🌟 Welcome to the SunFounder Facebook Community! Whether you’re into Raspberry Pi, Arduino, or ESP32, you’ll find inspiration, help ideas here.
✅ Be the first to get free learning resources.
✅ Stay updated on new products & exclusive giveaways.
✅ Share your creations and get real feedback.
Kit purchase
Looking for parts? Check out our all-in-one kits below — packed with components, beginner-friendly guides, and tons of fun.
Name |
Includes Arduino board |
PURCHASE LINK |
|---|---|---|
Elite Explorer Kit |
Arduino Uno R4 WiFi |
|
3 in 1 Ultimate Starter Kit |
Arduino Uno R4 Minima |
Course Introduction
This Arduino project builds a basic Heart Rate Monitor using the MAX30102 sensor and OLED.
It detects heartbeats by analyzing infrared signals and calculates the heart rate in BPM.
The measured BPM is displayed on the OLED. A buzzer beeps with each beat, and an RGB LED indicates heart rate level—red for high, green for normal.
Note
If this is your first time working with an Arduino project, we recommend downloading and reviewing the basic materials first.
Required Components
In this project, we need the following components:
SN |
COMPONENT INTRODUCTION |
QUANTITY |
PURCHASE LINK |
|---|---|---|---|
1 |
Arduino UNO R4 Minima |
1 |
|
2 |
USB Type-C cable |
1 |
|
3 |
Breadboard |
1 |
|
4 |
Wires |
Several |
|
5 |
OLED Display Module |
1 |
|
6 |
Pulse Oximeter and Heart Rate Sensor Module (MAX30102) |
1 |
|
7 |
RGB LED |
1 |
|
8 |
Passive buzzer |
1 |
Wiring
Common Connections:
Pulse Oximeter and Heart Rate Sensor Module (MAX30102)
SDA: Connect to SDA on the Arduino.
SCL: Connect to SCL on the Arduino.
GND: Connect to breadboard’s negative power bus.
VIN: Connect to breadboard’s red power bus.
LED
Connect the LED cathode to the negative power bus on the breadboard, and the LED anode to 1kΩ resistor then to 4 on the Arduino.
Passive Buzzer
+: Connect to 5 on the Arduino.
-: Connect to breadboard’s negative power bus.
OLED Display Module
SDA: Connect to A4 on the Arduino.
SCK: Connect to A5 on the Arduino.
GND: Connect to breadboard’s negative power bus.
VCC: Connect to breadboard’s red power bus.
Writing the Code
Note
You can copy this code into Arduino IDE.
To install the library, use the Arduino Library Manager and search for Adafruit SSD1306 and Adafruit GFX , MAX30105 and heartRate and install it.
Don’t forget to select the board(Arduino UNO R4) and the correct port before clicking the Upload button.
#ifdef LED_RED
#undef LED_RED // Prevent conflict with MAX30105 enum
#endif
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
// =========================
// OLED setup
// =========================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// =========================
// Pin setup
// =========================
const int BUZZER_PIN = 5; // Passive buzzer signal pin
const int RED_LED_PIN = 4; // Red LED pin
// =========================
// Heart rate variables
// =========================
MAX30105 particleSensor;
const byte RATE_SIZE = 4;
byte rates[RATE_SIZE];
byte rateSpot = 0;
long lastBeat = 0;
float beatsPerMinute;
int beatAvg = 75; // Default startup value
// =========================
// Beat flash / beep control
// =========================
bool beatEffectActive = false;
unsigned long beatEffectStart = 0;
const unsigned long beatEffectDuration = 80; // LED/Buzzer pulse duration (ms)
// =========================
// ECG drawing area
// Top 1/4 for BPM, bottom 3/4 for graph
// =========================
const int BPM_AREA_H = 16;
const int GRAPH_TOP = BPM_AREA_H;
const int GRAPH_BOTTOM = SCREEN_HEIGHT - 1;
const int GRAPH_HEIGHT = SCREEN_HEIGHT - BPM_AREA_H;
// 128 points buffer, one for each OLED column
int ecgBuffer[SCREEN_WIDTH];
// ECG animation timing
unsigned long lastGraphUpdate = 0;
const int GRAPH_UPDATE_INTERVAL = 20; // ms, scrolling speed
// Beat-synced ECG cycle
unsigned long lastWaveBeatTime = 0;
// =========================
// Function declarations
// =========================
void drawBPMArea();
void drawECGGraph();
void pushECGPoint();
int generateECGPoint(unsigned long now);
void triggerBeatEffect();
void updateBeatEffect();
void resetECGBuffer();
void showNoFingerMessage();
void setup() {
Serial.begin(9600);
// Uno / Nano default I2C:
// SDA -> A4
// SCL -> A5
Wire.begin();
Wire.setClock(400000);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RED_LED_PIN, OUTPUT);
digitalWrite(RED_LED_PIN, LOW);
noTone(BUZZER_PIN);
// Start OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("OLED not found");
while (true);
}
display.clearDisplay();
display.display();
// Start MAX30102 sensor
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("MAX30102 not found");
while (true);
}
particleSensor.setup();
particleSensor.setPulseAmplitudeRed(0x0A);
particleSensor.setPulseAmplitudeGreen(0);
resetECGBuffer();
Serial.println("Place your finger on the sensor.");
}
void loop() {
long irValue = particleSensor.getIR();
// If finger detected
if (irValue > 50000) {
// Detect heartbeat
if (checkForBeat(irValue)) {
long delta = millis() - lastBeat;
lastBeat = millis();
lastWaveBeatTime = lastBeat;
beatsPerMinute = 60.0 / (delta / 1000.0);
if (beatsPerMinute < 255 && beatsPerMinute > 20) {
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE;
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++) {
beatAvg += rates[x];
}
beatAvg /= RATE_SIZE;
}
triggerBeatEffect();
Serial.print("IR=");
Serial.print(irValue);
Serial.print(", BPM=");
Serial.print(beatsPerMinute);
Serial.print(", Avg BPM=");
Serial.println(beatAvg);
}
updateBeatEffect();
// Update ECG scrolling
if (millis() - lastGraphUpdate >= GRAPH_UPDATE_INTERVAL) {
lastGraphUpdate = millis();
pushECGPoint();
}
// Draw OLED
display.clearDisplay();
drawBPMArea();
drawECGGraph();
display.display();
} else {
// No finger
noTone(BUZZER_PIN);
digitalWrite(RED_LED_PIN, LOW);
beatEffectActive = false;
resetECGBuffer();
showNoFingerMessage();
Serial.println("Place your finger on the sensor");
delay(100);
}
}
// =========================
// Trigger LED + buzzer on beat
// =========================
void triggerBeatEffect() {
beatEffectActive = true;
beatEffectStart = millis();
digitalWrite(RED_LED_PIN, HIGH);
tone(BUZZER_PIN, 2000); // 2kHz short beep
}
void updateBeatEffect() {
if (beatEffectActive && millis() - beatEffectStart >= beatEffectDuration) {
beatEffectActive = false;
digitalWrite(RED_LED_PIN, LOW);
noTone(BUZZER_PIN);
}
}
// =========================
// OLED top area: BPM
// =========================
void drawBPMArea() {
display.drawLine(0, BPM_AREA_H - 1, SCREEN_WIDTH - 1, BPM_AREA_H - 1, WHITE);
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(4, 4);
display.print("BPM:");
display.setTextSize(2);
display.setCursor(38, 0);
display.print(beatAvg);
}
// =========================
// OLED bottom area: ECG graph
// =========================
void drawECGGraph() {
for (int x = 1; x < SCREEN_WIDTH; x++) {
display.drawLine(x - 1, ecgBuffer[x - 1], x, ecgBuffer[x], WHITE);
}
}
void pushECGPoint() {
for (int i = 0; i < SCREEN_WIDTH - 1; i++) {
ecgBuffer[i] = ecgBuffer[i + 1];
}
ecgBuffer[SCREEN_WIDTH - 1] = generateECGPoint(millis());
}
// Generate a simple ECG-like waveform synced to BPM
int generateECGPoint(unsigned long now) {
int baseline = GRAPH_TOP + GRAPH_HEIGHT / 2;
// Use beatAvg to determine cycle length
int bpm = constrain(beatAvg, 40, 180);
unsigned long beatInterval = 60000UL / bpm; // ms per beat
unsigned long phase = (now - lastWaveBeatTime) % beatInterval;
// Normalize phase into ECG segments
// We simulate: baseline -> small rise -> sharp spike -> drop -> recovery
int y = baseline;
unsigned long p1 = beatInterval * 10 / 100; // pre-rise
unsigned long p2 = beatInterval * 14 / 100; // sharp rise
unsigned long p3 = beatInterval * 18 / 100; // sharp drop
unsigned long p4 = beatInterval * 24 / 100; // recovery
unsigned long p5 = beatInterval * 35 / 100; // settle
if (phase < p1) {
y = baseline;
} else if (phase < p2) {
y = map(phase, p1, p2, baseline, baseline - 4);
} else if (phase < p3) {
y = map(phase, p2, p3, baseline - 4, GRAPH_TOP + 3);
} else if (phase < p4) {
y = map(phase, p3, p4, GRAPH_TOP + 3, GRAPH_BOTTOM - 3);
} else if (phase < p5) {
y = map(phase, p4, p5, GRAPH_BOTTOM - 3, baseline);
} else {
y = baseline;
}
return constrain(y, GRAPH_TOP + 1, GRAPH_BOTTOM - 1);
}
// =========================
// Utilities
// =========================
void resetECGBuffer() {
int baseline = GRAPH_TOP + GRAPH_HEIGHT / 2;
for (int i = 0; i < SCREEN_WIDTH; i++) {
ecgBuffer[i] = baseline;
}
}
void showNoFingerMessage() {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(18, 12);
display.println("Please place");
display.setCursor(18, 24);
display.println("your finger");
display.setCursor(18, 36);
display.println("and wait...");
display.display();
}