The MAX30102: Tiny Sensor, Big Potential
- Hunter Treleaven

- Oct 19, 2025
- 5 min read
Table of Contents
Small Tech, Big Impact
The MAX30102 is one of the smallest yet most capable sensors used in biomedical prototyping — including my ongoing CardioTrack project. It measures heart rate and blood oxygen levels (SpO₂) using just light, making it ideal for low-cost, portable health-tech experiments.
How It Works
This sensor uses two LEDs — one red and one infrared — that shine through the skin. The light that reflects back changes with every heartbeat as oxygen-rich and oxygen-poor blood absorb light differently.
By comparing how much red and infrared light returns, the MAX30102 can calculate both heart rate and oxygen saturation in real time.
It’s the same core principle used in pulse oximeters — just scaled down for makers, researchers, and innovators.
Built for Precision and Efficiency
Despite its size, the MAX30102 is smartly engineered:
FIFO Buffer – Stores up to 32 samples, so no data is lost when the microcontroller is busy.
Interrupt Alerts – Notifies the MCU when new data is ready or memory is nearly full.
Temperature Compensation – Adjusts readings automatically for changing environments.
Low Power Use – Runs efficiently between 3.3–5V, drawing less than 1 mA.
These features make it powerful enough for advanced applications but efficient enough for wearables or battery-based systems.
Easy Integration
The sensor connects via I²C — just two wires for data and clock — making it compatible with microcontrollers like the ESP32, Arduino, or Raspberry Pi.
Developers can skip complex setup by using the SparkFun MAX3010x Library, which handles all data communication behind the scenes.
Why It Matters
The MAX30102 proves how much can be done with small, affordable components.
Within CardioTrack, it’s a key example of how microcontrollers and biomedical sensors can work together to collect, visualize, and timestamp vital signs — bridging the gap between education and innovation in healthcare tech.
Learn More
Measuring Heart Rate (Code)
/*
Heart Rate Monitor using MAX30102 Optical Sensor
------------------------------------------------------------
Description:
This program uses the MAX30102 pulse oximeter and heart rate sensor
to detect heartbeats and calculate the user's beats per minute (BPM).
It averages recent BPM readings for smoother output.
Hardware:
- MAX30102 connected via I2C (SDA, SCL)
- Uses onboard IR LED for pulse detection
Libraries Required:
- Wire.h (for I2C communication)
- MAX30105.h (SparkFun MAX3010x sensor library)
- heartRate.h (for the checkForBeat() algorithm)
Project: CardioTrack
-----------------------------------------------------------
*/
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
// Create sensor object
MAX30105 particleSensor;
// Heart Rate Data Variables
const byte RATE_SIZE = 4; // Number of samples to average (higher = smoother, slower)
byte rates[RATE_SIZE]; // Stores recent BPM readings
byte rateSpot = 0; // Index pointer for the circular buffer
long lastBeat = 0; // Timestamp (ms) of last detected heartbeat
float beatsPerMinute; // Instantaneous BPM value
int beatAvg; // Rolling average of recent BPM readings
// SETUP FUNCTION
void setup() {
Serial.begin(9600);
Serial.println("Initializing...");
// Initialize the MAX30102 sensor via I2C
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("MAX30102 not found. Check wiring or power connections.");
while (1); // Stop execution if sensor not detected
}
Serial.println("Place your index finger on the sensor with steady pressure.");
// Configure sensor LED and sampling settings
particleSensor.setup(); // Load default configuration
particleSensor.setPulseAmplitudeRed(0x0A); // Dim red LED to show sensor is active
particleSensor.setPulseAmplitudeGreen(0x00); // Disable green LED (not needed for HR)
}
void loop() {
// Read the infrared (IR) value from the sensor
long irValue = particleSensor.getIR();
// Heartbeat detection logic
if (checkForBeat(irValue) == true) {
// A heartbeat was detected!
long delta = millis() - lastBeat; // Time since last beat (ms)
lastBeat = millis();
// Convert delta time into BPM
beatsPerMinute = 60 / (delta / 1000.0);
// Filter out unrealistic values
if (beatsPerMinute < 255 && beatsPerMinute > 20) {
// Store reading in the circular buffer
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE; // Wrap index when reaching end of buffer
// Calculate rolling average BPM
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++)
beatAvg += rates[x];
beatAvg /= RATE_SIZE;
}
}
// Serial Output (for monitoring via Serial Plotter/Monitor)
Serial.print("IR=");
Serial.print(irValue);
Serial.print(", BPM=");
Serial.print(beatsPerMinute);
Serial.print(", Avg BPM=");
Serial.print(beatAvg);
// If IR signal is low, assume no finger is on the sensor
if (irValue < 50000)
Serial.print(" No finger?");
Serial.println();
}
Measuring Blood Oxygen Levels (C0de)
/*
MAX30102 SpO₂ and Heart Rate Monitoring
----------------------------------------------------------------
Description:
This sketch interfaces with the MAX30102 optical sensor to measure
heart rate (in BPM) and blood oxygen saturation (SpO₂).
It continuously samples infrared (IR) and red LED signals, applies
a signal-processing algorithm, and outputs results via Serial Monitor.
Hardware:
- MAX30102 connected via I2C (SDA, SCL)
- Uses onboard IR LED for pulse detection
Libraries Required:
- Wire.h (for I2C communication)
- MAX30105.h (SparkFun MAX3010x sensor library)
- spo2_algorithm.h (for SpO₂ & heart rate computation)
Project: CardioTrack
---------------------------------------------------------------
Notes:
- Place your finger firmly but gently on the sensor.
- Do not move during measurement to reduce noise.
- Works on most Arduino-compatible boards (ESP32, Mega, Uno).
- UNO has memory limitations; this code adapts accordingly.
*/
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"
// Global Sensor Object
MAX30105 particleSensor;
#define MAX_BRIGHTNESS 255 // LED brightness limit
// Data Buffers for Sensor Readings
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
uint16_t irBuffer[100]; // Infrared LED data
uint16_t redBuffer[100]; // Red LED data
uint32_t irBuffer[100]; // Infrared LED data
uint32_t redBuffer[100]; // Red LED data
// Computation Variables
int32_t bufferLength; // Number of samples to process
int32_t spo2; // Calculated SpO₂ percentage
int8_t validSPO2; // Validity flag (1 = valid, 0 = invalid)
int32_t heartRate; // Calculated heart rate (BPM)
int8_t validHeartRate; // Validity flag (1 = valid, 0 = invalid)
// LED Indicators
byte pulseLED = 11; // PWM LED for pulse feedback
byte readLED = 13; // Blinks with each new data read
void setup() {
Serial.begin(9600);
Serial.println("Initializing MAX30102 sensor...");
pinMode(pulseLED, OUTPUT);
pinMode(readLED, OUTPUT);
// Initialize the sensor
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println(F("MAX30105 was not found. Check wiring/power."));
while (1); // Halt program
}
Serial.println(F("Attach finger to sensor using gentle pressure."));
Serial.println(F("Press any key to begin data acquisition."));
while (Serial.available() == 0); // Wait for user input
Serial.read(); // Clear input buffer
// Sensor Configuration
byte ledBrightness = 60; // 0 = Off → 255 = 50mA
byte sampleAverage = 4; // Number of samples to average
byte ledMode = 2; // 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 100; // Sampling frequency (Hz)
int pulseWidth = 411; // Pulse width in µs (higher = more light)
int adcRange = 4096; // ADC range in nA (2048–16384)
// Apply configuration
particleSensor.setup(ledBrightness, sampleAverage, ledMode,
sampleRate, pulseWidth, adcRange);
Serial.println(F("Sensor setup complete. Collecting data..."));
}
void loop() {
bufferLength = 100; // 100 samples ≈ 4 seconds at 25 samples/sec
// Initial Sampling Phase
for (byte i = 0; i < bufferLength; i++) {
while (particleSensor.available() == false)
particleSensor.check(); // Wait for new sample
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample(); // Move to next sample slot
// Display raw readings
Serial.print(F("red="));
Serial.print(redBuffer[i], DEC);
Serial.print(F(", ir="));
Serial.println(irBuffer[i], DEC);
}
// Compute baseline SpO₂ and heart rate
maxim_heart_rate_and_oxygen_saturation(
irBuffer, bufferLength,
redBuffer, &spo2, &validSPO2,
&heartRate, &validHeartRate);
// Continuous Monitoring Loop
while (1) {
// Shift last 75 samples up, discard oldest 25
for (byte i = 25; i < 100; i++) {
redBuffer[i - 25] = redBuffer[i];
irBuffer[i - 25] = irBuffer[i];
}
// Capture 25 new samples
for (byte i = 75; i < 100; i++) {
while (particleSensor.available() == false)
particleSensor.check();
digitalWrite(readLED, !digitalRead(readLED)); // Blink read LED
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample();
// Output current sample and prior computed 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 HR and SpO₂ using latest 100-sample window
maxim_heart_rate_and_oxygen_saturation(
irBuffer, bufferLength,
redBuffer, &spo2, &validSPO2,
&heartRate, &validHeartRate);
}
}



Comments