Display Heartbeat BPM on an Arduino LCD
Build an Arduino heartbeat monitor with a pulse-style heart beat sensor, an LCD1602 display, and a buzzer. The Arduino watches the analog pulse signal, detects clean beat peaks, estimates BPM from the time between beats, and shows the live result on the LCD.

Live project track
Interactive hardware & logic preview
Have you ever felt your pulse on your wrist and counted the beats for a few seconds? You are doing two things at once: noticing each beat and measuring the time between them. A heartbeat monitor does the same basic job, but it lets the Arduino do the counting and timing.
In this project, a heart beat sensor gives the Arduino a changing analog signal. The Arduino watches for clean peaks in that signal, estimates beats per minute, shows the result on an LCD1602 display, and makes a short buzzer beep when a beat is detected. The one main concept is: BPM comes from the time between repeated beat peaks, not from one single sensor reading.
This is a learning project, not a medical device. Use it to understand sensors, signals, and timing, not to make health decisions.
Blood Flow Changes Become a Pulse Waveform
Think of the sensor like a tiny flashlight and observer looking at your fingertip. With each heartbeat, blood flow in the finger changes slightly. That small change affects how much light is reflected or absorbed. The sensor turns that changing light level into an electrical signal.
The Arduino does not see a neat number called heartbeat. It sees an analog value that rises and falls. Analog means the value can move through a range, like 512, 548, 601, and back down again, instead of only being on or off. The peak of that rise is the clue we use to detect one beat.

Blood flow change becoming a pulse waveform
| What happens physically | What the Arduino receives | What the code does |
|---|---|---|
| Blood flow changes with a beat | The sensor value rises | Watch for the value crossing a threshold |
| The beat passes | The sensor value falls again | Wait until the signal is ready for another beat |
| Beats repeat over time | Peaks appear again and again | Measure the time between peaks |
This is why heartbeat projects can feel a little more delicate than a button project. A button usually gives a clear HIGH or LOW. A pulse sensor gives a shape. The code has to decide which part of that shape counts as a real beat and which part is just noise.
Two Thresholds Stop One Beat From Being Counted Twice
If the code counted a beat every time the signal was above one threshold, one real heartbeat could be counted many times. The Arduino loop runs very fast, so the signal might stay above the threshold for several loop cycles. That would make the BPM jump unrealistically high.
To prevent that, this project uses two thresholds. The higher
beatThreshold detects the beat peak. The lower releaseThreshold tells the Arduino when the waveform has fallen enough to be ready for the next beat. This gap is called hysteresis. In plain words, the signal must rise high enough to count, then fall low enough before it can count again.
Beat threshold and release threshold on a pulse waveform
- Beat threshold is the upper line that marks a likely heartbeat peak.
- Release threshold is the lower line that re-arms the detector after the signal falls.
- Ready for beat is the code's memory that prevents one long peak from being counted repeatedly.
If the buzzer chatters rapidly, the threshold is probably too low or the signal is noisy. Raise beatThreshold or steady the sensor contact before changing the rest of the code.The Gap Between Peaks Becomes BPM
BPM means beats per minute. The Arduino does not need to wait one full minute to estimate it. Instead, it measures the time between two neighboring beats and scales that interval up to one minute.
If two beats are 1000 milliseconds apart, that is one beat per second. One beat per second becomes 60 beats per minute. If the interval is 500 milliseconds, that is two beats per second, or 120 BPM. The formula in the code is
60000 / interval because there are 60,000 milliseconds in one minute.
Time between heartbeat peaks converted into BPM
| Beat interval | Estimated BPM | Meaning in the project |
|---|---|---|
| 1000 ms | 60 BPM | One beat every second |
| 750 ms | 80 BPM | Faster than one beat per second |
| 500 ms | 120 BPM | Two beats every second |
The sketch also rejects intervals that are too short or too long. A beat interval under 300 milliseconds would imply more than 200 BPM, which is usually a false trigger in this learning setup. A gap longer than 2000 milliseconds is treated as too old to produce a useful live BPM estimate.
Steady Contact Makes the BPM Reading Less Jumpy
Pulse sensors are sensitive to movement. If the fingertip slides, presses too hard, or barely touches the sensor, the waveform can become jagged. The Arduino may then see fake peaks and count them as beats. This is why heartbeat projects often need more patience than light or temperature projects.

