¿Alguna vez te fijaste en ese clip que la enfermera te pone en el dedo y muestra dos números al tiro? Ese aparato es un pulsioxímetro, y el corazón de un equipo así cabe en un módulo de dos centímetros que puedes conectar a un ESP32 con cuatro cables. Al final de esta guía vas a tener tres mediciones funcionando. frecuencia cardíaca en BPM, saturación de oxígeno (SpO2) y temperatura. y, más importante, vas a entender por qué el sensor entrega lo que entrega y en qué momento deja de ser confiable.
Este proyecto sirve como base para wearables, monitores de ejercicio, proyectos de bioinstrumentación para la universidad o simplemente para entender cómo funciona la fotopletismografía sin leer un paper.
Advertencia médica, en serio. El MAX30102 es un componente de prototipado, no un dispositivo médico. No está certificado, no está calibrado contra un estándar clínico y sus lecturas de SpO2 pueden desviarse varios puntos porcentuales. No lo uses para tomar decisiones de salud, diagnosticar nada ni reemplazar un oxímetro certificado. Si te preocupa tu saturación o tu pulso, anda a un centro de salud. Acá lo usamos para aprender electrónica.
Qué hace realmente el MAX30102
El módulo integra el circuito MAX30102, un sensor óptico que mide la saturación de oxígeno en sangre y la frecuencia cardíaca de forma no invasiva. Incluye además un sensor de temperatura que el propio chip usa para compensar y calibrar sus lecturas ópticas.

Adentro hay tres piezas: un LED rojo, un LED infrarrojo y un fotodetector. Los LED emiten luz hacia la piel y el fotodetector mide cuánta vuelve reflejada. Como el volumen de sangre en el dedo cambia con cada latido, la cantidad de luz reflejada también cambia. y de esa variación sale todo lo demás.

La física, en tres pasos

- Emisión: el LED rojo y el infrarrojo iluminan el tejido, cada uno en una longitud de onda distinta.
- Detección: parte de esa luz atraviesa el tejido y parte se refleja. Cuánta se refleja depende del volumen de sangre y de su nivel de oxigenación. El fotodetector mide esa luz de vuelta.
- Comparación: la hemoglobina oxigenada absorbe más luz infrarroja; la desoxigenada absorbe más luz roja. Comparando la intensidad reflejada en ambas longitudes de onda, el chip calcula la SpO2. Y midiendo el tiempo entre los máximos de esa onda, calcula los BPM.
Por qué tus lecturas van a ser más ruidosas que las del hospital
Acá hay un detalle que casi ningún tutorial menciona y que explica el 90% de las frustraciones con este sensor: el MAX30102 trabaja por reflectancia, no por transmitancia. El oxímetro de la clínica es una pinza que pone el LED a un lado del dedo y el fotodetector al otro: la luz atraviesa el dedo completo y la señal es limpia. El MAX30102 tiene LED y fotodetector en la misma cara, así que solo ve la luz que rebota en los primeros milímetros de tejido. Eso significa tres cosas concretas:
- La señal es del orden de decenas de veces más débil, así que el ruido pesa mucho más.
- Cualquier movimiento del dedo desplaza la zona iluminada y arruina la medición.
- La luz ambiente que se cuela por los costados entra directo al fotodetector como señal falsa.
Por eso la recomendación de apoyar el dedo quieto y con presión constante no es un detalle de cortesía: es la diferencia entre medir y no medir.
MAX30102 vs MAX30100 vs MAX30105
Los tres se ven casi iguales en las fotos de las tiendas y se confunden seguido:
| Chip | LED | Uso típico | Nota |
|---|---|---|---|
| MAX30100 | Rojo + IR | Versión antigua | Tiene erratas conocidas de I2C y peor relación señal/ruido. Evítalo si puedes elegir. |
| MAX30102 | Rojo + IR | Pulso y SpO2 | El punto dulce precio/rendimiento. Es el de esta guía. |
| MAX30105 | Rojo + IR + Verde | Pulso, SpO2, presencia y detección de partículas | El LED verde extra sirve para detección de humo/partículas y para pulso con movimiento. Más caro. |
Un detalle que confunde a todo el mundo: la librería se llama MAX30105 pero funciona perfecto con el MAX30102. SparkFun escribió un solo driver para toda la familia MAX3010x y lo nombró por el chip tope de línea. Si ves MAX30105 sensor; en el código y tu módulo dice MAX30102, está correcto. no te compraste el sensor equivocado.
Conexión al ESP32
El sensor habla por I2C, así que se conecta a los pines I2C por defecto de tu placa:
| Placa | SDA | SCL |
|---|---|---|
| ESP32 (DevKit clásico) | GPIO 21 | GPIO 22 |
| ESP32-S3 | GPIO 8 | GPIO 9 |
El módulo se alimenta por el pin VIN, y acepta tanto 5V como 3V3 porque trae un regulador propio a bordo. En total son cuatro cables: VIN, GND, SDA y SCL.

