This Heart Rate Sensor Arduino Project shows you How to make an infrared pulse
sensing detector from first principles. Using an Arduino, a cheap opamp,
a
matched IR pair and some passive components for a pulse rate sensor.
This heart rate sensor for the Arduino is a PPG or photoplethysmograph
project or pulse rate sensor using infrared reflected light. Yes I
absolutely hate that word and have to think how to say
it.
But first, since it is such a ridiculous word what does that word mean?
Photo added to the front end and From Greek Plethysmos
(increasing,enlargement), graphos (to write). So it is a Light (Photo) Volume
increasing (Plethysmos) written (Graph) measurement.
Photoplethysmography is a method for measuring the change in volume of an
organ resulting from changes in the volume of air or blood it contains and then
drawing it out in a graphical form. In this case you are measuring the
operation of the heart by detecting reflected infrared light, from a fingertip
sensor, which is changing due to the blood flowing in the finger.
From now on we'll just stick to PPG for the rest of this discussion!
This project uses a matched pair of infrared transmitter and receiver
components (Phototransistor and LED) to produce a useful PPG signal that can be
amplified using an opamp. The output is then converted using a microcontroller
ADC and processed by the microcontroller to give a heart rate reading in
beats-per-minute.
Warning: This project is not for
medical use.
The originating signal is the most critical part of the project and to get a
good signal requires the most appropriate sensor (a matched Infrared pair) used
in reflection mode. You can arrange the sensors beside each other (reflection
of IR detected) or one on top and one below the finger (transmission of IR
light through the finger).
The pair of sensors in this project are used in reflection mode providing a
useful output that is extremely sensitive to changes in reflected light from
blood flow in the finger.
In fact the output is so sensitive that any standard light bulbs will
trigger the output, just by waving your hand between the light and sensor) This
is why professional devices use a light excluding cover to stop false
triggering as you need that high sensitivity to get a good PPG output. I made
up a simple little cardboard cover stuck together with a glue-gun!
KY-039 - this is Rubbish!
At first I tried using the KY-039 device (very frustrating) as it's output
is far too low to get useful operation; You need a gain of 10000, and even then
you have to keep very still to allow the signal to just peak above the noise!
(that is if it is positioned exactly right really just luck).
TIP: Don't bother using the KY-039 - it is useless!
This is what you need to use: A matched IR pair. One generates the IR output and the other receives the IR signal.
Using and matched pair, and especially those that are designed to emit and
receive Infrared light allows a far bigger signal that is not mired in noise
and it only requires a modest nominal gain of 100. Basically do not use the
KY-039 and don't waste any time on it.
Heart rate sensor Arduino: Infrared TX, RX pair
For this project a matched pair of emitter 409 and receiver is used 309:
SHF409: Emitter If (max) 100mA, and Vf 1.3V, Output : 950nm
SFH309: Receiver Ic=15mA (surge 75mA,Vce 35V, Input Range : 380 ~ 1180nm,
Max sensitivity 860nm
Note: You can use any similar matched pair of IR TX and RX Diode and
transistor but note that the SHF409 emitter is capable of handling a maximum
continuous current of 100mA (even though it is in a very small package) and
that a standard remote IR TX diode is not capable of this current level. If you
use a different IR transmitter then change the current limit resistor to stop
blowing it up! i.e. check the datasheet for your device.
Warning: These devices emit highly concentrated invisible
light which can be hazardous to the human eye. So cover the devices and do not
put them near your eyes.
Heart rate sensor Arduino: Software Operation
The objective for the pulse rate sensor software is to detect peaks and
record the number of peaks over a 10 second period (changeable in software) and
then work out the heart rate in beats-per-minute. Detecting peaks requires
assumptions about the incoming signal since a normal PPG has many troughs and
valleys that have to be ignored.
One approach is to use a moving average filter to entirely smooth out the
signal so that reduced data is processed i.e. smoothing out all those troubling
noise fluctuations. That is slightly boring as it is interesting to see a more
real-life signal (with interesting signal transitions) so in this project the
output from the microcontroller is left intact i.e. the raw ADC outputs are
used. Instead of smoothing, an algorithm is used to ignore the parts of the
signal that are fluctuating all over the place.
The plot below uses the Serial Monitor of the Arduino IDE (opDebug is
defined in the code) showing the variables in the software : rawValue
(PPG blue), max_peak (maximum peak PPG signal- red ) and avg (average
faded pink). For this average, the left side shows the start of the
measurement period where the
average is
high and later settles to the average signal level after several pulses
have passed):
Typical Heart rate sensor Arduino PPG output (blue) with pulse detected (red line) and average
(faded pink)
The algorithm has found the maximum peak (indicated by the red line at the
top of the signal) and has stored this result in software (incrementing the
variable num_peaks).
Note how the top of the max_peak value is held for a time
(set in definition DLY_IGNORE) which shows the rest of the algorithm ignoring
the input signal for a set millisecond period (in this case 130ms) a
separate part of the program holds the peak value (max_peak variable) constant
while this time passes (which is not strictly necessary but allows
visualisation of the delay).
After this, the max peak value is decayed away
allowing the algorithm to find the next peak even if it is not as high as the
previous one i.e. for an erratic pulse or when the signal is not as large if
you move your finger away from the sensor etc.
Heart rate sensor Arduino: Detecting a PPG signal
If there is a real PPG signal the the difference between the 'max_peak' and
'avg' variables give a signal rejection method. In the software if the
difference is below 60 ADC values set in definition NOISE_THRESHOLD (with 5V
PSU this is 292mV) then the signal is considered to be noise and is therefore
ignored. In the image above you can see that the signal peak that we are
interested in (blue) is a long way above the average level (yellow).
The software effectively ignores anything below the noise threshold level
i.e when no PPG signal is output the signal varies between 170 to 185 (ADC
value) and is therefore ignored.
Heart rate sensor Arduino: Detecting the peak
Reliably detecting the signal peak requires a few calculated variables to
process the incoming signal:
rawValue - current ADC reading.
up - 1 if we are traversing the signal on the way up, and 0 for going
down.
raw_tot- total of all samples within the measurement period.
raw_num- number of samples within the measurement period.
avg, (from raw_tot and raw_num) the average ADC reading (over the
measurement period).
max_peak- Max ADC value in the measurement period (decayed down
slowly).
threshold- max-peak less 1/8th of max_peak.
The simple idea for the software algorithm is to average out the incoming
ADC samples and compare this value to the peak value. If the incoming ADC
sample is over a certain threshold and the peak to average value is above a
noise threshold then assume this is a heartbeat pulse.
Heart rate sensor Arduino: Example Code
Note: Clicking any text in the box below will copy the whole lot to
the clipboard.
Click in the code below to copy it to the
clipboard.
// Copyright John Main: TronicsBench.com
// Free for use in non-commercial projects.
#include <Wire.h>
#define sensorPin A0
#define buzzerPin 11
#define ledPin 13
// Coment out one of the following,
//#define opText // For serial monitor
#define opDebug // For serial plotter
#define MEASURE_TIME 10000
#define NOISE_THRESHOLD 60
#define DLY_IGNORE 130
#define PSEP Serial.print(" ")
intperiod=0,cline=0;
intnum_peaks,max_peak,avg_peak;
unsignedlongtime_was,ctimer;
unsignedlongpeak_time;
unsignedlongraw_tot,raw_num;
unsignedlongthreshold;
voidsetup(){
pinMode(ledPin,OUTPUT);// Inbuilt LED
Serial.begin(9600);
Serial.println("Heart Rate Monitor");
Serial.print(MEASURE_TIME/1000);
Serial.println("sec update...");
tone(11,1000,50);
tone(11,500,150);
peak_time=time_was=millis();
num_peaks=0;
threshold=0;
raw_tot=raw_num=0;
ctimer=millis();
}
voidloop(){
staticintup=1;
unsignedlongms;
intperiod=0;
intavg=0;// temp var.
intrawValue=analogRead(sensorPin);
raw_tot+=rawValue;
raw_num++;
avg=raw_tot/raw_num;
// Decay max peak to allow change to lower peak values.
// debug plot (millis()-peak_time) - not algo. shows delay on plot = straight line.
if((millis()-peak_time)>DLY_IGNORE)max_peak--;
// Capture max peak.
if(rawValue>max_peak)max_peak=rawValue;
// Set the threshold 1/8th below the max peak.
threshold=(max_peak-(max_peak>>4));
#ifdef opDebug
// Raw outputs for serial plotter
Serial.print(rawValue);PSEP;
Serial.print(avg);PSEP;
Serial.print(max_peak);PSEP;
Serial.print(threshold);
Serial.println("");
#else
delay(15);
#endif
#ifdef opText
if((millis()-ctimer)>1000){// Confidence timer
ctimer=millis();
Serial.print(",");
cline++;
if(cline%61==0)Serial.println();// Line wrap
}
#endif
// Value must be > minimum,
// and bigger than a fraction below the max measured peak,
// and not found within x ms of the last peak (100ms :max 300bpm).
//
if(up==1&&threshold>avg&&
rawValue>threshold&&
rawValue-avg>NOISE_THRESHOLD&&
((millis()-peak_time)>DLY_IGNORE)
){
ms=millis();
peak_time=ms;
up=0;
// Check n second timeout after a peak
period=(ms-time_was);
if(period>MEASURE_TIME){// Output and restart if > n secs period finished.
#ifdef opText
Serial.println();
Serial.print(MEASURE_TIME/1000);
Serial.print(" sec: ");
Serial.print(period);
Serial.print(" n: ");
Serial.print(num_peaks);
Serial.print(" peak: ");
Serial.print(max_peak);
Serial.print(" num peaks ");
Serial.print(num_peaks);
// Since this is not an interrupt driven timer, nominal n sec period will be larger than 5000 ms.
// so work out beats in period for 1min
Serial.print(" BPM ");
Serial.println(num_peaks*(60000/(float)period));
#else
delay(8);
#endif
time_was=ms;
num_peaks=0;
max_peak=0;
avg=0;
cline=0;// Don't need newline as output is generated.
raw_tot=raw_num=0;
}
// Indicators
digitalWrite(ledPin,1);
tone(11,500,30);
digitalWrite(ledPin,0);
num_peaks++;
}elseif(up==0&&rawValue<threshold)up=1;
}
Heart rate sensor Arduino: Code operation
In the main loop the sequence of operations is as follows:
Get an ADC sample : rawValue
Update raw_tot and raw_num and calculate avg.
If the time since a peak was last found is greater than DLY_IGNORE then
decay max_peak.
Capture the new max_peak value if the ADC value is greater than the current
rawValue.
Work out the threshold value (max_peak minus 1/8th).
Print values if opDebug is defined.
Print confidence output if opText is defined.
Work out if we have found a peak i.e. when:
The threshold is greater than the average signal
and...
The rawValue is greater than the threshold
and...
The difference between the rawValue and average is
> the noise level and...
We are not in an ignore signal period.
If the above is true then ask the question:
has the measurement period ended?
If yes then display BPM. And reset period
measurement variables.
If this is not a peak then check for down traversing ADC value:
by checking that
We found a peak when up is set to 0 by the peak
detection calculation and
and that the rawValue has fallen below the threshold
(definitely going down).
If the above is true the set up to 1 to find the
next peak signal
Heart rate sensor Arduino: Hardware operation
The pulse rate sensor hardware consists of the matched Infrared Transmitter
and receiver pair and an AC configured opamp amplifier. The opamp used is an
older chip. A more modern chip can go rail to rail but the CA3140 goes only to
about 2.6V for a 5V supply.
Even though a really bad headroom voltage of 2.4V sound terrible, by
careful design you can work within this limitation - the key is to work
out the signal levels you get from the IR pair. The output of the
IR-pair is very small - that's good thing!
If you don't have a CA3140 use a more modern opamp
(I have a few of these older devices and just used that type). A better choice
would be an MCP601 as this has a higher specification and is rail-to-rail
output although rail-to-rail output is not strictly required for this project
to operate. What ever you do make sure that the datasheet states that the
device can operate on a single supply i.e. 0 to 5V.
Note however that a higher spec. opamp would not improve the circuit too
much, as the frequencies involved are extremely low and the gain roll-off is
dominated by the filter values (1uF,470k,100k). Noise performance would be
improved a little though.
In fact you don't need rail-to-rail operation (since the circuit operates
just fine without that). The full ADC range is not used but the circuit
provides enough range for useful output using about half of the ADC range.
Note: You could feed in a reference voltage to the Arduino to allow the full
ADC range over the 2.6V (with a few adjustments to the ADC code) which would
the give finer resolution. However the most important aspect of the circuit is
obtaining a good signal in the first place and that depends on the IR matched
pair.
Note: Also
connect a piezo speaker across Arduino Uno pin 11 and GND.
A.C. Amplifying opamp configuration
The opamp is setup in A.C. amplifier mode; C4 decouples the gain
setting resistors R5 and R6, while C1 presents an A.C. signal from the
IR-pair. This is done for two reasons
Since the supply is 0~5V the input signal must always be above ground.
The offset voltage of the opamp is large 2~5mV and the gain
setting is large (R5,R6). If a D.C. configuration were used the
amplification of Vos would result in an output of 5V i.e. it would be
useless!
C1 allows only A.C. input while R5 and R6 are decoupled by C4 i.e.
only A.C. signals are amplified by these gain setting resistors.
Heart rate sensor Arduino: D.C. Bias for the A.C. opamp
The resistors R3 and R4 set the d.c. bias the level for the opamp at 5.0
*(100.0/(100.0+470)) = 0 .877V to raise the input signal level above zero
volts. This bias is not amplified (since the a.c. Amplifier configuration only
allows changing current to pass - using C1) so this level is passed straight to the
output.
This is done primarily to locate the PPG signal for viewing in the Serial
Plotter. If you look back at the PPG plot and at the yellow line (the average),
this is also the average D.C. level and it has a value of about 180 ADC counts
which is : (5/1024.0)*180 = 0.878V.
You could just forget about this D.C. Level (but it looks so much better on
the output). The other reason for the unbalanced values of R3 and R4 is that
the output of the CA3140 is only able to go up to 2.6V so this D.C. Level also
keeps the signal from hitting the top voltage.
If you used a rail-to-rail opamp you would be more likely to make a design
with equal R3 and R4 to set the D.C. Level to the power supply mid point.
Heart rate sensor Arduino: High Pass Corner Frequency - C1, R3, R4
The output from the IR receiver is fed into a 1uF capacitor and then to the
level setting voltage divider R3 and R4 (see above for the bias level). This
capacitor and the two resistors in parallel form a high pass RC filter with a
corner frequency of :
1.0/(2*3.14159*82e3*1e-6) = 1.94Hz.
Frequencies above this are amplified, below this they are reduced.
This is a bit high for a PPG (really 0.15Hz is needed check professional
specifications for exact frequency ranges - but you would need a larger
capacitor or smaller resistors). However the chosen corner frequency gives good
enough results.
Heart rate sensor Arduinor: A.C. Coupling capacitor - C4 R6
The A.C. amplifier uses an a.c. Coupling capacitor value 10uF (C4). High
frequency signals are passed through but at low frequency a.c. is blocked all
the way down to d.c. The higher this capacitance value the lower the corner
frequency above which signals are amplified.
This is the problem with the A.C. amplifier as the frequencies involved are
very low so you need a large coupling capacitor to let the desired signal pass
through i.e. a low corner frequency. In this case this is not really the action
you want from the amplifier; You want gain at lower frequency but the capacitor
reduces it at lower frequency so to allow lower frequency the corner frequency
must be lowered (by either increasing R or C).
The solution is to make this capacitor large. It then provides a corner
frequency (R6 and C4) of:
1.0/(2*3.14159*1e3*10e-6) = 15.91 Hz
Frequencies above this frequency are amplified, below it they are reduced.
It is not really a low enough corner frequency but it allows the signal to be
seen quite well anyway.
Heart rate sensor Arduino: Extra noise removal C3, R5
Electrolytics, as with all capacitors, are not ideal so C3 provides an extra
smoothing by reducing gain at high frequencies (C3 and R5). The corner
frequency is:
1.0/(2*3.14159*1000e3*100e-9) = 1.59Hz
Above this frequencies are reduced due to gain reduction (partly smoothing
the output signal = good).
Heart rate sensor Arduino: Parts List
Item
Part
Description
IDs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1u
100n
10u
SFH409
SFH309
A0
100
10k
470k
100k
1M
1k
CA3140
Arduino Uno
Piezo Disc
Ceramic Capacitor >6V
Capacitor
Electrolytic capacitor
Light emitting diode
Light detecting diode
Output pin
Resistor
Resistor
Resistor
Resistor
Resistor
Resistor
Operational Amplifier
Microcontroller board
Piezo
C1
C2 C3
C4
D1
D2
OP1
R1
R2
R3
R4
R5
R6
U1
Board
SPK1
Note: C1 - this must not be an electrolytic, it must be non polarising
i.e. does not have a polarity.
Conclusions
Limitations and improvements
The Heart rate sensor Arduino algorithm, as it stands provides a simple and
interesting BPM result and waveform output but does not use interrupts. Instead the
code goes round a loop checking whether 10000 ms has passed and if it has it
will calculate the BPM using the actual time that has passed and this will be
greater than 10000 ms (depending on how much debug data is output). Since the
time is known accurately that measurement is still good enough to show the
correct rate. As it is measured over 10 seconds the BPM value will vary
slightly but is broadly accurate.
You could average out several readings for a more stable reading but of
course that takes more time. Another approach would be to measure the peak to
peak times and display an BPM value - this would also require averaging the
output (the readings would vary over time) but would give an output
instantaneous output.
Written by John Main who has a degree in Electronic Engineering.
How to get accurate DHT22 digital humidity sensor readings with an Arduino. Did you know it also measures temperature as Well? Find out why, in this page...
A PIR sensor lets your Arduino sense movement without contact. This tutorial covers PIR sensor basics, connecting one to an Arduino board and coding a motion detector.
Arduino Hall Effect Sensor: Add magnetic sensing superpowers to your Arduino projects with an easy-to-use hall effect sensor. With full code and layout...
Comments
Have your say about what you just read! Leave me a comment in the box below.
Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.