In this tutorial, I am going to show you how the KY-039 module works and use it to develop a simple heartbeat monitoring system with an Arduino Mega 2560.
This tutorial can be a good kickstart for future development on digital health applications. One can easily imagine this kind of technology being used in hospital oximeters, which only require a patients’ finger to detect its heartbeat rate and blood oxygen levels (Figure 1). In this tutorial, the goal is to simply detect the user’s heartbeat with a KY-039 heartbeat sensor and a buzzer, while using an OLED display for guidance and animation purposes.

Parts Required
- Arduino Mega 2560;
- USB 2.0 Cable Type A/B;
- KY-039 Heartbeat Sensor;
- SSD1306 OLED Display;
- 1 Active Buzzer;
- Male to male jumper wires;
- 1 Breadboard;
Note: Since only 1 analog and digital pins are required for you to follow this tutorial, an Arduino Nano, ESP32 NodeMCU board or any similar board can be used instead. Just be sure to change the pins in the code if necessary.
The KY-039 Heartbeat Sensor
The heartbeat sensor used in this tutorial is the KY-039 module, which is composed of an Infrared Light-Emitting Diod (IR LED) and a Phototransistor, as seen in Figure 2.

As one may suggest if a finger is placed between the IR LED and the phototransistor, the heartbeat can be detected at the signal output. In order to understand how this works, one must know the working principle of a phototransistor, which is similar to a normal transistor. Shortly, and considering NPN type transistors, in a normal transistor (Figure 3a) the higher the control voltage applied between the base and the emitter, the higher the current flowing through it (from the collector to the emitter). In a phototransistor (Figure 3b), however, the control voltage applied is directly dependent on incident light, and so the higher the incident light, the higher the current passed through the phototransistor.

Regarding the KY-039 heartbeat sensor circuit, which can be analyzed in Figure 4, by adding a resistor in series with the phototransistor (R1), one can observe the following when measuring the voltage in S:
- If there is a lot of light shining on the phototransistor’s surface, a relatively large current flows through it, and so the voltage in S is close to 0V;
- If the phototransistor is in the dark, a relatively small current flows through it, and so the voltage in S is close to Vcc.

We know that our hand skin can be shone through, especially at the tip of our fingers, as seen in Figure 5.

This is also true if infrared light is used, such as in the KY-039 heartbeat sensor. If a finger is placed and a blood vessel is between the IR LED and the phototransistor, the blood flow can be recognized since blood has different densities at different points in the vein. Therefore, differences in brightness can be identified, and hence the heartbeat can be detected.
In this tutorial, the heartbeat monitoring system to be developed will include a buzzer and an OLED display. The buzzer is used to produce a “beep” every time a rising edge in the heartbeat signal from the sensor is detected. As for the OLED display, it is used to indicate the user to put its finger in the sensor and then to show a heart beating animation.
Circuit
Now, let’s get to the fun part! The KY-039 heartbeat sensor is very easy to use since it has only 3 pins, as shown in this sensor’s pinout (Figure 6).

Start by connecting the pin closer to the “–” sign (GND) to ground and the middle pin (Vcc) to 5V. Then, connect the pin next to the “S” (Signal) to the analog pin 0 (A0) of the Arduino Mega.
Regarding the SSD1306 OLED Display, connect its VCC pin to the 3.3V output on the Arduino and connect GND to ground. Then, connect the SCL and SDA pins to the I2C clock (pin 21) and the I2C data (pin 20) pins on the Arduino Mega board, respectively. You can find more information about this module in this tutorial.
Finally, connect the positive terminal of the buzzer to Arduino digital pin 2 and the negative terminal to ground (check this tutorial for more info on active and passive buzzers). In the end, your circuit should be set as shown in Figure 7.

Code
In this tutorial, the use of the Arduino IDE Serial Plotter is very important to check if our signal
resembles a heartbeat characteristic pulsating signal. You can access the Serial Plotter in the Tools menu.
If one just tries to read and plot the output from the KY-039 heartbeat sensor, something similar to Figure 8 will result.

