Close Menu
  • Articles
    • Learn Electronics
    • Product Review
    • Tech Articles
  • Electronics Circuits
    • 555 Timer Projects
    • Op-Amp Circuits
    • Power Electronics
  • Microcontrollers
    • Arduino Projects
    • STM32 Projects
    • AMB82-Mini IoT AI Camera
    • BLE Projects
  • IoT Projects
    • ESP8266 Projects
    • ESP32 Projects
    • ESP32 MicroPython
    • ESP32-CAM Projects
    • LoRa/LoRaWAN Projects
  • Raspberry Pi
    • Raspberry Pi Projects
    • Raspberry Pi Pico Projects
    • Raspberry Pi Pico W Projects
  • Electronics Calculator
Facebook X (Twitter) Instagram
  • About Us
  • Disclaimer
  • Privacy Policy
  • Contact Us
  • Advertise With Us
Facebook X (Twitter) Instagram Pinterest YouTube LinkedIn
How To Electronics
  • Articles
    • Learn Electronics
    • Product Review
    • Tech Articles
  • Electronics Circuits
    • 555 Timer Projects
    • Op-Amp Circuits
    • Power Electronics
  • Microcontrollers
    • Arduino Projects
    • STM32 Projects
    • AMB82-Mini IoT AI Camera
    • BLE Projects
  • IoT Projects
    • ESP8266 Projects
    • ESP32 Projects
    • ESP32 MicroPython
    • ESP32-CAM Projects
    • LoRa/LoRaWAN Projects
  • Raspberry Pi
    • Raspberry Pi Projects
    • Raspberry Pi Pico Projects
    • Raspberry Pi Pico W Projects
  • Electronics Calculator
How To Electronics
Home » MAX30102 & Arduino: Heart Rate + Blood Oxygen Monitoring
Arduino Projects

MAX30102 & Arduino: Heart Rate + Blood Oxygen Monitoring

Mamtaz AlamBy Mamtaz AlamUpdated:April 2, 20244 Comments10 Mins Read
Share Facebook Twitter LinkedIn Telegram Reddit WhatsApp
MAX30102 & Arduino Heart Rate + Blood Oxygen Monitoring
Share
Facebook Twitter LinkedIn Pinterest Email Reddit Telegram WhatsApp

Overview

In this project, we will be interfacing MAX30102 Pulse Oximeter Sensor with Arduino to measure Heart Rate (BPM) & Blood Oxygen Concentration (SP02). The Pulse Rate value and Blood Oxygen value will be displayed on 0.96″ OLED Display.

The MAX30102 is an integrated pulse oximetry and heart-rate monitor biosensor module based on PPG (PhotoPlethysmoGraphy). The MAX30102 pulse Oximeter Sensor can be integrated with any microcontroller like Arduino, ESP8266, ESP32, STM32 or Raspberry Pi Pico using the I2C communication protocol. BPM stands for ‘beats per minute,’ which typically range from 65 to 75 during rest for an average person. Athletes may have BPM rates lower than this range. SpO2 represents the oxygen saturation level, which, for a normal individual, is above 95%.

Earlier, we used MAX30100 Pulse Oximeter sensor but MAX30102 is better & précised version of same sensor. This project covers the MAX30102 introduction, pinout, working, connection & applications in detail. At the end, the sensor can be used in real-time application project.


Bill of Materials

Following are the components that we need for this project. The components can be purchased from the given links.

S.N.ComponentsQuantityPurchase Link
1Arduino UNO Board1Amazon | AliExpress | SunFounder
2MAX30102 Pulse Oximeter1Amazon | AliExpress | SunFounder
30.96" SSD1306 I2C OLED Display1Amazon | AliExpress | SunFounder
4Push Button Switch1Amazon | AliExpress | SunFounder
5Jumper Wires10Amazon | AliExpress | SunFounder
6Breadboard1Amazon | AliExpress | SunFounder



MAX30102 Pulse Oximeter Sensor

The MAX30102 is an integrated pulse oximetry and heart-rate monitor module.

MAX30102 Pulse Oximeter Sensor

Not only heart rate, this device can also measure the concentration of oxygen in blood. This is a sensor designed by Analog Devices. The module is available in various board configurations designed by different manufacturers.

It includes internal LEDs, photodetectors, optical elements, and low-noise electronics with ambient light rejection. The MAX30102 provides a complete system solution to ease the design-in process for mobile and wearable devices.

MAX30102 Functional Diagram with Detailed Description

Lets take a look at the design part of the MAX30102 Pulse Oximeter Sensor Module. The sensor is designed for the demanding requirements of wearable devices. The device maintains a very small solution size without sacrificing optical or electrical performance. Here is its block diagram.