Dos cosas que te van a ahorrar tiempo:
- No necesitas resistencias pull up externas. El módulo (la placa morada o azul que compras, no el chip pelado) ya trae las pull up de I2C soldadas. Si además pones las tuyas, bajas demasiado la resistencia equivalente del bus y puedes hacerlo fallar.
- La dirección I2C es 0x57 y es fija. No hay jumper para cambiarla. Si necesitas dos MAX30102 en el mismo proyecto, vas a tener que usar un segundo bus I2C del ESP32 o un multiplexor tipo TCA9548A.
Si tu placa no es ninguna de las dos de la tabla, revisa el pinout específico de tu modelo antes de conectar.
Preparar el Arduino IDE
Necesitas dos cosas instaladas: el soporte para placas ESP32 en el Arduino IDE (si nunca lo instalaste, hazlo primero desde el gestor de placas), y la librería del sensor.
Para la librería, abre el Gestor de Librerías del Arduino IDE, busca SparkFun MAX3010x e instala la de SparkFun.

Existen varias librerías para este sensor, pero la de SparkFun es la que mejor anda y la más directa de usar.
1) Frecuencia cardíaca (BPM)
Este primer sketch lee el pulso y muestra el promedio en el Monitor Serial.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
MAX30105 sensor;
// Settings
const byte RATE_SIZE = 5; // How many beats we average
byte rates[RATE_SIZE]; // Store last few valid BPM values
byte rateSpot = 0;
byte validBeats = 0;
long lastBeat = 0;
float currentBPM = 0;
int averageBPM = 0;
unsigned long lastPrint = 0;
// Only accept realistic heart rates (adjust for your case)
const float MIN_BPM = 45;
const float MAX_BPM = 180;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Heart Rate Sensor");
Serial.println();
Serial.println("Place your finger on the sensor...");
Serial.println();
// Initialize the sensor
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
sensor.setup(); // Use default settings
sensor.setPulseAmplitudeRed(0); // Turn off red LED
}
void loop() {
long irValue = sensor.getIR(); // Read infrared value
// Check if a heartbeat was detected
if (checkForBeat(irValue)) {
long timeBetweenBeats = millis() - lastBeat;
lastBeat = millis();
currentBPM = 60.0 / (timeBetweenBeats / 1000.0);
// Only keep realistic values
if (currentBPM >= MIN_BPM && currentBPM <= MAX_BPM) {
rates[rateSpot] = (byte)currentBPM;
rateSpot++;
if (rateSpot >= RATE_SIZE){
rateSpot = 0;
}
// Count valid beats (until buffer is full)
if (validBeats < RATE_SIZE) {
validBeats++;
}
// Only calculate average when buffer is full
if (validBeats >= RATE_SIZE) {
// Calculate the average
averageBPM = 0;
for (byte i = 0; i < RATE_SIZE; i++) {
averageBPM += rates[i];
}
averageBPM /= RATE_SIZE;
}
}
}
// Print once every second
if (millis() - lastPrint >= 1000) {
lastPrint = millis();
if (irValue < 50000) {
// No finger on the sensor
Serial.println("Waiting for finger...");
}
else if (averageBPM == 0) {
// Finger is present but not enough valid beats yet
Serial.println("Measuring... keep your finger still");
}
else {
// We have a valid average
Serial.print("Heart Rate: ");
Serial.print(averageBPM);
Serial.println(" BPM");
}
}
}
Cómo funciona el código
El truco está en el LED infrarrojo. Cuando el corazón late, entra más sangre a los vasos del dedo, se absorbe más luz infrarroja y llega menos luz al fotodetector. Entre latido y latido pasa lo contrario. Esa subida y bajada forma una onda, y midiendo el tiempo entre sus peaks sale la frecuencia.
Como el algoritmo de pulso solo usa el infrarrojo, el código apaga el LED rojo para que no moleste:
sensor.setup(); // Use default settings
sensor.setPulseAmplitudeRed(0); // Turn off red LED
En el loop(), la función checkForBeat() decide si acaba de ocurrir un latido a partir de la señal infrarroja. Si lo hubo, se calcula el tiempo transcurrido desde el latido anterior y de ahí salen los BPM.
Las variables globales son las que sostienen ese promedio:
const byte RATE_SIZE = 5;
byte rates[RATE_SIZE];
byte rateSpot = 0;
byte validBeats = 0;
long lastBeat = 0;
float currentBPM = 0;
int averageBPM = 0;
unsigned long lastPrint = 0;
RATE_SIZE define cuántas lecturas entran en el promedio. rates es el arreglo circular donde se guardan, y rateSpot marca en qué posición se va a escribir la próxima. cuando llega al final vuelve a cero y empieza a pisar las más viejas. validBeats cuenta cuántos latidos válidos llevas acumulados y crece hasta llegar a RATE_SIZE, momento en que el promedio recién se considera confiable. lastBeat guarda el instante del último latido válido, que es justamente lo que permite calcular el intervalo entre latidos. Y lastPrint no tiene nada que ver con la medición: existe solo para imprimir una vez por segundo en vez de inundar el Monitor Serial con miles de líneas.
Ahora, el promedio móvil merece una explicación, porque es lo que hace la diferencia entre un número que baila y uno legible. El código guarda las últimas 5 lecturas válidas en el arreglo rates y recién muestra un valor cuando el buffer se llenó. Además descarta cualquier lectura fuera del rango de 45 a 180 BPM:
const float MIN_BPM = 45;
const float MAX_BPM = 180;
Ese filtro existe porque un dedo mal apoyado genera falsos latidos que dan valores absurdos. Si eres deportista y tu pulso en reposo baja de 45, ajusta MIN_BPM. Si quieres un número más estable a costa de que tarde más en aparecer, sube RATE_SIZE a 8 o 10.
El umbral irValue < 50000 es el detector de "no hay dedo". No es un valor sagrado: depende de tu módulo, de la presión del dedo y hasta del tono de piel. Si el sensor dice "Waiting for finger..." con el dedo puesto, imprime irValue crudo en el Monitor Serial y ajusta el umbral a la mitad de lo que veas con el dedo apoyado.
El bloque de impresión distingue tres situaciones, y conviene reconocerlas en pantalla para saber qué está pasando: si irValue está bajo el umbral no hay dedo y verás Waiting for finger...; si hay dedo pero el promedio todavía vale 0, significa que el buffer aún no se llena y verás Measuring... keep your finger still; y recién cuando hay cinco latidos válidos acumulados aparece el número en BPM. Si te quedas pegado indefinidamente en el segundo mensaje, no es que el código esté malo. son latidos que están cayendo fuera del rango válido por movimiento.

