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
- time
- Adafruit_ADS1x15
- I2C_LCD_driver
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
[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.