The MAX30102 is fully adjustable through software registers, and the digital output data can be stored in a 32-deep FIFO within the IC. The FIFO allows the MAX30102 to be connected to a microcontroller or processor on a shared bus, where the data is not being read continuously from the MAX30102’s registers. It has a fixed I2C address of 0xAE (for write operation) and 0xAF (for read operation). You may refer to MAX30102 Datasheet for more information.



SpO2 Subsystem

The SpO2 subsystem of the MAX30102 contains ambient light cancellation (ALC), a continuous-time sigma-delta ADC, and a proprietary discrete time filter. The ALC has an internal Track/Hold circuit to cancel ambient light and increase the effective dynamic range. The SpO2 ADC has
programmable full-scale ranges from 2µA to 16µA. The ALC can cancel up to 200µA of ambient current. The internal ADC is a continuous time oversampling sigma-delta converter with 18-bit resolution. The ADC sampling rate is 10.24MHz. The ADC output data rate can be programmed from 50sps (samples per second) to 3200sps.

Temperature Sensor

The MAX30102 has an on-chip temperature sensor for calibrating the temperature dependence of the SpO2 subsystem. The temperature sensor has an inherent resolution of 0.0625°C. The device output data is relatively insensitive to the wavelength of the IR LED, where the Red LED’s wavelength is critical to correct interpretation of the data. An SpO2 algorithm used with the MAX30102 output signal can compensate for the associated SpO2 error with ambient temperature changes.

LED Driver

The MAX30102 integrates Red and IR LED drivers to modulate LED pulses for SpO2 and HR measurements. The LED current can be programmed from 0 to 50mA with proper supply voltage. The LED pulse width can be programmed from 69µs to 411µs to allow the algorithm to optimize SpO2 and HR accuracy and power consumption based on use cases.

Technical Specifications

Maximum Current Consumption 6mA
Voltage 3.3-5V
Sample Rate 50Hz – 3200Hz
I2C Address 0xAE (Read) & 0xAF (Write)
Temperature Range  -40°C to +85°C
Temperature Accuracy ±1˚C
ADC Resolution 18 bits
IR LED peak wavelength 880nm
Red LED peak wavelength 660nm



MAX30102 Power Requirements

The MAX30102 IC operates on a single 1.8V power supply but a separate 3.3V power supply is required for the internal LEDs. So the module comes with 3.3V and 1.8V regulators.

The module can be shut down through software with zero standby current, allowing the power rails to remain powered at all times.

MAX30102 Module Pinout

Most of the MAX30102 Module has 5 Pins while some have 7 pins which are explained below:

Pin Description
VIN This pin is used to supply power to the sensor. This sensor is powered on at 3.3-5V.
SCL This is the I2C serial clock pin.
SDA This is the I2C serial data pin.
INT This is the active low interrupt pin. It is pulled HIGH by the onboard resistor but when an interrupt occurs it goes LOW until the interrupt clears.
IRD IR LED Cathode and LED Driver Connection Point
RD Red LED Cathode and LED Driver Connection Point
GND Ground Pin

How MAX30102 Pulse Oximeter Sensor Works?

Let us understand the working of MAX30102 Pulse Oximeter Sensor.

Principle

The MAX30102 works by shining both lights onto the finger or earlobe (or essentially anywhere where the skin isn’t too thick, so both lights can easily penetrate the tissue) and measuring the amount of reflected light using a photodetector. This method of pulse detection through light is called Photoplethysmogram.

The working of MAX30102 can be divided into two parts: Heart Rate Measurement and Pulse Oximetry (measuring the oxygen level of the blood).


Heart Rate Measurement

The oxygenated hemoglobin (HbO2) in the arterial blood has the characteristic of absorbing IR light. The redder the blood (the higher the hemoglobin), the more IR light is absorbed.

Pulse Oximeter Sensor Working

As the blood is pumped through the finger with each heartbeat, the amount of reflected light changes, creating a changing waveform at the output of the photodetector. As you continue to shine light and take photodetector readings, you quickly start to get a heart-beat (HR) pulse reading.

Pulse Oximetry

Pulse oximetry is based on the principle that the amount of RED and IR light absorbed varies depending on the amount of oxygen in your blood.


Interfacing MAX30102 Pulse Oximeter Sensor with Arduino

Let us interface the MAX30102 Pulse Oximeter Sensor with Arduino & find out how we can use it for Heart Rate Blood Oxygen measurement.

Wiring up a MAX30102 Module to an Arduino

Connecting a MAX30102 module to an Arduino board allows you to experiment with monitoring heart rate and blood oxygen levels. Here is the connection diagram.

MAX30102 Arduino Interfacing

Connect the VIN pin & GND Pin of the MAX30102 to the 3.3V & GND pin on your Arduino. Similarly Connect the SCL & SDA pin of the MAX30102 to the A5 & A4 pin on the Arduino Uno respectively.

MAX30102 Arduino

You can use a breadboard and jumper wires for assembly.



