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

 

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

01 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 build a Heartbeat Counter that displays beats per minute on an LCD, using a Raspberry Pi Zero.

Project Use Case

This project is a good introduction to reading analog sensor data on a Raspberry Pi (via an external ADC), processing a noisy waveform to extract a meaningful signal, and displaying live results on an I2C character LCD — techniques that extend well beyond heart-rate monitoring into any analog-sensing project.

02 Hardware and Software Components

Gather everything below before you start.

Hardware Components

Component Description
Raspberry Pi Zero Main computer that reads the sensor data and drives the LCD.
I2C 1602 LCD Displays the calculated heart rate (BPM).
Pulse Sensor Optical sensor that detects blood flow to measure heartbeats.
ADS1115 16-bit ADC that reads the Pulse Sensor's analog output for the Raspberry Pi (which has no built-in analog input).
Jumper Wires (Male-to-Male) Connects components on the breadboard.
Jumper Wires (Male-to-Female) Connects breadboard components to the Raspberry Pi's GPIO header.
Breadboard Platform for building and testing the circuit.

Software Tools

Software Description
VNC Viewer Used to remotely access the Raspberry Pi Zero's desktop.
Python 3 (IDLE) Used to write and run the heartbeat-counting script.

Libraries Used

Library Purpose
time Standard Python library used for timing the pulse waveform.
Adafruit_ADS1x15 Driver library for reading analog values from the ADS1115 ADC.
I2C_LCD_driver Custom driver script (included below) for controlling the I2C 1602 LCD.

03 Application Discussion

How the Pulse Sensor Works

The Pulse/Heartbeat Sensor works in a fairly simple way. The sensor has two sides — on one side sits an LED alongside an ambient light sensor, and on the other side is the circuitry responsible for amplification and noise cancellation. The LED on the front of the sensor is placed directly over a vein, typically on a fingertip or ear tip.

The LED emits light that falls directly on the vein. Veins only have blood flowing through them when the heart is pumping, so monitoring blood flow lets you monitor heartbeats. When blood flow is detected, the ambient light sensor picks up more reflected light; this minor change in received light is analyzed over time to determine 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, plus +5VDC 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 a single slave or multiple slaves. This is useful when you want more than one microcontroller logging data to a single memory card or displaying text on a single LCD.

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

ADS1115

The ADS1115 is a 16-bit Analog-to-Digital Converter that can greatly improve resolution and measurement accuracy. It has four input channels that can be configured for single-ended, differential, or comparator measurements.

04 Hardware Setup

Wire the Pulse Sensor, ADS1115, and LCD to the Raspberry Pi Zero as described below.

Pulse Sensor Setup

Sensor Pin Connect To
VCC Raspberry Pi Zero 3V3 pin
GND Raspberry Pi Zero GND pin
Note: The Pulse Sensor's analog output pin connects to the ADS1115 (see below), not directly to the Raspberry Pi, since the Pi has no analog input pins.

ADS1115 Setup

ADS1115 Pin Connect To
SDA Raspberry Pi Zero GPIO 2 (SDA)
SCL Raspberry Pi Zero GPIO 3 (SCL)
VCC Raspberry Pi Zero 3V3
GND Raspberry Pi Zero GND

I2C 1602 LCD Setup

LCD Pin Connect To
SDA Raspberry Pi Zero GPIO 2
SCL Raspberry Pi Zero GPIO 3
VCC Raspberry Pi Zero 5V pin
GND Raspberry Pi Zero GND pin

05 Software Setup

Follow these steps before running the script.

Step 1 — Set Up Remote Access (Optional)

Install VNC Viewer on your computer if you'd like to access your Raspberry Pi Zero's desktop remotely.

Step 2 — Install the ADS1x15 Library

Install the Adafruit_ADS1x15 Python library on your Raspberry Pi Zero, following the installation instructions in its repository.

Step 3 — Add the I2C LCD Driver

Create a new Python file named I2C_LCD_driver.py, copy in the driver code from the Code section below, and save it in the same folder as your main script.

Step 4 — Run the Script

Save the main Python code from the Code section as its own file in the same folder, then run it with Python 3 (IDLE or terminal). Place your finger on the Pulse Sensor and watch the LCD for your BPM reading.

