Skip to content
Tutorial
13 min read
October 17, 2026

MAX30102 Heart Rate & SpO2 Sensor with ESP32 and Arduino Uno

Wire the MAX30102 pulse oximeter to an ESP32 or Arduino Uno, detect heartbeats, read SpO2, plot the raw PPG waveform, and understand why the readings wobble. Four tested-logic sketches included.

What You'll Build

A fingertip pulse monitor: you rest a finger on a tiny MAX30102 module and your ESP32 or Arduino Uno prints your heart rate in beats per minute and your blood oxygen saturation (SpO2) over the Serial Monitor. Along the way you will also stream the raw optical waveform to the Serial Plotter, so you can actually see your pulse.

You will learn:

  • How a pulse oximeter turns red and infrared light into a heart rate and an oxygen percentage
  • How to wire the MAX30102 to an ESP32 and to an Arduino Uno (and the voltage trap to avoid)
  • How to confirm the sensor with an I2C scan, then read beats with checkForBeat()
  • How to compute SpO2 with the Maxim algorithm, including the RAM trick that makes it fit on an Uno
  • Why readings jump around and how to get steadier ones
DifficultyBeginner to Intermediate
Build timeAbout 30 minutes
BoardsESP32 DevKit or Arduino Uno
InterfaceI2C (address 0x57)

Not a medical device. The MAX30102 is a hobby-grade sensor. Use this project to learn, never to make health decisions. See the accuracy section below.

The finished build: a MAX30102 module wired to an ESP32 (and the same module on an Arduino Uno).

How a Pulse Oximeter Works

The MAX30102 contains two LEDs and one photodiode, all facing the same direction. One LED is red (about 660 nm) and the other is infrared (about 880 nm). When your fingertip covers the module, light from the LEDs travels through the tissue and a fraction is reflected back to the photodiode. This technique is called photoplethysmography (PPG).

Two things make the measurement possible:

1. Your pulse changes how much light comes back. Each heartbeat pushes a wave of blood into the capillaries of your finger. More blood means more absorption and less reflected light, so the photodiode signal rises and falls in time with your heart. Count the peaks and you have your heart rate.

2. Oxygenated and deoxygenated blood absorb red and infrared differently. Oxygen-rich hemoglobin (HbO2) absorbs less red light and more infrared than oxygen-poor hemoglobin (Hb). By comparing the two wavelengths you can estimate what fraction of your hemoglobin is carrying oxygen.

The comparison is done with a ratio of ratios. For each wavelength, take the pulsing part of the signal (AC) divided by the steady part (DC), then divide red by infrared:

R = (AC_red / DC_red) / (AC_ir / DC_ir)

A popular rule of thumb maps R to saturation as roughly SpO2 ≈ 110 - 25 x R. Real devices, including the Maxim algorithm used below, use an empirical curve fitted against calibrated reference measurements, but the idea is identical: a lower R means more oxygen.

Reflective PPG: red and IR LEDs shine into the fingertip, the photodiode reads what bounces back, and the pulsing blood volume modulates the signal.
Absorption of oxygenated (HbO2) and deoxygenated (Hb) hemoglobin versus wavelength, with the 660 nm and 880 nm LEDs marked.

MAX30102 at a Glance

SpecValue
ChipAnalog Devices (Maxim) MAX30102
MeasuresHeart rate (pulse) and SpO2
LEDsRed 660 nm, infrared 880 nm
ADC resolutionUp to 18 bits
Sample rate50 to 3200 samples per second (selectable)
LED currentUp to about 50 mA (programmable)
InterfaceI2C, address 0x57, up to 400 kHz
Supply (chip)1.8 V core, 3.3 V LED supply
Supply (module VIN)Usually 3.3 V (check your board)
ExtrasOn-die temperature sensor, 32-sample FIFO, interrupt pin

The older MAX30100 is a different chip with a different address and register set. This article is for the MAX30102 (and the near-identical MAX30105, which adds a green LED). The SparkFun library below supports all of these.

Components

5 items
NameQtyLink
MAX30102 breakout module (GY-MAX30102 or similar)×1—
ESP32 DevKit (or Arduino Uno R3)×1—
Jumper wires (female-to-female or male-to-female)×4—
Breadboard (optional)×1—
USB cable for the board×1—

Module Pinout

Most modules break out seven pins. You only need four.

PinFunctionConnect to
VINPower input3.3 V
GNDGroundGND
SCLI2C clockSCL pin of your board
SDAI2C dataSDA pin of your board
INTInterrupt (active low)Not needed for this article
IRDIR LED cathodeLeave unconnected
RDRed LED cathodeLeave unconnected

IRD and RD give access to the LED cathodes for custom drive circuits. Wiring them to anything will break the sensor's LED control, so leave them floating.

Wiring

ESP32 DevKit

MAX30102ESP32
VIN3V3
GNDGND
SDAGPIO 21
SCLGPIO 22

Arduino Uno

MAX30102Uno
VIN3.3V
GNDGND
SDAA4
SCLA5