Measuring Heart Rate, Blood Oxygen & Temperature with MAX30102 & Arduino

In the coding part of this project, we can use the MAX30102 Arduino library to measure the value of Heart Rate in BPM, Blood Oxygen in percentage and Temperature in degree Celsius. Before that, we need to install the MAX30102 library.

MAX30102 Arduino Library Installation

There are multiple libraries available for MAX30102 Sensor to measure the heart beat, blood oxygen and temperature. Out of all these libraries, the library from Sparkfun Electronics is the most popular one.

To install the library, search by typing MAX3010x. Look for SparkFun MAX3010x Pulse and Proximity Sensor Library. Click on install to install the library.

Example 1: Measuring Heart-Rate (BPM) with MAX30102 & Arduino

This code initializes a MAX30105 sensor on an Arduino to measure heart rate in beats per minute, averages the readings for accuracy, and outputs the data along with the infrared sensor value to the serial monitor, indicating whether a finger is placed on the sensor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Include necessary libraries for communication and for the MAX30105 sensor
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
 
// Create an instance of the MAX30105 class to interact with the sensor
MAX30105 particleSensor;
 
// Define the size of the rates array for averaging BPM; can be adjusted for smoother results
const byte RATE_SIZE = 4; // Increase this for more averaging. 4 is a good starting point.
byte rates[RATE_SIZE]; // Array to store heart rate readings for averaging
byte rateSpot = 0; // Index for inserting the next heart rate reading into the array
long lastBeat = 0; // Timestamp of the last detected beat, used to calculate BPM
 
float beatsPerMinute; // Calculated heart rate in beats per minute
int beatAvg; // Average heart rate after processing multiple readings
 
void setup() {
  Serial.begin(115200); // Start serial communication at 115200 baud rate
  Serial.println("Initializing...");
 
  // Attempt to initialize the MAX30105 sensor. Check for a successful connection and report.
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { // Start communication using fast I2C speed
    Serial.println("MAX30102 was not found. Please check wiring/power. ");
    while (1); // Infinite loop to halt further execution if sensor is not found
  }
  Serial.println("Place your index finger on the sensor with steady pressure.");
 
  particleSensor.setup(); // Configure sensor with default settings for heart rate monitoring
  particleSensor.setPulseAmplitudeRed(0x0A); // Set the red LED pulse amplitude (intensity) to a low value as an indicator
  particleSensor.setPulseAmplitudeGreen(0); // Turn off the green LED as it's not used here
}
 