It doesn’t look much like a pulsating signal, right? Well, that is because the phototransistor is detecting all the insignificant shades caused by the blood flow. Another thing to take into consideration is ambient light, which can severely affect the sensor’s output. To mitigate this noise, only an average of the last 20 readings is taken into consideration. Moreover, I wanted this project to look as much as possible like a hospital oximeter, so I have developed a very sophisticated enclosure prototype for my sensor (a tiny board box 😂). With these measures, it was possible to obtain the pulsating signal shown in Figure 9.

Much better! Now that we have our heartbeat signal, it is necessary to identify when a finger is placed in the sensor, in order for us to control the OLED display operation. To do so, I have defined an output threshold based on the difference in the output values given by my KY-039 heartbeat sensor whenever I had and did not have my finger in the sensor. In Figure 10, it is clearly possible to see this difference, and so I have defined a conservative threshold value of 700. Remember that what the sensor provides is just an analog value from 0 to 1023, indicating how the amount of infrared light reaches the phototransistor [1].

Finally, in order for the buzzer to be activated and the animation in the OLED display to appear whenever a pulse is detected, the maximum and minimum peaks of the output signal need to be detected. Due to some irregularities in the output signal, I have defined a number of samples (5) so that a peak is detected when 5 consecutive sensor output values are greater than the previous ones (in the case of a minimum). When a minimum is reached, it is followed by a rising edge and so the buzzer is activated and the animation appears in the OLED.
#include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define OLED_RESET LED_BUILTIN // Reset pin #define sensorThreshold 700.00 // Threshold value in order to detect if the user puts a finguer in the sensor #define pulseSamples 20.00 // Number of samples to compute the average value of the sensor's output #define peakSamples 5 // Number of samples to detect a peak in the sensor's output // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Image 1 - full heart const unsigned char heartbeat1 [] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x0f, 0xff, 0x80, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xe0, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x7f, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0x80, 0x03, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xf0, 0x40, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe0, 0x60, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe0, 0x60, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe0, 0xf0, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xc1, 0xf0, 0x3f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0x81, 0xf8, 0x3f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0x81, 0xf8, 0x1f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0x83, 0xfc, 0x1f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0x03, 0xfc, 0x1f, 0xff, 0xff, 0xfc, 0x1c, 0x00, 0x07, 0xfc, 0x0f, 0x80, 0x00, 0x38, 0x18, 0x00, 0x07, 0xfe, 0x0f, 0x00, 0x00, 0x18, 0x08, 0x00, 0x07, 0xfe, 0x07, 0x00, 0x00, 0x18, 0x08, 0x00, 0x0f, 0xff, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x1f, 0xff, 0x06, 0x00, 0x00, 0x20, 0x07, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xc0, 0x1f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xc0, 0x3f, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x3f, 0xfc, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xe0, 0x7f, 0xfc, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x7f, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xf0, 0xff, 0xf0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Image 2 - empty heart const unsigned char heartbeat2 [] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x1f, 0xff, 0x80, 0x01, 0xff, 0xf8, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x07, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xc0, 0x07, 0xff, 0x07, 0xfe, 0x7f, 0xe0, 0xff, 0xe0, 0x07, 0xf8, 0x00, 0xff, 0xff, 0x00, 0x1f, 0xe0, 0x0f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x07, 0xf0, 0x1f, 0xc0, 0x00, 0x3f, 0xfc, 0x00, 0x03, 0xf8, 0x1f, 0x80, 0x00, 0x1f, 0xf8, 0x00, 0x01, 0xf8, 0x3f, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0xfc, 0x3e, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x7c, 0x7e, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x7e, 0x7c, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xfc, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3e, 0xfc, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x3e, 0x7c, 0x00, 0x00, 0xf7, 0x80, 0x00, 0x00, 0x3e, 0x7e, 0x00, 0x00, 0xe7, 0x80, 0x00, 0x00, 0x7e, 0x7e, 0x00, 0x01, 0xe3, 0x80, 0x00, 0x00, 0x7e, 0x3e, 0x00, 0x01, 0xe3, 0xc0, 0x00, 0x00, 0x7c, 0x3f, 0x00, 0x03, 0xc1, 0xc0, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x03, 0xc1, 0xe0, 0x00, 0x00, 0xfc, 0x1f, 0x81, 0xff, 0x81, 0xe3, 0xff, 0xc1, 0xf8, 0x1f, 0x83, 0xff, 0x80, 0xe3, 0xff, 0xe1, 0xf8, 0x0f, 0xc3, 0xff, 0x80, 0xf7, 0xff, 0xe3, 0xf0, 0x0f, 0xc0, 0x00, 0x00, 0x77, 0xff, 0x83, 0xf0, 0x07, 0xe0, 0x00, 0x00, 0x7f, 0x00, 0x07, 0xe0, 0x07, 0xf0, 0x00, 0x00, 0x7f, 0x00, 0x0f, 0xe0, 0x03, 0xf8, 0x00, 0x00, 0x3e, 0x00, 0x1f, 0xc0, 0x01, 0xfc, 0x00, 0x00, 0x3e, 0x00, 0x3f, 0x80, 0x01, 0xfc, 0x00, 0x00, 0x1e, 0x00, 0x3f, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x1c, 0x00, 0x7f, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x00, 0x03, 0xf8, 0x00, 0x00, 0x0f, 0xe0, 0x00, 0x00, 0x07, 0xf0, 0x00, 0x00, 0x07, 0xf0, 0x00, 0x00, 0x0f, 0xe0, 0x00, 0x00, 0x03, 0xf8, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x01, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x03, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x07, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xf8, 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfc, 0x7f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; int sensorPin = A0; int buzzer = 2; int text; int textPixels; char initString[] = "Place your finger in the reader"; float currentRead; float lastRead; int riseCount = 0; int fallCount = 0; bool peak; void setup() { // Define input and output pins pinMode(A0, INPUT); pinMode(buzzer,OUTPUT); Serial.begin(9600); // Initialize with the I2c addr 0x3c display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(WHITE); display.setTextWrap(false); display.clearDisplay(); text = display.width(); textPixels = -12*strlen(initString); // 6 (pixels/character) * 2 (text size) * length of the string = nr. of string pixels } void loop() { // Gives instruction to place a finger in the sensor while(analogRead(sensorPin)<sensorThreshold){ display.clearDisplay(); display.setTextSize(2); display.setCursor(text,22); // String starts appearing from right to left in the OLED display display.println(initString); display.display(); text = text - 3; // Speed at which the string moves in the OLED display (nr. of pixels) if(text < textPixels){ text = display.width(); // When the nr. of pixels moved reaches the total number of pixels, restarts the process } } float heartBeat; int sum = 0; for (int i = 0; i < pulseSamples; i++){ sum += analogRead(sensorPin); // Sum of the sensor's output values } heartBeat = sum / pulseSamples; // Average of the sensor's output values Serial.println(heartBeat); currentRead = heartBeat; // Detection of a rising behaviour in the heartbeat signal if (currentRead > lastRead){ ++riseCount; if (riseCount > peakSamples){ // A peak is reached when n consecutive sensor values are bigger than the previous ones (minimum) if (!peak){ digitalWrite(buzzer, HIGH); // If a minimum is detected, the buzzer is activated // Image 1 appeares in the display display.clearDisplay(); display.drawBitmap(35, 0, heartbeat1 , 64, 64, WHITE); display.display(); peak = true; } riseCount = 0; fallCount = 0; } } // Detection of a falling behaviour in the heartbeat signal if (currentRead < lastRead){ ++fallCount; if (fallCount > peakSamples){ // A peak is reached when n consecutive sensor values are lower than the previous ones (maximum) riseCount = 0; fallCount = 0; peak = false; } } lastRead = currentRead; digitalWrite(buzzer, LOW); // Image 2 appeares in the display display.clearDisplay(); display.drawBitmap(35, 0, heartbeat2 , 64, 64, WHITE); display.display(); }
That’s it! If you enjoyed this tutorial, you can visit our YouTube channel and watch this and many other tutorials. Thanks for following us and be sure to rate, comment and share our content.
References
[1] https://www.hackster.io/Johan_Ha/from-ky-039-to-heart-rate-0abfca
Towards the Future !!!
😉