Introduction: Photoplethysmography - (IR Heart Rate Monitor)

This Instructable documents how to create a simple heart rate monitor using Photoplethysmography with an IR phototransistor via transmissive absorption using the Arduino to process the pulsatile data and display live results via a TFT screen.

To use the source code and create the necessary circuitry you will need a reasonable grasp of electronics, knowledge of the Arduino, a DMM and some patience.

The design has been optimised to work with easily obtainable 'off the shelf' commercial parts and re-purposed household items and gives reasonable results.

You will need the following parts;

  1. Arduino Mega 2560 (from SainSmart)
  2. 1 off old coat hanger as depicted in the picture in Step 2 : The Sensor
  3. 1 off 1.8 SPI 128x160 TFT Module. I got mine for £2.79 from ebay. Here;
  4. 2 off TL072 FET OpAmps
  5. 1 off T121 NPN Darlington Transistor
  6. 3 off 1N4148 diodes
  7. 1 off BZY88C 3v3 Zener diode
  8. 1 off BPW96B Phototransistor
  9. 1 off TSAL6400 940nm IR 5mm Led
  10. 1 off Ceramic 1uF capacitor
  11. 2 off 4.7uF Electrolytic capacitors
  12. 1 off 22pF Ceramic capacitor
  13. 1 off 22nF Ceramic capacitor
  14. 1 off 10nF Ceramic capacitor
  15. 1 off 50K 10 turn potentiometer
  16. 3 off 4K7 resistors
  17. 4 off 10R resistors
  18. 2 off 7R5 resistors
  19. 1 off 1M0 resistor
  20. 1 off 3K9 resistor
  21. 2 off 10K resistors
  22. 1 off 100K resistor
  23. 5 off 1K resistors
  24. 1 off 220R resistor

Other than the Arduino Mega 2560 (Genuino), coat hanger and the TFT display, I purchased all the parts from FARNELL in the UK.


WARNING : The details contained herein are for information only and should not be relied upon for accurate heart rate monitoring in a clinical or any other environment.

Step 1: Now for the Science Part!

So what is Photoplethysmography?

Photoplethysmography (PPG) is a simple and low cost optical technique that can be used to detect blood volume changes in the microvascular bed of tissue. It is used to make non-invasive measurements at the surface of the skin.

A PPG waveform comprises two main components; 'AC' arterial pulsatile changes in blood flow synchronised to heart beat and 'DC' elements attributed to venous blood, tissue, respiration, sympathetic nervous system activity and thermoregulation. See diagram above 'Variation in light attenuation by tissue'.

It is these AC changes which are used to extract heart beat.

The interaction of light with human tissue is quite complex and involves; scattering, reflection and absorption. Research has shown that IR light around 940nm gives the deepest penetration and yields the best deep tissue blood flow measurement. See

Detection is achieved by shining a source of illumination (in this case an IR LED) at an optically sensitive receiver (photodiode/phototransistor).

Positioning of sensors is in one of two ways, reflective or transmissive. See diagram above 'Transmissive and Reflective modes'. Transmissive mode yields the best results with IR illumination, which is what this project is based around.

Step 2: The Sensor

The sensor was constructed from an old clothes hanger.

I initially used the sprung metal clip to secure the device to the finger tip, but found it too tight in it's original form, blocking blood flow.

Consequently I opened it up as shown above.

However, once I had opened it up sufficiently to allow for good blood flow in the finger tip, the clip would no longer hold together.

To overcome this I drilled two holes in the steel clip and used M2.5 allen screws to attach it to the sensor platform. Note: As the spring comprised hardened steel I needed to heat it up with a gas blow torch to soften the metal first. You can see the resultant discolouration in the image above.

Once the clamp had been created I drilled 5mm holes into the top and bottom sensor platforms (you may notice there are two holes in one of the plates, this is because I has initially started out trying to calculate Sp02 Max. and required leds of differing wavelength. I quickly determined the electronics and signal processing is probably going to be out of the scope of Instructables. Maybe for another time).