On newer Uno R3 boards SDA and SCL are also broken out next to AREF. They are the same signals as A4 and A5.

Wiring diagram: MAX30102 to ESP32 (SDA 21, SCL 22) and to Arduino Uno (A4, A5), both powered from 3.3 V.

⚠️ Warning

Power the module from **3.3 V**, not 5 V. Many breakouts tie the I2C pull-up resistors to the same rail as VIN, so powering from 5 V would push 5 V into the sensor chip's 3.3 V-tolerant pins. The Uno's I2C lines swing to 5 V, which the module's 3.3 V pull-ups do not prevent. It commonly works, but for a permanent build on an Uno, put a bidirectional I2C level shifter between the two. The ESP32 is natively 3.3 V and needs nothing extra.

Steps

  1. 1Wire the four connections from the tables above with the board unpowered.
  2. 2In the Arduino IDE open Library Manager and install **SparkFun MAX3010x Pulse and Proximity Sensor Library**.
  3. 3Select your board (ESP32 Dev Module or Arduino Uno) and the correct port.
  4. 4Upload the I2C scanner below and confirm the sensor appears at 0x57.
  5. 5Upload the heart rate sketch, place a fingertip on the sensor, and watch the Serial Monitor at 115200 baud.

Step 1: Confirm the Sensor with an I2C Scan

Before blaming code, make sure the board can see the sensor. This sketch tries every I2C address and prints the ones that answer. You should see exactly one device, at 0x57.

C++

If you see "No I2C devices found", recheck SDA and SCL (they are easy to swap) and the 3.3 V and GND connections. If you see a different address, you probably have a different sensor on the bus. For more on how I2C addressing works, read I2C vs SPI: Differences and When to Use Each.

Step 2: Detect Heartbeats and Read BPM

This sketch uses the library's checkForBeat() helper, which watches the infrared signal for the characteristic rising-then-falling shape of a pulse. Each time it fires, we measure the time since the previous beat, convert that to BPM, and keep a rolling average of the last four beats so one noisy beat does not make the display jump.

A few details worth understanding:

  • Finger detection: when no finger is present the IR reading is small. Above roughly 50,000 counts means skin is covering the sensor. The threshold is a starting point; a different module or LED current may need a different value.
  • Sanity limits: the sketch ignores any beat outside 30 to 220 BPM, which filters out the glitch you get from a single missed or double-counted beat.
  • Settings: sensor.setup(0x1F, 4, 2, 400, 411, 4096) selects a low LED current, 4-sample averaging, red plus IR mode, 400 samples per second (so 100 per second after averaging), a 411 microsecond pulse width, and the 4096 nA ADC range.
C++
Serial Monitor output: one line per detected beat with the instantaneous BPM and the rolling average.

The first one or two readings after you place your finger are usually off, because the algorithm needs a couple of beats to lock on. Give it five to ten seconds.

Step 3: See Your Pulse in the Serial Plotter

Numbers are useful, but nothing builds intuition like watching the waveform. This tiny sketch streams the raw IR and red readings. Open Tools > Serial Plotter at 115200 baud and put a finger on the sensor.

C++
The raw IR signal in the Serial Plotter. Each hump is one heartbeat; the large slow drift is the DC component and breathing.

What you should see is a large, slowly varying baseline (the DC part) with a small ripple riding on it (the AC part, your pulse). The IR line is typically larger than the red line. If the waveform is flat, the finger is not covering the sensor well. If it is jagged and chaotic, the finger is moving or pressing too hard.

This is also the best debugging tool you have. When the BPM output looks wrong, look at the waveform first: a clean periodic signal gives good readings and a messy one never will.

Step 4: Add SpO2

For oxygen saturation we use the Maxim reference algorithm shipped with the SparkFun library (spo2_algorithm.h). Unlike the beat detector it does not work sample by sample. It analyses a window of 100 samples (4 seconds at 25 samples per second), finds the pulse peaks, computes the red/IR ratio R, and maps it to a percentage. It also returns its own heart rate.

The sketch uses a sliding window: after the first 4 seconds it drops the oldest 25 samples, collects 25 fresh ones (about 1 second), and recalculates. That gives you an updated result every second with a 4-second memory.

The Uno RAM trick. The reference code stores each sample in a 32-bit integer. Two buffers of 100 samples is 800 bytes, which is 40 percent of the Uno's 2 KB of RAM. The sketch below detects the AVR chip at compile time and keeps 16-bit samples instead, shifting each 18-bit reading right by two bits. The ratio of ratios barely changes, because both channels are scaled equally, so SpO2 is unaffected while memory use is halved. On the ESP32 the full 32-bit values are kept.

C++

A -- means the algorithm did not find a trustworthy result in that window, which is normal while you settle your finger. Only trust values after they have been stable for several consecutive seconds.

Understanding the setup() Parameters

sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange) is where most tuning happens.