void loop() {
  long irValue = particleSensor.getIR(); // Read the infrared value from the sensor
 
  if (checkForBeat(irValue) == true) { // Check if a heart beat is detected
    long delta = millis() - lastBeat; // Calculate the time between the current and last beat
    lastBeat = millis(); // Update lastBeat to the current time
 
    beatsPerMinute = 60 / (delta / 1000.0); // Calculate BPM
 
    // Ensure BPM is within a reasonable range before updating the rates array
    if (beatsPerMinute < 255 && beatsPerMinute > 20) {
      rates[rateSpot++] = (byte)beatsPerMinute; // Store this reading in the rates array
      rateSpot %= RATE_SIZE; // Wrap the rateSpot index to keep it within the bounds of the rates array
 
      // Compute the average of stored heart rates to smooth out the BPM
      beatAvg = 0;
      for (byte x = 0 ; x < RATE_SIZE ; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
 
  // Output the current IR value, BPM, and averaged BPM to the serial monitor
  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);
 
  // Check if the sensor reading suggests that no finger is placed on the sensor
  if (irValue < 50000)
    Serial.print(" No finger?");
 
  Serial.println();
}

From the Tools Menu, Select Arduino UNO Board and the COM Port. Click on upload button to upload the code. Once code uploading is done, the MAX30102 Pulse Oximeter Sensor is ready to test.

MAX30102 Heart Rate Measurement

Now keep your finger on the sensor as steady as possible and wait a few seconds for the readings to appear. The Serial monitor will display this reading.

MAX30102 Heart Rate Arduino

For consistent blood flow and reliable sensor data, maintain constant pressure. Applying the optimal pressure on the pulse sensor — not too hard to avoid squeezing out blood and not too light to prevent noise from movement and ambient light.



Example 2: Measuring Blood Oxygen (SpO2) with MAX30102 & Arduino

This code reads heart rate and oxygen levels from a finger using the MAX30105 sensor, then sends this information to a Serial Screen. It dynamically adjusts the data buffer based on the Arduino model to accommodate memory constraints.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include <Wire.h>
#include "MAX30105.h" // Include MAX30105 sensor library
#include "spo2_algorithm.h" // Include SpO2 calculation algorithm
 
MAX30105 particleSensor; // Create an instance of the MAX30105 class
 
#define MAX_BRIGHTNESS 255 // Define maximum LED brightness
 
// Adjust buffer size based on the microcontroller's memory capacity
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
uint16_t irBuffer[100]; // Buffer for IR data (16-bit for memory-limited devices)
uint16_t redBuffer[100]; // Buffer for red LED data
#else
uint32_t irBuffer[100]; // Buffer for IR data (32-bit for devices with more memory)
uint32_t redBuffer[100]; // Buffer for red LED data
#endif
 
int32_t bufferLength = 100; // Length of data buffer
int32_t spo2; // Variable to store calculated SpO2 value
int8_t validSPO2; // Flag indicating if SpO2 calculation is valid
int32_t heartRate; // Variable to store calculated heart rate
int8_t validHeartRate; // Flag indicating if heart rate calculation is valid
 
byte pulseLED = 11; // LED pin for pulse indication (must support PWM)
byte readLED = 13; // LED pin to indicate data read operation
 
void setup() {
  Serial.begin(115200); // Initialize serial communication
  pinMode(pulseLED, OUTPUT); // Set pulseLED as output
  pinMode(readLED, OUTPUT); // Set readLED as output
 
  // Initialize MAX30105 sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println(F("MAX30105 not found. Check wiring/power."));
    while (1); // Halt execution if sensor not found
  }
 
  Serial.println(F("Attach sensor to finger. Press key to start."));
  while (Serial.available() == 0); // Wait for user input to proceed
  Serial.read(); // Clear the serial buffer
 
  // Sensor configuration settings
  byte ledBrightness = 60; // LED brightness (0-255)
  byte sampleAverage = 4; // Averaging (1, 2, 4, 8, 16, 32)
  byte ledMode = 2; // LED mode (1=Red, 2=Red+IR, 3=Red+IR+Green)
  byte sampleRate = 100; // Sampling rate (50, 100, 200, 400, 800, 1000, 1600, 3200)
  int pulseWidth = 411; // Pulse width (69, 118, 215, 411)
  int adcRange = 4096; // ADC range (2048, 4096, 8192, 16384)
 
  // Apply configuration settings to the sensor
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
}
 
void loop() {
  // Collect 100 samples and output raw red and IR data
  for (byte i = 0; i < bufferLength; i++) {
    while (!particleSensor.available()) particleSensor.check(); // Wait for new data
 
    redBuffer[i] = particleSensor.getRed(); // Store red LED data
    irBuffer[i] = particleSensor.getIR(); // Store IR data
    particleSensor.nextSample(); // Move to next sample
 
    Serial.print(F("red="));
    Serial.print(redBuffer[i], DEC);
    Serial.print(F(", ir="));
    Serial.println(irBuffer[i], DEC);
  }
 
  // Calculate heart rate and SpO2 from the first 100 samples
  maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
 
  // Continuously update heart rate and SpO2 values with new samples
  while (1) {
    // Shift the last 75 samples to the beginning and fill the remaining with new data
    for (byte i = 25; i < 100; i++) {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }
 
    // Collect new samples to refill the buffer
    for (byte i = 75; i < 100; i++) {
      while (!particleSensor.available()) particleSensor.check(); // Wait for new data
 
      digitalWrite(readLED, !digitalRead(readLED)); // Blink LED with each data read
 
      redBuffer[i] = particleSensor.getRed(); // Store new red data
      irBuffer[i] = particleSensor.getIR(); // Store new IR data
      particleSensor.nextSample(); // Move to next sample
 
      // Output raw data and calculated heart rate/SpO2 values
      Serial.print(F("red="));
      Serial.print(redBuffer[i], DEC);
      Serial.print(F(", ir="));
      Serial.print(irBuffer[i], DEC);
      Serial.print(F(", HR="));
      Serial.print(heartRate, DEC);
      Serial.print(F(", HRvalid="));
      Serial.print(validHeartRate, DEC);
      Serial.print(F(", SPO2="));
      Serial.print(spo2, DEC);
      Serial.print(F(", SPO2Valid="));
      Serial.println(validSPO2, DEC);
    }
 
    // Recalculate heart rate and SpO2 with the updated buffer
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
  }
}

After uploading the code, put your finger on the MAX30102 Pulse Oximeter Sensor. The Serial Monitor will display the Blood Oxygen reading in percentage.

MAX30102 Blood Oxygen Arduino


Example 3: Measuring Temperature with MAX30102 & Arduino

The following code initializes a MAX30105 sensor to measure temperature, then continuously reads and displays the temperature in both Celsius and Fahrenheit on the serial monitor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <Wire.h>
#include "MAX30105.h" // Include library for MAX30105 sensor
 
MAX30105 particleSensor; // Create an instance of the MAX30105 class
 
void setup() {
  Serial.begin(115200); // Start serial communication at 9600 baud rate
  Serial.println("Initializing...");
 
  // Check if MAX30105 sensor is properly connected
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { // Initialize sensor with fast I2C speed
    Serial.println("MAX30102 was not found. Please check wiring/power. ");
    while (1); // Infinite loop if sensor is not found
  }
 
  particleSensor.setup(0); // Initialize sensor with LEDs turned off to prevent local heating
  particleSensor.enableDIETEMPRDY(); // Enable temperature ready interrupt for reading temperature
}
 
void loop() {
  float temperature = particleSensor.readTemperature(); // Read temperature in Celsius
  Serial.print("temperatureC="); // Print temperature in Celsius
  Serial.print(temperature, 4); // Print with 4 decimal places
 
  float temperatureF = particleSensor.readTemperatureF(); // Convert temperature to Fahrenheit
  Serial.print(" temperatureF="); // Print temperature in Fahrenheit
  Serial.print(temperatureF, 4); // Print with 4 decimal places
 
  Serial.println(); // New line for readability
}

Open the Serial Monitor after uploading the code. The Serial Monitor will show the value of Temperature in both Celcius and Fahrenheit Scale.

MAX30102 Temperature Arduino



Making a Portable Pulse Oximeter with OLED Display

Let us make a portable Pulse Oximeter by interfacing OLED Display & MAX30102 with Arduino Nano. The OLED will display the real-time Heart Rate and Oxygen Saturation value.

Circuit Diagram & Connection

In the hardware part, we will use 0.96″ I2C OLED Display with SSD1306 Driver. We will also use a push button switch here to control the display part. Here is the simple connection diagram.

Portable Pulse Oximeter Arduino MAX30102 OLED

Connect the SSD1306 OLED Display to I2C Pins of Arduino Nano. Also connect the Push button switch to digital pin 3 of the Arduino and its other terminal to the GND.

MAX30102 Arduino OLED Display

While making connection on a breadboard, you need a lot of wiring. Therefore if you can make a custom PCB to avoid wiring, it would be better.

Source Code/Program

Let us see the programming part of the portable pulse oximeter. The following code integrates an OLED display and a MAX30102 sensor with an Arduino to measure and display heart rate and blood oxygen saturation (SpO2) levels.

It includes functionality to filter signal noise for more accurate readings, dynamically adjust the display based on sensor data, and enter a low-power mode when the sensor is not in use. The program also offers user interaction through a button to change display modes and uses EEPROM for saving user preferences.

The code requires a custom made library. The library contains all the header files for OLED Display and MAX30102 Sensor. First download the MAX30102 & OLED Library and add it to your Arduino Library folder.



Copy the following code and paste it on your Arduino IDE. Then finally you can upload the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#include "ssd1306h.h"
#include "MAX30102.h"
#include "Pulse.h"
#include <avr/pgmspace.h>
#include <EEPROM.h>
#include <avr/sleep.h>
 
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
 
 
SSD1306 oled;
MAX30102 sensor;
Pulse pulseIR;
Pulse pulseRed;
MAFilter bpm;
 
#define LED LED_BUILTIN
#define BUTTON 3
#define OPTIONS 7
 
static const uint8_t heart_bits[] PROGMEM = { 0x00, 0x00, 0x38, 0x38, 0x7c, 0x7c, 0xfe, 0xfe, 0xfe, 0xff,
                                              0xfe, 0xff, 0xfc, 0x7f, 0xf8, 0x3f, 0xf0, 0x1f, 0xe0, 0x0f,
                                              0xc0, 0x07, 0x80, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
                                              0x00, 0x00
                                            };
 
//spo2_table is approximated as  -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ;
const uint8_t spo2_table[184] PROGMEM =
{ 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99,
  99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
  100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97,
  97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91,
  90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81,
  80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67,
  66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50,
  49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29,
  28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5,
  3, 2, 1
} ;
 
 
int getVCC() {
  //reads internal 1V1 reference against VCC
#if defined(__AVR_ATmega1284P__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);  // For ATmega1284
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);  // For ATmega328
#endif
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA, ADSC));
  uint8_t low = ADCL;
  unsigned int val = (ADCH << 8) | low;
  //discard previous result
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA, ADSC));
  low = ADCL;
  val = (ADCH << 8) | low;
 
  return (((long)1024 * 1100) / val) / 100;
}
 