Noisy moving contact compared with steady pulse sensor contact
For the cleanest result, hold the sensor still and give the reading a few seconds to settle. In the simulator, you can use the pulse control to test different signal levels. On a real sensor, the first debugging step is usually not code. It is better contact, less movement, and less strong light hitting the sensor.
Each Heartbeat Monitor Wire Has One Simple Role
The circuit has three groups: the pulse sensor reads the body signal, the LCD shows the BPM and raw signal, and the buzzer gives a short beep for each detected beat. The Arduino sits in the middle and makes the timing decision.
5V is the positive power supply from the Arduino. GND is the return path for electricity, like the negative terminal of a battery. The sensor, LCD, and buzzer all need a shared GND so their signals use the same zero-voltage reference. Pin Connection Map
Heart Beat Sensor
VCC
→
Arduino Uno
5V
Explanation
Powers the light-sensing circuit inside the pulse module.
Heart Beat Sensor
GND
→
Arduino Uno
GND
Explanation
Shares the same return path and voltage reference as the Arduino.
Heart Beat Sensor
OUT
→
Arduino Uno
A0
Explanation
Sends the changing analog pulse signal into the Arduino.
Pin Connection Map
Buzzer
Positive
→
Arduino Uno
D8
Explanation
Receives the short tone signal when a beat is detected.
Buzzer
Negative
→
Arduino Uno
GND
Explanation
Completes the buzzer circuit back to ground.
Pin Connection Map
LCD1602
VSS
→
Arduino Uno
GND
Explanation
Ground for the LCD logic.
LCD1602
VDD
→
Arduino Uno
5V
Explanation
Power for the LCD logic.
LCD1602
RW
→
Arduino Uno
GND
Explanation
Keeps the LCD in write mode so Arduino sends text to it.
LCD1602
RS
→
Arduino Uno
D12
Explanation
Chooses whether the LCD receives a command or display text.
LCD1602
E
→
Arduino Uno
D11
Explanation
Tells the LCD to accept the current data.
LCD1602
D4
→
Arduino Uno
D5
Explanation
First data line used by the display in 4-bit mode.
LCD1602
D5
→
Arduino Uno
D4
Explanation
Second data line used by the display in 4-bit mode.
LCD1602
D6
→
Arduino Uno
D3
Explanation
Third data line used by the display in 4-bit mode.
LCD1602
D7
→
Arduino Uno
D2
Explanation
Fourth data line used by the display in 4-bit mode.
LCD1602
A
→
Arduino Uno
5V
Explanation
Power for the LCD backlight.
LCD1602
K
→
Arduino Uno
GND
Explanation
Ground for the LCD backlight.
If the LCD works but BPM stays as --, check the raw signal number on row 2. If it never rises above the beat threshold, the sensor signal is too low or the threshold is too high.The Complete Heartbeat BPM Monitor Sketch
This sketch uses timing logic, so it is more advanced than a basic LED project. The key idea is still understandable: sample the pulse sensor, count only clean peaks, measure the time between peaks, and update the LCD.
The code uses
millis() instead of only delay() because the buzzer beep should end while the sensor keeps being checked. millis() returns how many milliseconds have passed since the Arduino started running, which makes it useful for timing events without stopping the whole sketch.C++ Source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <LiquidCrystal.h>
const int sensorPin = A0;
const int buzzerPin = 8;
const int rs = 12;
const int en = 11;
const int d4 = 5;
const int d5 = 4;
const int d6 = 3;
const int d7 = 2;
const int beatThreshold = 560;
const int releaseThreshold = 520;
const int beepFrequency = 1800;
const unsigned long minBeatIntervalMs = 300;
const unsigned long maxBeatIntervalMs = 2000;
const unsigned long beepDurationMs = 40;
const unsigned long displayRefreshMs = 250;
const unsigned long sampleDelayMs = 20;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
bool readyForBeat = true;
unsigned long lastBeatTime = 0;
unsigned long lastBeepStart = 0;
unsigned long lastDisplayUpdate = 0;
int bpm = 0;
void setup() {
pinMode(buzzerPin, OUTPUT);
lcd.begin(16, 2);
lcd.print("Heartbeat BPM");
lcd.setCursor(0, 1);
lcd.print("Hold still...");
delay(1200);
}
void loop() {
int signal = analogRead(sensorPin);
unsigned long now = millis();
if (readyForBeat && signal > beatThreshold) {
handleBeat(now);
readyForBeat = false;
}
if (!readyForBeat && signal < releaseThreshold) {
readyForBeat = true;
}
if (lastBeepStart > 0 && now - lastBeepStart >= beepDurationMs) {
noTone(buzzerPin);
lastBeepStart = 0;
}
if (now - lastDisplayUpdate >= displayRefreshMs) {
showReading(signal);
lastDisplayUpdate = now;
}
delay(sampleDelayMs);
}
void handleBeat(unsigned long now) {
if (lastBeatTime > 0) {
unsigned long interval = now - lastBeatTime;
if (interval >= minBeatIntervalMs && interval <= maxBeatIntervalMs) {
bpm = 60000UL / interval;
}
}
lastBeatTime = now;
tone(buzzerPin, beepFrequency);
lastBeepStart = now;
}
void showReading(int signal) {
lcd.setCursor(0, 0);
lcd.print("BPM: ");
if (bpm > 0) {
lcd.print(bpm);
lcd.print(" ");
} else {
lcd.print("-- ");
}
lcd.setCursor(0, 1);
lcd.print("Signal: ");
lcd.print(signal);
lcd.print(" ");
}How the Heartbeat BPM Code Works, Part By Part
Now let us break the sketch into smaller jobs. Each block supports one part of the monitor: sensor input, peak detection, beat timing, buzzer feedback, and LCD display.
▸ Heartbeat Monitor Pins and Thresholds Define the Signal Rules
The first constants name the sensor pin, buzzer pin, LCD pins, and detection values. A constant is a named value that does not change while the program runs. Using names keeps the project readable because
beatThreshold explains itself better than a mystery number like 560.C++ Source
1
2
3
4
5
6
7
8
const int sensorPin = A0;
const int buzzerPin = 8;
const int beatThreshold = 560;
const int releaseThreshold = 520;
const int beepFrequency = 1800;
const unsigned long minBeatIntervalMs = 300;
const unsigned long maxBeatIntervalMs = 2000;The threshold pair is the most important setting to tune. If real beats are missed, lower
beatThreshold a little. If fake beats are counted, raise it or increase the gap between the beat and release thresholds.▸ Monitor Memory Prevents Repeat Counting
The variables remember the state of the monitor between loop cycles.
readyForBeat tells the sketch whether the next high signal is allowed to count as a new beat. The time variables remember when the last beat, beep, and display update happened.C++ Source
1
2
3
4
5
bool readyForBeat = true;
unsigned long lastBeatTime = 0;
unsigned long lastBeepStart = 0;
unsigned long lastDisplayUpdate = 0;
int bpm = 0;▸ Setup Starts the LCD Before Reading
The setup section prepares the buzzer as an
OUTPUT, which means the Arduino sends a signal from that pin. Then it starts the LCD and prints a short message so the reader sees that the display is responding before the BPM value appears.C++ Source
1
2
3
4
5
6
7
8
9
void setup() {
pinMode(buzzerPin, OUTPUT);
lcd.begin(16, 2);
lcd.print("Heartbeat BPM");
lcd.setCursor(0, 1);
lcd.print("Hold still...");
delay(1200);
}▸ Loop Samples the Pulse Waveform
Each loop reads the analog sensor signal from
A0. analogRead() returns a number from 0 to 1023, where higher numbers mean a higher input voltage. The sketch also stores the current time from millis() so every timing decision uses the same time reference.C++ Source
1
2
int signal = analogRead(sensorPin);
unsigned long now = millis();▸ Threshold Crossing Counts One Beat
The beat detector only runs when two things are true: the monitor is ready for a beat, and the signal is above the beat threshold. After counting that peak, the code sets
readyForBeat to false so the same high pulse cannot be counted again.C++ Source
1
2
3
4
5
6
7
8
if (readyForBeat && signal > beatThreshold) {
handleBeat(now);
readyForBeat = false;
}
if (!readyForBeat && signal < releaseThreshold) {
readyForBeat = true;
}The second
if statement re-arms the detector only after the signal falls below the release threshold. That fall is what tells the Arduino that the previous beat has passed.▸ Interval Math Updates the BPM
handleBeat() measures the gap between this beat and the previous beat. If the interval looks realistic, the code divides 60,000 by that interval to estimate BPM. The UL in 60000UL tells Arduino to treat the number as an unsigned long value, which is safer for time math.C++ Source
1
2
3
4
5
6
7
8
9
if (lastBeatTime > 0) {
unsigned long interval = now - lastBeatTime;
if (interval >= minBeatIntervalMs && interval <= maxBeatIntervalMs) {
bpm = 60000UL / interval;
}
}
lastBeatTime = now;▸ Short Beep Confirms the Detected Peak
The buzzer starts when a beat is detected, but it should not stay on forever. The sketch stores
lastBeepStart, then later turns the tone off when beepDurationMs has passed. This gives each beat a quick audible tick without blocking sensor reading for too long.C++ Source
1
2
3
4
5
6
7
tone(buzzerPin, beepFrequency);
lastBeepStart = now;
if (lastBeepStart > 0 && now - lastBeepStart >= beepDurationMs) {
noTone(buzzerPin);
lastBeepStart = 0;
}▸ LCD Shows BPM and Raw Signal
The LCD shows the estimated BPM on the first row and the raw sensor signal on the second row. The raw signal is included on purpose. It helps learners tune the threshold because they can see whether the sensor value is rising above the detection line.
C++ Source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
lcd.setCursor(0, 0);
lcd.print("BPM: ");
if (bpm > 0) {
lcd.print(bpm);
lcd.print(" ");
} else {
lcd.print("-- ");
}
lcd.setCursor(0, 1);
lcd.print("Signal: ");
lcd.print(signal);
lcd.print(" ");Adjusting the Heartbeat Threshold For Your Sensor
If the BPM never appears, watch the raw signal on the LCD. If the signal peaks around 535, the default threshold of 560 is too high for that setup. Lower it in small steps, such as 560 to 545, then test again.
If the BPM appears but jumps wildly, the threshold may be too low or the sensor contact may be moving. Raise
beatThreshold, keep releaseThreshold lower than it, and hold the sensor more steadily. The best result usually comes from both better placement and a threshold that matches the observed signal.Taking It Further
Once the BPM display works, you can add an LED that flashes with the buzzer so each detected beat has both visual and audio feedback. You could also store the last few BPM values and display an average to make the reading less jumpy.
For a more polished version, try showing a warning when the signal stays too low for several seconds. That would teach the user to adjust their finger instead of trusting a blank or unstable BPM value.
Project Category
Keywords
#Arduino #Heartbeat #BPM #Pulse Sensor #LCD #Buzzer #Analog #Intermediate
Total word count: 1787 words
More in Arduino Uno
Continue exploring this hardware category
More in Buzzer
Continue exploring this hardware category
Project discussion
Questions, feedback, and community insights
No discussions yet. Be the first to start!