Sube el código, abre el Monitor Serial a 115200 baudios y apoya el dedo. Mantenlo quieto y con presión pareja. un elástico o una banda de goma para sujetar el sensor al dedo funciona mucho mejor que el pulso.

2) Saturación de oxígeno (SpO2)
La SpO2 no se calcula latido a latido: el algoritmo necesita una ventana completa de muestras para comparar las dos longitudes de onda. Por eso este sketch junta 100 lecturas antes de entregar un resultado.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"
#include "spo2_algorithm.h"
MAX30105 sensor;
// Buffers needed by the algorithm
#define BUFFER_SIZE 100
uint32_t irBuffer[BUFFER_SIZE];
uint32_t redBuffer[BUFFER_SIZE];
int32_t spo2; // Oxygen saturation value
int8_t validSPO2; // 1 = valid reading, 0 = not valid yet
int32_t heartRate; // Not used, but required by the function
int8_t validHeartRate;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Oxygen Saturation");
Serial.println();
Serial.println("Place your finger on the sensor...");
Serial.println();
// Initialize the sensor
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
byte ledBrightness = 60; // 0=Off to 255=50mA
byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32
byte ledMode = 2; // 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulseWidth = 411; // 69, 118, 215, 411
int adcRange = 4096; // 2048, 4096, 8192, 16384
// Configure sensor with these settings
sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
}
void loop() {
// Collect 100 samples (takes a few seconds)
for (byte i = 0; i < BUFFER_SIZE; i++) {
while (sensor.available() == false) {
sensor.check();
}
redBuffer[i] = sensor.getRed();
irBuffer[i] = sensor.getIR();
sensor.nextSample();
}
// Calculate SpO2
maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_SIZE, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
// Display oxygen saturation
if (validSPO2) {
Serial.print("Oxygen Saturation: ");
Serial.print(spo2);
Serial.println(" %");
} else {
Serial.println("Measuring... keep your finger still");
}
Serial.println();
}
Cómo funciona el código
Acá el LED rojo sí se usa: la SpO2 sale justamente de la razón entre la luz roja y la infrarroja reflejadas. El algoritmo necesita dos buffers paralelos, uno por cada longitud de onda, más las variables donde deja el resultado:
// Buffers needed by the algorithm
#define BUFFER_SIZE 100
uint32_t irBuffer[BUFFER_SIZE];
uint32_t redBuffer[BUFFER_SIZE];
int32_t spo2; // Oxygen saturation value
int8_t validSPO2; // 1 = valid reading, 0 = not valid yet
int32_t heartRate; // Not used, but required by the function
int8_t validHeartRate;
Los uint32_t no son capricho: las lecturas crudas del ADC del MAX30102 llegan como enteros de 18 bits sin signo, así que no caben en un int de 16 bits. Con 100 muestras de cada color, los dos arreglos ocupan 800 bytes de RAM. irrelevante en un ESP32, pero es lo primero que tendrías que recortar si alguna vez portas este código a un microcontrolador de 2 KB. Y fíjate en heartRate y validHeartRate: están declaradas solo porque la firma de la función las exige, no porque este sketch las use.
El sensor se configura con parámetros específicos para esta medición:
byte ledBrightness = 60;
byte sampleAverage = 4;
byte ledMode = 2;
byte sampleRate = 100;
int pulseWidth = 411;
int adcRange = 4096;
sensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
Qué significa cada uno:
| Parámetro | Valor | Qué controla | Opciones |
|---|---|---|---|
ledBrightness |
60 | Brillo de los LED | 0 a 255 |
sampleAverage |
4 | Muestras que promedia el sensor | 1, 2, 4, 8, 16, 32 |
ledMode |
2 | Qué LED se activan | 1 = solo rojo, 2 = rojo + IR, 3 = rojo + IR + verde |
sampleRate |
100 | Muestras por segundo | 50, 100, 200, 400, 800, 1000, 1600, 3200 |
pulseWidth |
411 | Ancho de pulso del LED | 69, 118, 215, 411 |
adcRange |
4096 | Rango del ADC (sensibilidad) | 2048, 4096, 8192, 16384 |
Con sampleRate = 100 y un buffer de 100 muestras, cada resultado toma aproximadamente un segundo de captura. Si subes el buffer para tener más estabilidad, ese tiempo sube en la misma proporción. y el dedo tiene que quedarse quieto todo ese rato.
La función que hace el trabajo pesado es:
maxim_heart_rate_and_oxygen_saturation(irBuffer, BUFFER_SIZE, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
Deja la saturación en spo2 y levanta la bandera validSPO2 cuando la lectura es confiable. Fíjate que la función también devuelve frecuencia cardíaca, pero en este sketch no se usa: el algoritmo de pulso del primer ejemplo responde mejor.

Espera unos segundos con el dedo apoyado hasta que aparezcan valores válidos. Si ves lecturas que saltan entre 80 y 99, no es que te estés asfixiando: es movimiento.
3) Temperatura
El sensor de temperatura que el chip usa internamente para calibrarse también se puede leer directamente.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-max30102-oximeter-heart-rate-sensor/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Wire.h>
#include "MAX30105.h"
MAX30105 sensor;
void setup() {
Serial.begin(115200);
Serial.println("MAX30102 Temperature Sensor");
Serial.println();
// Initialize the sensor
if (!sensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Error initializing the sensor...");
while (1);
}
// Turn off the LEDs so they don't heat the sensor
sensor.setup(0); // 0 = LEDs off
sensor.enableDIETEMPRDY(); // Enable temperature ready signal
}
void loop() {
// Read temperature in Celsius
float tempC = sensor.readTemperature();
// Convert to Fahrenheit
float tempF = tempC * 1.8 + 32.0;
// Print the temperature readings
Serial.print("Temperature: ");
Serial.print(tempC, 2);
Serial.print(" °C | ");
Serial.print(tempF, 2);
Serial.println(" °F");
delay(1000);
}
Acá el orden importa: se apagan los LED con sensor.setup(0) para que no calienten el encapsulado y falseen la medición, y se habilita la señal de temperatura lista con sensor.enableDIETEMPRDY(). Después, readTemperature() devuelve grados Celsius y el resto es aritmética para pasar a Fahrenheit.