With the holes in place I added 'P' foam used as draft insulation on doors, to give lateral cushioning of the inserted digit and attenuate any incident light from hitting the phototransistor.

Finally I mounted the IR led and Phototransistor on some custom cut veroboard fixed to the sensor platform with M3 screws such that it gives reasonable strain relief for the attached cables.

Step 3: Signal Conditioning


In designing the circuit for the detector I went through many iterations and tried many types of detectors. From photodiodes in both photovoltaic and transconductance mode. Reflective and transmissive sensing methods. With many different sources of illumination including Red 650nm, Green 535nm and IR 940nm.

I finally settled on using a phototransistor with a 940nm IR source being reasonably well matched spectrally as this made the electronics the simplest by 'far'.

As I mention above, the choice of both phototransistor and IR source was specific (stick to what is in the circuit diagram) as this was the best 'off the shelf' match I could obtain.

Description of Circuit

The source IR led (LED1) is illuminated via a constant current arrangement (D1..D3, R5, R6, R18...R21 and T1). The components were chosen to give approximately 100mA through the led. For the TSAL6400 this is the maximum you can drive this led at. It does get warm over time if left on for prolonged periods, though the manufacturers data sheet indicates this is acceptable.

Capacitors C4 and C5 are present to provide supply rail decoupling.

As the Arduino ADC is unipolar, to maximise signal swing I created a False Ground (FG) via IC1A connected as a unity gain buffer amp fed by a constant voltage source formed with ballast resistor R8 and a 3.3V zener diode D5 (3.3V is the most optimal value to give low cost and low drift). So as not to load R8 a 50K pot is used to tap off the reference feed to IC1. C1 is there to prevent any transients from appearing on R4, given 50K is quite a high value.

This false ground feeds the signal conditioning chain connected to the IR sensor T2.

To maximise output from T2 it is coupled across the GND and +5V rails. The emitter resistor R1 was chosen empirically (though is within the manufacturers typical dedicated characteristics for collector light current) so as to give the best response. The emitter of T2 is AC coupled to IC1B and inverting amp via C2 a 1uF ceramic capacitor. The TL07X opamp was chosen as it has a high impedance FET input stage and will minimally load the output of T2. IC1B provides high gain amplification of the photodiode signal. C3 is used to provision some attenuation of transients. The typical output from this amp is given above (note the mains 'hum' on the signal).

R10 and C6 form a simple single pole Low Pass Filter with a break frequency of approximately 3.38Hz or 200BPM. This provides antialiasing for the ADC and mains supply rejection. The typical output for this is also given above (note the improved signal).

IC2A is a non-inverting unity gain buffer used to prevent loading by the next stage.

IC2B is an inverting amp with a gain of approximately 10. It scales the signal such that it will typically by 20% of the supply rail to allow for offset drift. It also allows for easier processing and display in the Arduino once read by the ADC. One less calculation to make.


To calibrate the circuit ensure +5V supply is applied, T1 is switched 'off' and the sensor is shielded from any light. Adjust R4 until output of IC2B is as close to 2.5V as possible.

Practical notes on construction

If you do decide to create this project then here are some things to watch out for;

  1. Use coax to connect to the Phototransistor as in the diagram above. Reduces signal loss and noise.
  2. Ensure there is no solder flux is present between the Emitter and Collector of the photodiode as this can attenuate the signal.
  3. Provide supply via a high capacity battery or good linear PSU (noise free, steer clear of switched mode supplies).
  4. Separate the Arduino from the initial stage of the analogue signal conditioning as far as is practically possible. High speed clock noise from the processor can induce noise in the signal path.
  5. Keep wire lengths as short as possible.
  6. Shroud both your illumination source and sensor as much as is practically possible.
  7. Ensure the sensor clamp is 'sufficient' and not tight on the finger. Too tight will block blood flow and attenuate the readings.

