Heart Beat Counter on Raspberry Pi Zero Using Pulse sensor and LCD Display

Overview

Your heart rate, or pulse, is the number of times your heart beats per minute. Normal heart rate varies from person to person. Knowing yours can be an important heart-health gauge.

In this tutorial we made a Heartbeat Counter which displays the beats per minute in an LCD using a Raspberry Pi Zero.

Hardware Components

Software Components

Application Discussion

How Pulse Sensor works

The working of the Pulse/Heart beat sensor is very simple. The sensor has two sides, on one side the LED is placed along with an ambient light sensor and on the other side we have some circuitry. This circuitry is responsible for the amplification and noise cancellation work. The LED on the front side of the sensor is placed over a vein in our human body. This can either be your Finger tip or you ear tips, but it should be placed directly on top of a vein.

Now the LED emits light which will fall on the vein directly. The veins will have blood flow inside them only when the heart is pumping, so if we monitor the flow of blood we can monitor the heart beats as well.  If the flow of blood is detected then the ambient light sensor will pick up more light since they will be reflected by the blood, this minor change in received light is analysed over time to determine our heart beats.

What is an I2C 1602 LCD

The I2C 1602 LCD module is a 2 line by 16 character display interfaced to an I2C daughter board. The I2C interface only requires 2 data connections, +5 VDC and GND to operate.

I2C (Inter-Integrated Circuit)

In I2C you can connect multiple slaves to a single master and you can have multiple masters controlling single, or multiple slaves. This is really useful when you want to have more than one microcontroller logging data to a single memory card or displaying text to a single LCD.

I2C is a serial communication protocol, so data is transferred bit by bit along a single wire (the SDA line). Like SPI, I2C is synchronous, so the output of bits is synchronized to the sampling of bits by a clock signal shared between the master and the slave. The clock signal is always controlled by the master.

ADS1115

The ADS1115 is a 16 bit Analog-to-Digital Converter that can greatly improve your Arduino resolution and measurement accuracy.   It has four input channels that can be configured for Single Ended, Differential or Comparator Measurements.

Hardware Setup

Pulse sensor setup

Connect the VCC to the 3V3 pin of Raspberry Pi Zero and GND to GND pin.

ADS1115 setup

Connect the SDA pin to GPIO 2 pin (SDA) of Raspberry Pi Zero and SCL to GPIO 3 pin (SCL) and the VCC to 3v3 and GND to GND of Raspberry Pi Zero.

I2C 1602 LCD setup

Connect the SDA and SCL to GPIO 2 and 3 of Raspberry Pi Zero and the VCC to 5v pin and the GND to GND pin.

Code

Libraries Included

I2C_LCD_driver Code

# -*- coding: utf-8 -*-
# Original code found at:
# https://gist.github.com/DenisFromHR/cc863375a6e19dce359d

"""
Compiled, mashed and generally mutilated 2014-2015 by Denis Pleic
Made available under GNU GENERAL PUBLIC LICENSE
# Modified Python I2C library for Raspberry Pi
# as found on http://www.recantha.co.uk/blog/?p=4849
# Joined existing 'i2c_lib.py' and 'lcddriver.py' into a single library
# added bits and pieces from various sources
# By DenisFromHR (Denis Pleic)
# 2015-02-10, ver 0.1
"""

# i2c bus (0 -- original Pi, 1 -- Rev 2 Pi)
I2CBUS = 1

# LCD Address
ADDRESS = 0x27

import smbus
from time import sleep

class i2c_device:
  def __init__(self, addr, port=I2CBUS):
    self.addr = addr
    self.bus = smbus.SMBus(port)

# Write a single command
  def write_cmd(self, cmd):
    self.bus.write_byte(self.addr, cmd)
    sleep(0.0001)

# Write a command and argument
  def write_cmd_arg(self, cmd, data):
    self.bus.write_byte_data(self.addr, cmd, data)
    sleep(0.0001)

# Write a block of data
  def write_block_data(self, cmd, data):
    self.bus.write_block_data(self.addr, cmd, data)
    sleep(0.0001)

# Read a single byte
  def read(self):
    return self.bus.read_byte(self.addr)

# Read
  def read_data(self, cmd):
    return self.bus.read_byte_data(self.addr, cmd)

# Read a block of data
  def read_block_data(self, cmd):
    return self.bus.read_block_data(self.addr, cmd)


# commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80

# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00

# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00

# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00

# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

# flags for backlight control
LCD_BACKLIGHT = 0x08
LCD_NOBACKLIGHT = 0x00

En = 0b00000100 # Enable bit
Rw = 0b00000010 # Read/Write bit
Rs = 0b00000001 # Register select bit

class lcd:
  #initializes objects and lcd
  def __init__(self):
    self.lcd_device = i2c_device(ADDRESS)

    self.lcd_write(0x03)
    self.lcd_write(0x03)
    self.lcd_write(0x03)
    self.lcd_write(0x02)

    self.lcd_write(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_4BITMODE)
    self.lcd_write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)
    self.lcd_write(LCD_CLEARDISPLAY)
    self.lcd_write(LCD_ENTRYMODESET | LCD_ENTRYLEFT)
    sleep(0.2)


  # clocks EN to latch command
  def lcd_strobe(self, data):
    self.lcd_device.write_cmd(data | En | LCD_BACKLIGHT)
    sleep(.0005)
    self.lcd_device.write_cmd(((data & ~En) | LCD_BACKLIGHT))
    sleep(.0001)

  def lcd_write_four_bits(self, data):
    self.lcd_device.write_cmd(data | LCD_BACKLIGHT)
    self.lcd_strobe(data)

  # write a command to lcd
  def lcd_write(self, cmd, mode=0):
    self.lcd_write_four_bits(mode | (cmd & 0xF0))
    self.lcd_write_four_bits(mode | ((cmd << 4) & 0xF0))

  # write a character to lcd (or character rom) 0x09: backlight | RS=DR<
  # works!
  def lcd_write_char(self, charvalue, mode=1):
    self.lcd_write_four_bits(mode | (charvalue & 0xF0))
    self.lcd_write_four_bits(mode | ((charvalue << 4) & 0xF0))
  
  # put string function with optional char positioning
  def lcd_display_string(self, string, line=1, pos=0):
    if line == 1:
      pos_new = pos
    elif line == 2:
      pos_new = 0x40 + pos
    elif line == 3:
      pos_new = 0x14 + pos
    elif line == 4:
      pos_new = 0x54 + pos

    self.lcd_write(0x80 + pos_new)

    for char in string:
      self.lcd_write(ord(char), Rs)

  # clear lcd and set to home
  def lcd_clear(self):
    self.lcd_write(LCD_CLEARDISPLAY)
    self.lcd_write(LCD_RETURNHOME)

  # define backlight on/off (lcd.backlight(1); off= lcd.backlight(0)
  def backlight(self, state): # for state, 1 = on, 0 = off
    if state == 1:
      self.lcd_device.write_cmd(LCD_BACKLIGHT)
    elif state == 0:
      self.lcd_device.write_cmd(LCD_NOBACKLIGHT)

  # add custom characters (0 - 7)
  def lcd_load_custom_chars(self, fontdata):
    self.lcd_write(0x40);
    for char in fontdata:
      for line in char:
        self.lcd_write_char(line)

Create a new script, and then name it I2C_LCD_driver. And then copy the code and save it in the folder where your main script is located.

Python Code

import time
# Import the ADS1x15 module.
import Adafruit_ADS1x15
import I2C_LCD_driver
from time import sleep, strftime


mylcd = I2C_LCD_driver.lcd()

adc = Adafruit_ADS1x15.ADS1015()
# initialization 
GAIN = 2/3  
curState = 0
thresh = 525  # mid point in the waveform
P = 512
T = 512
stateChanged = 0
sampleCounter = 0
lastBeatTime = 0
firstBeat = True
secondBeat = False
Pulse = False
IBI = 600
rate = [0]*10
amp = 100
lastBPM = 0
exitfunc = 0
startfunc = 1

lastTime = int(time.time()*1000)