06 Code

Read the Code Breakdown section to understand what each part does.

I2C_LCD_driver.py

Python
# -*- 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)
Note: Create a new script named I2C_LCD_driver.py, copy this code into it, and save it in the same folder as your main script.

heartbeat_counter.py

Python
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)
Note: This script uses Python 2-style print statements (e.g. print 'BPM: {}'.format(BPM)). If you're running Python 3, update these to function-call syntax, e.g. print('BPM: {}'.format(BPM)).

07 Code Breakdown

Here is what the main parts of heartbeat_counter.py do.

Reading the Signal and Finding Peak/Trough

Python
# 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

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

This reads the analog value from the pulse sensor via the ADC, then tracks the peak (P) and trough (T) of the pulse waveform while filtering out noise.

Detecting a Heartbeat and Calculating BPM

Python
#  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 :
        secondBeat = False;
        for i in range(0,10):             # seed the running total to get a realisitic BPM at startup
          rate[i] = IBI;                      

      if firstBeat :
        firstBeat = False;
        secondBeat = True;
        continue                              # IBI value is unreliable so discard it

      # keep a running total of the last 10 IBI values
      runningTotal = 0;
      for i in range(0,9):
        rate[i] = rate[i+1];
        runningTotal += rate[i];

      rate[9] = IBI;
      runningTotal += rate[9];
      runningTotal /= 10;                     # average the last 10 IBI values 
      BPM = 60000/runningTotal;               # how many beats can fit into a minute? that's BPM!

This is where a heartbeat is detected and BPM (Beats Per Minute) is calculated. When the signal surges above the threshold and no pulse is currently flagged, it's treated as a new beat: the time since the last beat (IBI, inter-beat interval) is measured, and the first two beats are used to seed a running average rather than being trusted outright (their IBI values are unreliable). From then on, the code keeps a running total of the last 10 IBI values, averages them, and converts that average into BPM.

Displaying and Resetting

Python
if Signal < thresh and Pulse == True :   # when the values are going down, the beat is over
    Pulse = False;
    amp = P - T;                           # get amplitude of the pulse wave
    thresh = amp/2 + T;                    # set thresh at 50% of the amplitude
    P = thresh;
    T = thresh;

if N > 2500 :                          # if 2.5 seconds go by without a beat
    thresh = 512;
    P = 512;
    T = 512;
    lastBeatTime = sampleCounter;
    firstBeat = True;
    secondBeat = False;
    print "no beats found"
    mylcd.lcd_display_string("Heart Rate:")
    mylcd.lcd_display_string("no beats found",2,0)

Once a BPM value is calculated and matches the previous reading (lastBPM == BPM), it's considered stable and gets printed to the LCD — showing "Please Wait" if the reading is above 120 BPM (likely noise), or the actual BPM if it's below 120. If 2.5 seconds pass without detecting a beat, the thresholds reset to their defaults and "no beats found" is shown, so the algorithm can recover cleanly the next time a finger is placed on the sensor.

08 Conclusion

There are many types of sensors today applicable to health monitoring and the medical field. The pulse sensor is a great example — it lets us build a project that counts our heartbeat and helps us keep an eye on our heart health.

09 References

  • Arduino ADS1115 Module Getting Started Tutorial — Henry's Bench
    (http://henrysbench.capnfatz.com/henrys-bench/arduino-voltage-measurements/arduino-ads1115-module-getting-started-tutorial/)
  • Pulse Sensor — Components101
    (https://components101.com/sensors/pulse-sensor)
  • Basics of the I2C Communication Protocol — Circuit Basics
    (http://www.circuitbasics.com/basics-of-the-i2c-communication-protocol/)

10 Project Authors

  • Jason Lopez
Quality Checked by
  • Lukie-Mar Lonzaga
  • Miles Darren Bagnol
  • Elijah Joseph Orias
Heart Beat Counter on Raspberry Pi Zero Using Pulse Sensor and LCD Display – CreateLabz

 

Ads1115, Heart beat sensor, Heartbeat, I2c, I2c 1602 lcd, Knowledgebase, Pulse, Pulse sensor, Raspberry pi, Raspberry pi zero

Leave a comment

All comments are moderated before being published