Step 4: Arduino Software

To get going you will need the GFX and ST7735 Arduino libraries from Adafruit. You can find them here (thanks Limor).

GFX Library

ST7735 Library

Not sure how to install an Arduino library? Then go here for full instructions;

How it works

On start up, the software turns on the IR Led and indicates to the user ranging of the device is required and will commence in 10 seconds. This gives the user time to place their finger in the sensor.

During ranging the software reads the output of the ADC for 10000 times, pausing briefly for 1mS per read. This is used to record the maximum and minimum limits of the raw sampled signal.

These maximum and minimum values are used to determine dynamic trigger points to detect a sharply rising edge in the heart beat to enable timing of the period between pulses and hence calculate the BPM.

The low level trigger introduces hysteresis and is necessary to prevent re-triggering due to high sample rate as show above.

The high level trigger point iPulseTriggerLevelHigh is 90% of the peak (green stars in diagrams above) and the low level trigger point is iPulseTiggerLevelLow is 70% of the peak (blue stars in diagrams above).

The software then enters an infinite loop taking signal samples after an elapsed time dictated by ulElapsedTime, approximately 1mS or 1000Hz. This can be adjusted by varying the #define SAMPLE_PERIOD_uS.

The elapsed time delay is not a blocking call so other background tasks can be carried out if necessary.

Once a sample of the heart beat is read from the ADC the value is scaled to fit the waveform window. This scaling is just a direct 0-5v => 0-1023 => 0-100.

I deliberately omitted auto scaling for the waveform window as I found when I included it, the waveform pretty much fitted the window each time, as you would expect. In doing so you lost a lot of important information, such as when my fingers were cold or badly positioned in the sensor and as a consequence the pulse output level was low. I felt it more useful to keep this information present. Though I did provide a variable fAmplificationFactor (line 171, set to 2.1) that can be used to scale your signal if your electronics and construction skills yield a less sensitive signal than I was able to achieve.

The code then determines if this is a rising edge (see diagrams above for logic). If it is and no falling edge has been detected it stores the time in ulPulseCurrentTime. However if it is and a falling edge has been detected this means the elapsed time is the period between pulses.

The software then calculates the BPM (as in the diagrams above) and adjusts for a wrap in the millis() function call if necessary.

This new pulse rate is sequentially stored in the rolling window buffer array lBPMArray[] and the average calculated across all samples. The new calculated BPM is compared with the old BPM. If there is a difference the display is updated with the new value. Thus reducing TFT update overhead.

During development of the code I noticed that it wasn't possible to optimise a single scan rate of the TFT for BPMs ranging 50...200. So iSampleCountMax is dynamically changed when the pulse rate exceeds 100BPM such that the screen doesn't become too crowded and the shape of the pulse is still clear.

The software then clips the waveform if for some reason it is took big for the display and updates the screen with the latest ADC sample and plots it in the waveform window.

Earlier plotted values are first removed by writing a vertical black line ahead of the plot position 'on the fly'.

Loop repeats.

Rolling Average

Heart rate is calculated by taking a continuously updated rolling average of the periods between pulses. The length of the rolling average can be adjusted by modifying the value of #define MAX_BPM_ARRAY_SIZE. The longer you make it the slower the updates, but the better the approximation (assuming the finger is kept steady in the sensor).

In order to seed the average with an initial value the array lBPMArray[] is pre-loaded with a heart rate of 60BPM at start up.


A copy of the code has been included below.

Step 5: Putting It All Together

The video above shows the HRM in use. For comparison I also have a CMS50E Contec Medical Systems Pulse Oximeter on my index finger. The heart rate on the Oximeter is given in green.

Although this is a commercial product (not life support systems, medical grade) it does show that the reading given by the 'lashed up' device works pretty well and gives a better facsimile of the pulse waveform. I also checked the results with a Polar HRM which concur.