ParameterOptionsWhat it does
ledBrightness0 to 255 (about 0 to 50 mA)LED drive current. Higher means a stronger signal but more heat and power.
sampleAverage1, 2, 4, 8, 16, 32Hardware averaging. More means less noise and a lower effective rate.
ledMode1 (red), 2 (red + IR), 3 (red + IR + green*)SpO2 needs mode 2. *Green is MAX30105 only.
sampleRate50, 100, 200, 400, 800, 1000, 1600, 3200Raw samples per second before averaging.
pulseWidth69, 118, 215, 411LED pulse in microseconds. Longer means higher resolution (15 to 18 bits).
adcRange2048, 4096, 8192, 16384Full-scale current. Larger avoids saturation under bright light or strong LEDs.

The two sketches use different settings on purpose. The beat detector wants a high sample rate for a smooth waveform. The SpO2 algorithm was designed around 25 samples per second, so it uses 100 samples per second with 4-sample averaging.

Getting Steady Readings

Optical sensors are extremely sensitive to how they are used. Most "my sensor is wrong" problems are technique, not hardware.

  • Light pressure. Rest the finger on the glass. Pressing hard squeezes blood out of the capillaries and flattens the pulse signal.
  • Stay still. Movement is the number one cause of garbage readings. Rest your hand on a table.
  • Cover the sensor completely. Ambient light leaking in adds noise.
  • Warm hands. Cold fingers have poor blood flow and a weak signal.
  • Right LED current. If the IR value is very low (a few thousand), increase ledBrightness. If it sits at the maximum of 262143 for 18-bit mode, the sensor is saturated: reduce the LED current or enlarge the ADC range.
  • Wait for lock. Give the algorithm 5 to 10 seconds before trusting a number.
  • Skin tone, nail polish and tattoos all affect optical absorption. Dark pigment and some nail colors reduce the signal and can bias SpO2.
Correct placement: fingertip resting lightly and fully covering the sensor window, hand supported on a table.

🚨 Danger

**This is not a medical device.** The MAX30102 module and the Maxim algorithm are not clinically validated, and results can be off by several percent or several beats per minute. Hobby readings must never be used to diagnose, monitor or treat any condition. If you are worried about your health, use a certified device and speak to a medical professional.

Troubleshooting

SymptomLikely causeFix
"MAX30102 not found"Wiring, power, or wrong addressRun the I2C scanner; check SDA/SCL and 3.3 V; try another jumper.
Scanner shows no devicesSDA/SCL swapped, no pull-ups, no powerSwap SDA and SCL; confirm the module LED is lit.
Module LED does not light upNo powerCheck VIN and GND; use a multimeter on VIN.
IR value is always tinyFinger absent or LED current too lowCover the sensor; raise ledBrightness.
IR value stuck at maximumSaturationLower LED current or increase adcRange.
BPM jumps aroundMovement or pressureHold still; lighter touch; wait for averaging.
SpO2 always --Algorithm cannot lockKeep finger steady for 5 to 10 s; check the raw waveform.
SpO2 reads 100 % or in the 80s constantlyPoor signal or wrong settingsUse ledMode 2, the SpO2 settings above, and a steady finger.
Uno resets or prints garbageRAM exhaustionUse the Uno-aware sketch in this article; avoid extra large buffers or strings.
Works on ESP32, flaky on Uno5 V I2C levels on a 3.3 V moduleAdd an I2C level shifter and power the module from 3.3 V.

Ideas to Extend

  • OLED display. Show BPM and SpO2 on a small SSD1306 screen over the same I2C bus, so the project works without a computer.
  • Bluetooth heart rate service. Advertise the BPM through the standard BLE Heart Rate profile and read it on your phone. Start with the ESP32 BLE Scanner guide to learn how BLE advertising works.
  • MQTT logging. Publish readings and chart them over time using the ESP32 MQTT tutorial.
  • Battery power. Put the ESP32 to sleep between spot checks, as described in the ESP32 deep sleep guide.
  • Temperature. The MAX30102 has an on-die temperature sensor that you can read with sensor.readTemperature(). It measures the chip, not your body, but is handy for sanity checks.

FAQ

Can I use the MAX30100 library for the MAX30102? No. They are different chips with different register maps. Use the SparkFun MAX3010x library for the MAX30102.

Does this work on 5 V Arduino boards like the Uno? Yes, with the module powered from 3.3 V. Add a level shifter for a permanent build.

Why is my SpO2 always 99 or 100 percent? A healthy person at rest normally reads 95 to 100 percent, so a high value is often plausible. If it never changes regardless of finger or conditions, check the raw waveform for a saturated or flat signal.

Can it measure from the wrist? Not reliably with this module. Fingertips and earlobes have far stronger pulsatile signals than a wrist, and wrist readings need careful optical design and motion compensation.

What is the difference between the two sketches' heart rates? The beat detector reacts to every pulse and averages four. The Maxim algorithm computes the rate across a 4-second window, so it is smoother but slower to respond.

Related on SolderHub

Published on SolderHub

Signal In

Built from what makers search for

The lookups piling up in SolderHub's search bar every day shape what we publish next — new component pages, board guides, and firmware notes, sent out as they go live.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.