while True:
        # read from the ADC
        Signal = adc.read_adc(0, gain=GAIN)   #TODO: Select the correct ADC channel. I have selected A0 here
        curTime = int(time.time()*1000)

        sampleCounter += curTime - lastTime;      #                   # keep track of the time in mS with this variable
        lastTime = curTime
        N = sampleCounter - lastBeatTime;     #  # monitor the time since the last beat to avoid noise
        #print N, Signal, curTime, sampleCounter, lastBeatTime

        ##  find the peak and trough of the pulse wave
        if Signal < thresh and N > (IBI/5.0)*3.0 :  #       # avoid dichrotic noise by waiting 3/5 of last IBI
            if Signal < T :                        # T is the trough
              T = Signal;                         # keep track of lowest point in pulse wave 

        if Signal > thresh and  Signal > P:           # thresh condition helps avoid noise
            P = Signal;                             # P is the peak
                                                # keep track of highest point in pulse wave

          #  NOW IT'S TIME TO LOOK FOR THE HEART BEAT
          # signal surges up in value every time there is a pulse
        if N > 250 :                                   # avoid high frequency noise
            if  (Signal > thresh) and  (Pulse == False) and  (N > (IBI/5.0)*3.0)  :       
              Pulse = True;                               # set the Pulse flag when we think there is a pulse
              IBI = sampleCounter - lastBeatTime;         # measure time between beats in mS
              lastBeatTime = sampleCounter;               # keep track of time for next pulse

              if secondBeat :                        # if this is the second beat, if secondBeat == TRUE
                secondBeat = False;                  # clear secondBeat flag
                for i in range(0,10):             # seed the running total to get a realisitic BPM at startup
                  rate[i] = IBI;                      

              if firstBeat :                        # if it's the first time we found a beat, if firstBeat == TRUE
                firstBeat = False;                   # clear firstBeat flag
                secondBeat = True;                   # set the second beat flag
                continue                              # IBI value is unreliable so discard it


              # keep a running total of the last 10 IBI values
              runningTotal = 0;                  # clear the runningTotal variable    

              for i in range(0,9):                # shift data in the rate array
                rate[i] = rate[i+1];                  # and drop the oldest IBI value 
                runningTotal += rate[i];              # add up the 9 oldest IBI values

              rate[9] = IBI;                          # add the latest IBI to the rate array
              runningTotal += rate[9];                # add the latest IBI to runningTotal
              runningTotal /= 10;                     # average the last 10 IBI values 
              BPM = 60000/runningTotal;               # how many beats can fit into a minute? that's BPM!
              print 'BPM: {}'.format(BPM)
              mylcd.lcd_display_string("                   ", 2, 0)
              mylcd.lcd_display_string("Heart Rate:", 1, 0)
              
              if lastBPM == BPM:                         
                  if BPM > 120:
                      mylcd.lcd_display_string('Please Wait', 2, 0);
                      
              
                  if BPM < 120:
                      mylcd.lcd_display_string('BPM: {}'.format(BPM),2,0);
                      mylcd.lcd_display_string(' <3',2,9);
                      sys.exit()
              if lastBPM != BPM:
                      mylcd.lcd_display_string('Please Wait :)', 2, 0);
                            
              lastBPM = BPM;
              

        if Signal < thresh and Pulse == True :   # when the values are going down, the beat is over
            Pulse = False;                         # reset the Pulse flag so we can do it again
            amp = P - T;                           # get amplitude of the pulse wave
            thresh = amp/2 + T;                    # set thresh at 50% of the amplitude
            P = thresh;                            # reset these for next time
            T = thresh;

        if N > 2500 :                          # if 2.5 seconds go by without a beat
            thresh = 512;                          # set thresh default
            P = 512;                               # set P default
            T = 512;                               # set T default
            lastBeatTime = sampleCounter;          # bring the lastBeatTime up to date        
            firstBeat = True;                      # set these to avoid noise
            secondBeat = False;                    # when we get the heartbeat back
            print "no beats found"
            mylcd.lcd_display_string("Heart Rate:")
            mylcd.lcd_display_string("no beats found",2,0)
        
       

        time.sleep(0.05)

Code Breakdown

# read from the ADC
        Signal = adc.read_adc(0, gain=GAIN)   #TODO: Select the correct ADC channel. I have selected A0 here
        curTime = int(time.time()*1000)

        sampleCounter += curTime - lastTime;      #                   # keep track of the time in mS with this variable
        lastTime = curTime
        N = sampleCounter - lastBeatTime;     #  # monitor the time since the last beat to avoid noise
        #print N, Signal, curTime, sampleCounter, lastBeatTime

        ##  find the peak and trough of the pulse wave
        if Signal < thresh and N > (IBI/5.0)*3.0 :  #       # avoid dichrotic noise by waiting 3/5 of last IBI
            if Signal < T :                        # T is the trough
              T = Signal;                         # keep track of lowest point in pulse wave 

        if Signal > thresh and  Signal > P:           # thresh condition helps avoid noise
            P = Signal;                             # P is the peak
                                                # keep track of highest point in pulse wave