The code, circuit diagrams, construction details etc. are provided free to use in whatever way you see fit (just make a mention of me), though as always it comes unsupported and is used at your own risk.

Happy inventing.


Other items to consider

If you are successful in creating this project the following are a few enhancements you may wish to consider;

  1. Auto zero, adjusting for FG ground drift over time. This can be accomplished by reading the output of TL072 IC2B with T1 switched off and sensor shielded. This capability was originally in the code, hence the DIGITAL 46 Pin control of T1. I took it out after a complete re-write and forgot to put it back in.
  2. Detection of missing finger and suppression of screen output. This can be done by counting calculated edges coming off the sensor. After a given period, say 1 second if no edges are detected, you could try pulsing the IR led and looking for rapid changes in the ADC value. Rapid changes would suggest a missing digit and not a cadaver. :-)


nal106 made it! (author)2017-07-24

Hey Can I have the code? Because I find it difficult to detect the rising edge. Thank u in advance. The one attached is not opening.

SteveQuinn made it! (author)SteveQuinn2017-07-24

Hi nal106,

I just downloaded the code from the instructable it checks out ok.

Have you tried downloading from this site before?



nal106 made it! (author)2017-07-24

Can I have the code?the one that is given can not be opened

RashiK2 made it! (author)2017-07-02

Can you please tell me the Name and ID of the sensor used to sense heart rate ?

BreannaA7 made it! (author)BreannaA72017-07-04

Hey there. He's making a sensor by using and LED and a photo transistor. (Check out the schematic and finger photo in Part 1)

SteveQuinn made it! (author)SteveQuinn2017-07-04

That would be correct, the only thing I could add would be Name : BPW96B, ID : T2 Phototransistor.

MatejJ7 made it! (author)2016-11-25

Hi man, nice work. But I'm wondering why didn't you use AnalogWrite() and a low-pass filter to obtain 2.5V for the fake ground. Any explanations?

SteveQuinn made it! (author)SteveQuinn2016-11-25

Hi MatejJ7,

£4£ a 3v3 zener has a good band gap reference (exhibits good temp stability), is cheap, and the inclusion of IC1A as a buffer amp gives a good low impedance output meaning in combination will provision a cheap and stable(ish) 0v.

Let me know how you get on with a microprocessor driven PWM and a single pole filter. I suspect the S/N will be high.

Good luck with the Uni project.



marcpr made it! (author)2016-08-28