void print_digit(int x, int y, long val, char c = ' ', uint8_t field = 3, const int BIG = 2)
{
  uint8_t ff = field;
  do {
    char ch = (val != 0) ? val % 10 + '0' : c;
    oled.drawChar( x + BIG * (ff - 1) * 6, y, ch, BIG);
    val = val / 10;
    --ff;
  } while (ff > 0);
}
 
 
/*
     Record, scale  and display PPG Wavefoem
*/
const uint8_t MAXWAVE = 72;
 
class Waveform {
  public:
    Waveform(void) {
      wavep = 0;
    }
 
    void record(int waveval) {
      waveval = waveval / 8;       // scale to fit in byte  缩放以适合字节
      waveval += 128;              //shift so entired waveform is +ve
      waveval = waveval < 0 ? 0 : waveval;
      waveform[wavep] = (uint8_t) (waveval > 255) ? 255 : waveval;
      wavep = (wavep + 1) % MAXWAVE;
    }
 
    void scale() {
      uint8_t maxw = 0;
      uint8_t minw = 255;
      for (int i = 0; i < MAXWAVE; i++) {
        maxw = waveform[i] > maxw ? waveform[i] : maxw;
        minw = waveform[i] < minw ? waveform[i] : minw;
      }
      uint8_t scale8 = (maxw - minw) / 4 + 1; //scale * 8 to preserve precision
      uint8_t index = wavep;
      for (int i = 0; i < MAXWAVE; i++) {
        disp_wave[i] = 31 - ((uint16_t)(waveform[index] - minw) * 8) / scale8;
        index = (index + 1) % MAXWAVE;
      }
    }
 
    void draw(uint8_t X) {
      for (int i = 0; i < MAXWAVE; i++) {
        uint8_t y = disp_wave[i];
        oled.drawPixel(X + i, y);
        if (i < MAXWAVE - 1) {
          uint8_t nexty = disp_wave[i + 1];
          if (nexty > y) {
            for (uint8_t iy = y + 1; iy < nexty; ++iy)
              oled.drawPixel(X + i, iy);
          }
          else if (nexty < y) {
            for (uint8_t iy = nexty + 1; iy < y; ++iy)
              oled.drawPixel(X + i, iy);
          }
        }
      }
    }
 
  private:
    uint8_t waveform[MAXWAVE];
    uint8_t disp_wave[MAXWAVE];
    uint8_t wavep = 0;
 
} wave;
 
int  beatAvg;
int  SPO2, SPO2f;
int  voltage;
bool filter_for_graph = false;
bool draw_Red = false;
uint8_t pcflag = 0;
uint8_t istate = 0;
uint8_t sleep_counter = 0;
 
void button(void) {
  pcflag = 1;
}
 
void checkbutton() {
  if (pcflag && !digitalRead(BUTTON)) {
    istate = (istate + 1) % 4;
    filter_for_graph = istate & 0x01;
    draw_Red = istate & 0x02;
    EEPROM.write(OPTIONS, filter_for_graph);
    EEPROM.write(OPTIONS + 1, draw_Red);
  }
  pcflag = 0;
}
 
 
 
void Display_5() {
  if (pcflag && !digitalRead(BUTTON)) {
    draw_oled(5);
    delay(1100);
  }
  pcflag = 0;
 
 
}
 
void go_sleep() {
  oled.fill(0);
  oled.off();
  delay(10);
  sensor.off();
  delay(10);
  cbi(ADCSRA, ADEN);  // disable adc
  delay(10);
  pinMode(0, INPUT);
  pinMode(2, INPUT);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_mode();  // sleep until button press
  // cause reset
  setup();
}
 
void draw_oled(int msg) {
  oled.firstPage();
  do {
    switch (msg) {
      case 0:  oled.drawStr(10, 0, F("Device error"), 1);
        break;
      case 1:  oled.drawStr(0, 0, F("PLACE YOUR"), 2);
        oled.drawStr(25, 18, F("FINGER"), 2);
 
 
        break;
      case 2:  print_digit(86, 0, beatAvg);
        oled.drawStr(0, 3, F("PULSE RATE"), 1);
        oled.drawStr(11, 17, F("OXYGEN"), 1);
        oled.drawStr(0, 25, F("SATURATION"), 1);
        print_digit(73, 16, SPO2f, ' ', 3, 2);
        oled.drawChar(116, 16, '%', 2);
 
        break;
      case 3:  oled.drawStr(33, 0, F("Pulse"), 2);
        oled.drawStr(17, 15, F("Oximeter"), 2);
 
        //oled.drawXBMP(6,8,16,16,heart_bits);
 
        break;
      case 4:  oled.drawStr(28, 12, F("OFF IN"), 1);
        oled.drawChar(76, 12, 10 - sleep_counter / 10 + '0');
        oled.drawChar(82, 12, 's');
        break;
      case 5:  oled.drawStr(0, 0, F("Avg Pulse"), 1);
        print_digit(75, 0, beatAvg);
        oled.drawStr(0, 15, F("AVG OXYGEN"), 1);
        oled.drawStr(0, 22, F("saturation"), 1);
        print_digit(75, 15, SPO2);
 
        break;
    }
  } while (oled.nextPage());
}
 
void setup(void) {
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  filter_for_graph = EEPROM.read(OPTIONS);
  draw_Red = EEPROM.read(OPTIONS + 1);
  oled.init();
  oled.fill(0x00);
  draw_oled(3);
  delay(3000);
  if (!sensor.begin())  {
    draw_oled(0);
    while (1);
  }
  sensor.setup();
  attachInterrupt(digitalPinToInterrupt(BUTTON), button, CHANGE);
}
 