Una advertencia importante que el sensor no te va a dar: esto es la temperatura del die, o sea del silicio del chip, no tu temperatura corporal. Está pensada para compensación interna, tiene una precisión del orden de ±1 °C y lo que mide es la temperatura de la yema del dedo en contacto con el encapsulado, que es varios grados más baja que la temperatura central del cuerpo y varía con la temperatura ambiente. Sirve perfecto para ver tendencias o para el proyecto; no sirve para saber si alguien tiene fiebre.
Errores comunes y cómo salir de ellos
- "Error initializing the sensor..." y se queda pegado. Es el
while(1)del setup. Revisa el cableado y confirma que el sensor aparece en el bus: sube un sketch de I2C scanner y busca la dirección0x57. Si no aparece ninguna dirección, el problema es de conexión o de alimentación. Si aparece otra, no es un MAX30102. - Compila pero nunca detecta el dedo. Baja el umbral
irValue < 50000según lo que midas realmente (ver arriba). - Los BPM demoran mucho en aparecer. Es el buffer: necesita 5 latidos válidos consecutivos. Con el pulso en reposo eso son unos 5 segundos, más lo que tardes en estabilizar el dedo.
- Valores erráticos en SpO2. Casi siempre es movimiento o luz ambiente. Tapa el sensor con el dedo completo y aléjate de luz directa.
- El sensor se calienta. Si dejaste
ledBrightnessalto y el sensor corriendo mucho rato, es normal. Baja el brillo: para un dedo apoyado con presión, valores entre 30 y 60 sobran.
Variantes y mejoras
Estas tres ideas extienden el proyecto más allá del Monitor Serial:
- Unificar los tres sketches en un solo oxímetro con pantalla. Es el paso natural: en lugar de tener tres programas separados, puedes alternar entre los modos de medición y mostrar BPM, SpO2 y temperatura en una pantalla OLED de 0.96" por I2C. Lo bueno es que la OLED comparte el mismo bus que el sensor. los mismos GPIO 21 y 22. así que no necesitas cables adicionales, solo cuidar que las direcciones I2C no choquen (la OLED suele estar en
0x3Cy el sensor en0x57, así que conviven sin problema). Si armas esto, deja el disclaimer médico visible en la pantalla de inicio. - Registrar en el tiempo en vez de mirar números. Un pulso aislado dice poco; una curva dice bastante. Guarda las lecturas con marca de tiempo en una microSD por SPI, o mándalas por WiFi a un servidor web del propio ESP32 para graficarlas en el navegador. El ESP32 tiene WiFi integrado, así que esta variante no cuesta hardware extra.
- Convertirlo en un dispositivo a batería. Con una batería LiPo y el deep sleep del ESP32 puedes hacer un medidor portátil que despierte solo cuando lo necesitas. La clave está en apagar los LED del sensor con
sensor.setup(0)antes de dormir: son, por lejos, lo que más consume del circuito.
Personalización para Chile
Todo lo que necesitas para este proyecto se consigue en MechatronicStore:
- Placa ESP32 DevKit. el cerebro del proyecto. Cualquier variante DevKit de 30 o 38 pines sirve; solo confirma en qué GPIO quedan SDA y SCL en tu modelo.
- Módulo sensor MAX30102. el protagonista. Búscalo también como "GY MAX30102"; es el mismo módulo.
- Protoboard 400 puntos y jumpers macho hembra. el sensor viene con header de pines, así que macho hembra es la combinación que necesitas para llegar de la protoboard al módulo.
- Cable USB para programar y alimentar el ESP32 (micro USB o USB-C según tu placa).
Equivalencias, para que no te compliques leyendo el tutorial original: si en un tutorial en inglés ves un "SparkFun ESP32 Thing" o un "Adafruit HUZZAH32", el ESP32 DevKit del catálogo local cumple exactamente la misma función a una fracción del precio. cambian los GPIO expuestos, no el chip. Y donde digan "MAX30102 breakout board", cualquier módulo MAX30102 del mercado local es equivalente: todos usan el mismo chip de Maxim y la misma librería.
Lo único de la lista de materiales que no vas a encontrar en una tienda de electrónica es el elástico para sujetar el sensor al dedo, y ese lo tienes en la casa.
Presupuesto aproximado: el proyecto completo sale del orden de CLP $20.000 a $25.000 si partes de cero, y bastante menos si ya tienes el ESP32 y la protoboard. Confirma precios y stock en el catálogo antes de comprar.
Recursos
- Tutorial original: ESP32 with MAX30102: Pulse Oximeter and Heart Rate Sensor (Arduino IDE). Rui Santos & Sara Santos, Random Nerd Tutorials
- Librería SparkFun MAX3010x: SparkFun_MAX3010x_Sensor_Library
Versión chilena con componentes en stock local en MechatronicStore. Guía inspirada en el tutorial de Random Nerd Tutorials, reescrita con contexto adicional sobre las limitaciones del sensor, comparación con la familia MAX3010x y sección de troubleshooting.




