How to Make a Heartbeat Monitoring System with Heartbeat Sensor – KY-039

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.

Figure 1 – Hospital oximeter

Parts Required

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.

Figure 2 – KY-039 heartbeat sensor

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.

Figure 3 – a) NPN transistor and b) NPN 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.
Figure 4 – KY-039 heartbeat sensor circuit

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

Figure 5 – Light shining through human hands

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).

Figure 6 – KY-039 heartbeat sensor pinout

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.

Figure 7 – Circuit’s schematics

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.

Figure 8 – Raw output of the KY-039 heartbeat sensor

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.

Figure 9 – Resulting pulsating signal of the KY-039 heartbeat sensor

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].

Figure 10 – KY-039 heartbeat sensor’s output differences

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 !!!

😉

Leave a Reply

Your email address will not be published.