long lastBeat = 0;    //Time of the last beat
long displaytime = 0; //Time of the last display update
bool led_on = false;
 
 
void loop()  {
  sensor.check();
  long now = millis();   //start time of this cycle
  if (!sensor.available()) return;
  uint32_t irValue = sensor.getIR();
  uint32_t redValue = sensor.getRed();
  sensor.nextSample();
  if (irValue < 5000) {
    voltage = getVCC();
    checkbutton();
    draw_oled(sleep_counter <= 50 ? 1 : 4); // finger not down message
    //? : 是三元运算符,整个表达式根据条件返回不同的值,如果x>y为真则返回x,如果为假则返回y,之后=赋值给z。相当于:if(x>y)z=x;elsez=y
    delay(200);
    ++sleep_counter;
    if (sleep_counter > 100) {
      go_sleep();
      sleep_counter = 0;
    }
  } else {
    sleep_counter = 0;
    // remove DC element移除直流元件
    int16_t IR_signal, Red_signal;
    bool beatRed, beatIR;
    if (!filter_for_graph) {//图形过滤器
      IR_signal =  pulseIR.dc_filter(irValue) ;
      Red_signal = pulseRed.dc_filter(redValue);
      beatRed = pulseRed.isBeat(pulseRed.ma_filter(Red_signal));
      beatIR =  pulseIR.isBeat(pulseIR.ma_filter(IR_signal));
    } else {
      IR_signal =  pulseIR.ma_filter(pulseIR.dc_filter(irValue)) ;
      Red_signal = pulseRed.ma_filter(pulseRed.dc_filter(redValue));
      beatRed = pulseRed.isBeat(Red_signal);
      beatIR =  pulseIR.isBeat(IR_signal);
    }
    // invert waveform to get classical BP waveshape
    wave.record(draw_Red ? -Red_signal : -IR_signal );
    // check IR or Red for heartbeat
    if (draw_Red ? beatRed : beatIR) {
      long btpm = 60000 / (now - lastBeat);
      if (btpm > 0 && btpm < 200) beatAvg = bpm.filter((int16_t)btpm);
      lastBeat = now;
      digitalWrite(LED, HIGH);
      led_on = true;
      // compute SpO2 ratio
      long numerator   = (pulseRed.avgAC() * pulseIR.avgDC()) / 256;
      long denominator = (pulseRed.avgDC() * pulseIR.avgAC()) / 256;
      int RX100 = (denominator > 0) ? (numerator * 100) / denominator : 999;
      // using formula
      SPO2f = (10400 - RX100 * 17 + 50) / 100;
      // from table
      if ((RX100 >= 0) && (RX100 < 184))
        SPO2 = pgm_read_byte_near(&spo2_table[RX100]);
    }
    // update display every 50 ms if fingerdown
    if (now - displaytime > 50) {
      displaytime = now;
      wave.scale();
      draw_oled(2);
 
    }
    Display_5();
 
 
  }
  // flash led for 25 ms
  if (led_on && (now - lastBeat) > 25) {
    digitalWrite(LED, LOW);
    led_on = false;
  }
}

Testing the Portable Pulse Oximeter Device

After uploading the code, the device is ready for testing. Immediately the OLED will display the “Pulse Oximeter” text.

Then it will display the message to place your finger.



When you place the finger, it will start measuring the Pulse Rate in BPM and Oxygen Saturation in percentage.

If you press the push button while placing the finger on MAX30102, the OLED will display the Average Pulse and Average Oxygen Saturation.

When no finger is placed on MAX30102 Sensor, the device goes to sleep mode and OLED will turn off in 5 seconds. The OLED will display the countdown timer as well.

You can bring the OLED to working mode after pressing the push button again. This will wake up the device from sleep mode.

Thus, this is how you can make a portable pulse Oximeter using the Arduino, MAX30102 Sensor & display the Blood Oxygen and Heart Rate in OLED Display.

Share. Facebook Twitter Pinterest LinkedIn Tumblr Email Reddit Telegram WhatsApp
Previous ArticleIoT Home Automation using ESP32 WebServer with PCB
Next Article Stopwatch using Arduino & LCD with Start, Stop & Reset Button

Related Posts

DC Energy Meter using Arduino

Build a DC Energy Meter using Arduino – 32V/5A

Updated:August 26, 20252K
Interfacing ADXL375 Accelerometer with Arduino

Interfacing ADXL375 Accelerometer with Arduino (±200g)

Updated:June 28, 2025
PZEM-004T Arduino Energy Meter

DIY AC Energy Meter using PZEM-004T & Arduino

Updated:March 6, 20258K
Interfacing BMI160 Accelerometer & Gyroscope with Arduino