Is it possible to use a pre-made sensor (like Adafruit's Pulse Sensor Amped) and incorporate the display code with it?

SteveQuinn made it! (author)SteveQuinn2016-08-29

Hi marcpr,

The sensor output is conditioned and fed into the Arduino via analogue pin 0, so long as the output from this sensor is an analogue of the heart rate (ie. the signal is continuous in time) and you ensure the signal swing is within the dynamic range of the Arduino it should be ok. You can then do away with the electronics in the 'front end'. Though it kind of dumbs down the challenge of making the device outlined in the instructable.
You will most probably need to tweak the code in the routine named 'void handleCalibration(void)'



jtome made it! (author)2016-08-20

I dont know whether you are still answering questions OP. but I have one, why did you use that array of parallel series parallel resistors for the current supply? thanks.

SteveQuinn made it! (author)SteveQuinn2016-08-21

Hi jtome,

If you mean R5, R6, R18...21 I describe this in the text of the Instructable. Step 3 Signal Conditioning : Description of circuit. Does this help?



maxwellmoojw made it! (author)2016-03-13

Nice project

Ugifer made it! (author)2016-02-01

This is a great project! I've been wanting to try something like this, so many thanks for the 'ible!

Can I ask: Is it necessary to use the Arduino Mega, or would a normal Uno with a ATMega 328 be sufficient? I haven't had a chance to look at the sketch but conceptually, there doesn't seem to be too much analysis involved - read the ADC, write to the screen and compare with threshold values.

Either way, a superb project!


SteveQuinn made it! (author)SteveQuinn2016-02-01

Hi Ugifer, I've taken a quick look at the data sheets for the ATMega2560 and ATMega328, it looks like the ADC block is the same (approx. 13 clock cycles to convert, less S&H etc.). The schematics for the Uno and 2560 look like they use a 16MHz clock. So, tentatively that would be a yes.

In this project there is nothing really time critical but you may have to tweak some of the counters to get the best output. Though the most important bit is the front end. Try to get the best signal off the sensor that you can.

Best of luck.

Naeem RahmanO made it! (author)2016-01-27

Can you give a nice view of circuit diagram without arduino and tft screen?
If I can run it by using function generator(oscilloscope) and 5v power supply?
I wanted to have good look at the clip sensor that how you connected the diodes.
Hope you will help.
If you can then mail me at
Thank you...

SteveQuinn made it! (author)SteveQuinn2016-01-27

Hi Naeem, You're better off constructing your own circuit from the full and comprehensive details I gave in step 3. My 'lashed up' protoboard example also contains a second constant current source which is not required for this project. Besides I've got wires dangling all over the place no doubt coupling to the mains. To answer your question can it be run by an oscilloscope (I'm guessing FnGen is a typo), yes would be the answer, but this was developed as a portable Arduino project. Hence the Arduino and TFT display. Interestingly though, I initially wrote the s/w using a RIGOL DZ1032 Arbitrary Waveform Generator which has a built in heart beat waveform and allowed me to deterministically reproduce a beat signature without having to keep my finger in the sensor. This allowed me to get a good feel for the s/w trigger levels (90%/70%).

If you choose to use a scope as an output device then you can do away with IC2 and couple direct to the o/p of the LPF (R10, C6). Your probe will need to be HiZ at least >= 1MOhm. You can suitably scale the i/p signal by choosing whatever Y-axis amplification works best with your facsimile of the circuit. Though you will need to make your own pulse rate measurements/calculations which will be tricky as the pulse can vary substantially between beats and auto trigger won't work. That's why the rolling average is useful.

The circuit was developed for single rail +5V so it will work fine, however as I say, use a linear supply or better still a battery. If you use switched mode, especially one of the poor quality derivative Chinese brands there will invariably be a lot of noise on the +ve rail which will couple to the sensor supply and degrade the quality of your signal.

As for the sensor clip. This is just trail and error around 0.1" pitches. You can see from the pictures in Step 2 how the device is assembled. The Led and Phototransistor are just soldered directly into the veroboard (paper/phenolic - not the best material, FR4 would be better) and pressed through the plastic clip. I did make sure I removed any residual flux between tracks after soldering. As described in 'Practical' note #2.

Hope the final year project works out. ;-)

Naeem RahmanO made it! (author)Naeem RahmanO2016-01-31

Many many thanks for information brother... :)

DonR25 made it! (author)2016-01-26

Great smooth signal, but couldn't fine the circuit diagram.

SteveQuinn made it! (author)SteveQuinn2016-01-26

Try clicking on 'Show All Items' in step 3.

ThomasK19 made it! (author)2016-01-24

If you don't see a signal, you know you are dead xD

Great i'ible!

SteveQuinn made it! (author)SteveQuinn2016-01-25

Correct, or indeed 'walking dead'. Though I suspect you would need to add a temp sensor to be sure. ;-)

Tecwyn Twmffat made it! (author)2016-01-24

Very cool project !

SteveQuinn made it! (author)SteveQuinn2016-01-24

Thanks for the nod.

Like the cool apiarist related projects.

About This Instructable




More by SteveQuinn:WiFi IoT Temperature and Humidity Sensor. Part : 8 IoT, Home AutomationUsing ESP8266 SPIFFSExpand your Arduino's I/O with an I2C Slave device
Add instructable to: