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.

Teacher Notes

Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.

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. :-)
Arduino All The Things! Contest

Participated in the
Arduino All The Things! Contest

Be the First to Share


    • Made with Math Contest

      Made with Math Contest
    • Multi-Discipline Contest

      Multi-Discipline Contest
    • Robotics Contest

      Robotics Contest

    28 Discussions


    Question 10 months ago on Step 5

    Why your IR has length of 940 nm and your PD has length of 850 nm, why they are not the same ?

    1 answer

    Answer 10 months ago

    The BPW96B ranges 450nm to 1080nm so is pretty well spectrally matched. Also as I mention in step 2 I had intended to calculate Sp02 Max which requires another source of differing wavelength. At the time I settled on green 520nm - 560nm so this sensor would have been adequate for detection of both (with some calibration).


    1 year ago

    I don't know if you're still answering questions but I'd like to know what the output of your signal conditioning circuit is. I have designed one using a TIA, bandpass filter and amplifier which outputs up to around 4.5 V and was wondering if that sort of signal would be compatible with the arduino code in this project, since it is the code for a heart rate monitor I'm looking for.

    2 replies

    Reply 1 year ago

    Hi MichaelR257,

    It's been a while since I did this instructable. However...

    I opted for a photo transistor in the end and not a photo diode/transimpedance combo as I found it gave the best results (I kind of elude to that in Steps 3).

    Regarding your question.

    The ADC of the ATMega2560 has a range of 0-5v so long as you remain inside this common mode voltage you should be ok. ie no -ve voltages applied to the ADC and nothing greater than 5v.

    The expected signal range is +2.5v +/-2.5v ie. an AC signal 5v Pk-to-Pk sat on a DC offset of 2.5v (actually its about 20% less than this to cater for drift, so it doesn't clip the supply rails). This will give maximum swing and make most use of the ADC (maximal quantisation for the 10 bit ADC being used).

    So, to summarise, if your signal is 4.5v peak and goes no lower than 0v. It should work, though you may need to play around with 'fAmplificationFactor' to get it to fit in the window. The trigger levels for timing the pulse are calculated dynamically from the raw ADC values so should be unaffected by any scaling factors.


    Reply 1 year ago

    Thanks for your quick and detailed reply! I'm using a Osram SFH 7050 sensor with the LEDs ready for a blood oxygen saturation upgrade in the future and this has a diode included that's why I've gone that route. Thanks again for the reply


    2 years ago

    Hi, could you please send a better quality photo. Thanks.


    2 years ago

    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.

    1 reply

    Reply 2 years ago

    Hi nal106,

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

    Have you tried downloading from this site before?




    2 years ago

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


    2 years ago

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

    2 replies

    Reply 2 years ago

    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)


    Reply 2 years ago

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


    2 years ago

    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?

    1 reply

    Reply 2 years ago

    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.




    3 years ago

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

    1 reply

    Reply 3 years ago

    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)'




    3 years ago

    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.

    1 reply

    Reply 3 years ago

    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?




    3 years ago

    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!