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

  • Raspberry Pi Zero
  • I2C 1602 LCD

  • Pulse Sensor
  • ADS1115
  • Jumper wires (Male to Male)
  • Jumper wires (Male to Female)
  • Breadboard
  • 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