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.

[source:] -from

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)

heart rate sensor ppg waveform with average and peak

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:
// 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 DLY_IGNORE 130
#define PSEP Serial.print(" ")

int period = 0, cline = 0;
int num_peaks,max_peak,avg_peak;
unsigned long time_was,ctimer;
unsigned long peak_time;
unsigned long raw_tot,raw_num;
unsigned long threshold;

void setup() {

  pinMode(ledPin, OUTPUT); // Inbuilt LED

  Serial.println("Heart Rate Monitor");
  Serial.println("sec update...");

  peak_time = time_was = millis();
  num_peaks = 0;

  threshold = 0;
  raw_tot = raw_num = 0;
  ctimer = millis();

void loop() {
   static int up=1;
   unsigned long ms;
   int period=0;
   int avg = 0; // temp var.
   int rawValue = analogRead (sensorPin);

   raw_tot += rawValue;
   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;


#ifdef opText
   if ((millis()-ctimer)>1000) { // Confidence timer
      if (cline%61==0) Serial.println(); // Line wrap

   // 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 &&
                ) {
      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.print(" sec: ");
         Serial.print(" n: ");
         Serial.print(" peak: ");
         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));

         time_was = ms;
         num_peaks = 0;
         max_peak = 0;
         avg = 0;
         cline = 0; // Don't need newline as output is generated.


      // Indicators


   } else if (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.

ppg ac amplifier circuit

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

  1. Since the supply is 0~5V the input signal must always be above ground.
  2. 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




























Arduino Uno

Piezo Disc

Ceramic Capacitor >6V


Electrolytic capacitor

Light emitting diode

Light detecting diode

Output pin







Operational Amplifier

Microcontroller board



C2 C3














Note: C1 - this must not be an electrolytic, it must be non polarising i.e. does not have a polarity.


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.


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.

Privacy Policy | Contact | About Me

Site Map | Terms of Use