In this code, we read the analog value from pulse sensor and we find the peak and trough of the pulse wave and removing the noise from the pulse wave.

#  NOW IT'S TIME TO LOOK FOR THE HEART BEAT
          # signal surges up in value every time there is a pulse
        if N > 250 :                                   # avoid high frequency noise
            if  (Signal > thresh) and  (Pulse == False) and  (N > (IBI/5.0)*3.0)  :       
              Pulse = True;                               # set the Pulse flag when we think there is a pulse
              IBI = sampleCounter - lastBeatTime;         # measure time between beats in mS
              lastBeatTime = sampleCounter;               # keep track of time for next pulse

              if secondBeat :                        # if this is the second beat, if secondBeat == TRUE
                secondBeat = False;                  # clear secondBeat flag
                for i in range(0,10):             # seed the running total to get a realisitic BPM at startup
                  rate[i] = IBI;                      

              if firstBeat :                        # if it's the first time we found a beat, if firstBeat == TRUE
                firstBeat = False;                   # clear firstBeat flag
                secondBeat = True;                   # set the second beat flag
                continue                              # IBI value is unreliable so discard it


              # keep a running total of the last 10 IBI values
              runningTotal = 0;                  # clear the runningTotal variable    

              for i in range(0,9):                # shift data in the rate array
                rate[i] = rate[i+1];                  # and drop the oldest IBI value 
                runningTotal += rate[i];              # add up the 9 oldest IBI values

              rate[9] = IBI;                          # add the latest IBI to the rate array
              runningTotal += rate[9];                # add the latest IBI to runningTotal
              runningTotal /= 10;                     # average the last 10 IBI values 
              BPM = 60000/runningTotal;               # how many beats can fit into a minute? that's BPM!
              print 'BPM: {}'.format(BPM)
              mylcd.lcd_display_string("                   ", 2, 0)
              mylcd.lcd_display_string("Heart Rate:", 1, 0)
              
              if lastBPM == BPM:                         
                  if BPM > 120:
                      mylcd.lcd_display_string('Please Wait', 2, 0);
                      
              
                  if BPM < 120:
                      mylcd.lcd_display_string('BPM: {}'.format(BPM),2,0);
                      mylcd.lcd_display_string(' <3',2,9);
                      sys.exit()
              if lastBPM != BPM:
                      mylcd.lcd_display_string('Please Wait :)', 2, 0);
                            
              lastBPM = BPM;
              

        if Signal < thresh and Pulse == True :   # when the values are going down, the beat is over
            Pulse = False;                         # reset the Pulse flag so we can do it again
            amp = P - T;                           # get amplitude of the pulse wave
            thresh = amp/2 + T;                    # set thresh at 50% of the amplitude
            P = thresh;                            # reset these for next time
            T = thresh;

        if N > 2500 :                          # if 2.5 seconds go by without a beat
            thresh = 512;                          # set thresh default
            P = 512;                               # set P default
            T = 512;                               # set T default
            lastBeatTime = sampleCounter;          # bring the lastBeatTime up to date        
            firstBeat = True;                      # set these to avoid noise
            secondBeat = False;                    # when we get the heartbeat back
            print "no beats found"
            mylcd.lcd_display_string("Heart Rate:")
            mylcd.lcd_display_string("no beats found",2,0)

This is the code were we find the heart beat and calculate BPM (Beats Per Minute) and if the the lastBPM is equal to the current BPM reading it will now display the BPM in the LCD. This means that it is now showing the actual BPM.

Conclusion

There so many types of sensors today that is applicable in health monitoring or in medical field. Like the pulse sensor, it allows us to create a project that helps us count our heartbeat and also helps us monitor our heart and health.

References

[1] http://henrysbench.capnfatz.com/henrys-bench/arduino-voltage-measurements/arduino-ads1115-module-getting-started-tutorial/

[2] https://components101.com/sensors/pulse-sensor

[3] http://www.circuitbasics.com/basics-of-the-i2c-communication-protocol/

The post Heart Beat Counter on Raspberry Pi Zero Using Pulse sensor and LCD Display appeared first on CreateLabz.

Ads1115Heart beat sensorHeartbeatI2cI2c 1602 lcdKnowledgebasePulsePulse sensorRaspberry piRaspberry pi zero

Leave a comment

All comments are moderated before being published