Interfacing BMI160 Accelerometer & Gyroscope with Arduino

Updated:February 2, 20259K
Password Based Door Lock Security System Using Arduino & Keypad

Password Based Door Lock Security System Using Arduino & Keypad

Updated:February 2, 20252436K
Earthquake Detector Alarm with with Accelerometer & Arduino

Earthquake Detector Alarm with Accelerometer & Arduino

Updated:February 2, 2025661K
View 4 Comments

4 Comments

  1. TDHofstetter on March 19, 2024 8:04 PM

    Why do you constantly mix references to the MAX30102 and the MAX30105 within your text here?

    Reply
  2. Matcha Town on October 27, 2024 5:07 AM

    make code for ESP32 please

    Reply
  3. qqevey on July 31, 2025 1:44 PM

    Is there a way to change the beat average?

    Reply
  4. Arthur on January 24, 2026 5:45 PM

    Why doesn’t the monitor work and the sensor itself doesn’t light up? Only the Arduino board itself lights up? Although everything is fine with the code(

    Reply

CommentsCancel reply

Latest Posts
ESP32 Fingerprint Attendance System with Live Web Dashboard

ESP32 Fingerprint Attendance System with Live Web Dashboard

June 16, 2026
IoT Based PM & Air Quality Monitoring System using ESP32

IoT Based PM & Air Quality Monitoring System using ESP32

June 14, 2026
DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

May 10, 2026
IoT Activity Tracker with ESP32 & Accelerometer Gyroscope

IoT Activity Tracker with ESP32 & Accelerometer/Gyroscope

May 2, 2026
A Guide to Sourcing Obsolete ICs for Vintage Projects

Beyond AliExpress: A Guide to Sourcing Obsolete ICs for Vintage Projects

April 21, 2026

ESP32 IoT Vehicle Motion Analyzer with MPU6050 & LIS3MDL

April 27, 2026
Building a Smart Sensor Node with a BLE Microcontroller

Building a Smart Sensor Node with a BLE Microcontroller

February 26, 2026
High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

April 27, 2026
Top Posts & Pages
  • IoT Based PM & Air Quality Monitoring System using ESP32
    IoT Based PM & Air Quality Monitoring System using ESP32
  • 12V DC to 220V AC Inverter Circuit & PCB
    12V DC to 220V AC Inverter Circuit & PCB
  • Buck Converter: Basics, Working, Design & Application
    Buck Converter: Basics, Working, Design & Application
  • IoT AC Energy Meter with PZEM-004T & ESP32 WebServer
    IoT AC Energy Meter with PZEM-004T & ESP32 WebServer
  • How to use Modbus RTU with ESP32 to read Sensor Data
    How to use Modbus RTU with ESP32 to read Sensor Data
  • ESP32 CAN Bus Tutorial | Interfacing MCP2515 CAN Module with ESP32
    ESP32 CAN Bus Tutorial | Interfacing MCP2515 CAN Module with ESP32
  • RS-485 Simplex Communication with Arduino & MAX485
    RS-485 Simplex Communication with Arduino & MAX485
  • Designing of MPPT Solar Charge Controller using Arduino
    Designing of MPPT Solar Charge Controller using Arduino
Categories
  • Arduino Projects (197)
  • Articles (60)
    • Learn Electronics (19)
    • Product Review (15)
    • Tech Articles (28)
  • Electronics Circuits (46)
    • 555 Timer Projects (21)
    • Op-Amp Circuits (7)
    • Power Electronics (13)
  • IoT Projects (205)
    • ESP32 MicroPython (7)
    • ESP32 Projects (82)
    • ESP32-CAM Projects (15)
    • ESP8266 Projects (76)
    • LoRa/LoRaWAN Projects (22)
  • Microcontrollers (38)
    • AMB82-Mini IoT AI Camera (4)
    • BLE Projects (18)
    • STM32 Projects (19)
  • Raspberry Pi (93)
    • Raspberry Pi Pico Projects (57)
    • Raspberry Pi Pico W Projects (12)
    • Raspberry Pi Projects (24)
Follow Us
  • Facebook
  • Twitter
  • Pinterest
  • Instagram
  • YouTube
About Us

“‘How to Electronics’ is a vibrant community for electronics enthusiasts and professionals. We deliver latest insights in areas such as Embedded Systems, Power Electronics, AI, IoT, and Robotics. Our goal is to stimulate innovation and provide practical solutions for students, organizations, and industries. Join us to transform learning into a joyful journey of discovery and innovation.

Copyright © How To Electronics. All rights reserved.
  • About Us
  • Disclaimer
  • Privacy Policy
  • Contact Us
  • Advertise With Us

Type above and press Enter to search. Press Esc to cancel.

Ad Blocker Enabled!
Ad Blocker Enabled!
Looks like you're using an ad blocker. Please allow ads on our site. We rely on advertising to help fund our site.