Table of Contents
Part 1: System Concept and Architecture
Part 2 : Sponsored Kit Overview
Part 3 : Alternatives for Heart Rate, SpO₂, and Temperature Sensing
Part 4 : MAX30208EVSYS Evaluation System – Technical Overview & Operation
Part 5 : Reverse-Engineering the MAX30208EVSYS Communication Protocol
Part 6 : Communicating with the MAX30208EVSYS in LabVIEW
Part 7 : Building a Cross-Platform Qt6 Desktop Application for the MAX30208EVSYS Temperature Sensor
Part 8 : STM32 Pulse Oximeter & Heart Rate Monitor (MAXREFDES117 + USB CDC)
Part 9 : MAXREFDES117 – JSON Data Acquisition via USB-CDC in LabVIEW
Part 10 : Building a Real-Time Sensor Data Visualizer for MAXREFDES117 / USB CDC
Part 11 : Research Workstation System Architecture
Part 12 : Combined Pulse Oximeter & Temperature Monitoring System in LabVIEW
Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt
In this article, I will walk you through the complete journey of developing a robust, real-time heart rate (BPM) and blood oxygen saturation (SpO2) monitoring system using an STM32L476RG microcontroller (Nucleo development board) and the MAX30102 optical sensor (MAXREFDES117 module).
1. Project Goals and Key Development Challenges
The main objective was to build a non-blocking, highly responsive firmware that streams clean sensor data in real-time to a PC via USB CDC (Virtual COM Port) using a universal JSON format.
Throughout the development process, we successfully tackled several core challenges typical of PPG (Photoplethysmography) optical systems:
-
DC vs. AC Component Separation: The raw optical signal reflected from tissue has a massive DC component (~200,000 units), while the actual pulse wave accounts for less than 2% of the total signal.
-
Motion Artifacts: Finger tremors or shifting pressure cause severe signal spikes that ruin traditional peak-detection algorithms.
-
Avoiding System Freezes: Standard rigid buffer-filling loops easily freeze the microcontroller if the user momentarily lifts their finger.
2. Hardware Architecture & Pinout
The hardware stack is built around low-power, high-reliability components:
-
Microcontroller: STM32 Nucleo-L476RG (ARM Cortex-M4 core with FPU, ultra-low-power L4 series).
-
Sensor Module: MAXREFDES117 featuring the MAX30102 integrated optical sensor (Red and Infrared LEDs, plus a sensitive photodetector).
-
Communication: I2C Bus (pins
PB8for SCL andPB9for SDA) and USB Virtual COM at 115200 baud.

Before diving into the code, let's take a closer look at the hardware layer that forms the foundation of our device.
A. Microcontroller Choice: STM32 Nucleo-L476RG
The core of the system is a 32-bit microcontroller from the STM32L4 family (an ARM Cortex-M4 core with a floating-point unit / FPU). The "L4" series is designed specifically for battery-powered and low-power applications, offering massive processing power while maintaining high energy efficiency.
B. Optical Sensor: MAX30102 vs. MAX30105
Mounted on the MAXREFDES117 board is the MAX30102 – an advanced optical PPG (Photoplethysmography) sensor designed specifically for pulse oximetry and heart rate monitoring. It contains two integrated LEDs: Red and Infrared (IR), along with a sensitive photodetector.
-
Good to know: In the Arduino/PlatformIO ecosystem, SparkFun's universal library based on the
MAX30105.hclass is commonly used. Although we physically have the MAX30102 model (equipped with 2 LEDs, unlike the MAX30105 which adds a green LED for things like smoke detectors), the registers and I2C communication protocol are identical, allowing for seamless compilation. -
Pinout & Connections: Communication takes place via the I2C bus (
PB8for SCL,PB9for SDA).
3. Step-by-Step Development Milestones
To reach a production-grade hobbyist firmware, the development evolved through several crucial stages:
Milestone 1: Establishing Basic I2C and Raw IR Streaming
We started by verifying basic sensor communication over I2C and streaming raw IR values. This confirmed that the wiring, power rails, and USB CDC serial pipeline were working properly without any driver conflicts.
Milestone 2: Transitioning from Blocking Buffers to Non-Blocking Architecture
Initial standard buffer algorithms (like those requiring strict 100-sample windows at 100Hz) caused the system to hang or reset whenever a finger twitched. We replaced this with a non-blocking sliding window and timeout mechanism, allowing the firmware to recover instantly if contact was temporarily lost.
Milestone 3: Implementing Digital Filtering (DC Removal / High-Pass)
To strip away slow baseline drifts (such as skin deformation or changing pressure), we implemented a recursive Infinite Impulse Response (IIR) filter:

Setting the smoothing factor to alpha = 0.95 effectively isolated the clean AC pulse wave. Furthermore, we added a 1.5-second warm-up phase upon finger placement to initialize the filter smoothly and prevent startup transient spikes.
Milestone 4: Motion Artifact Detection & Signal Quality Control
Optical sensors are notoriously sensitive to movement. We added real-time delta tracking

If a sudden spike exceeds the safety threshold (8000), the sample is discarded, and a status message ({"status": "Motion detected - keep finger still"}) is sent instead of corrupted BPM values.
Maximizing MAXREFDES117 (MAX30102) Performance on Difficult Body Locations (e.g., Wrist)
Placing the PPG sensor on the wrist or forearm significantly attenuates the blood volume pulse signal, introducing heavy motion and muscle noise. To overcome this using an STM32 microcontroller, you must transition to non-blocking register management, apply digital band-pass filtering, implement an adaptive peak detection algorithm, and re-calibrate the SpO₂ R-curve.
Modifying Sample Rate (SPS) and Register Configurations
Default libraries often configure the sensor for power-saving mode with lower resolution. For difficult locations, you must increase the LED drive current and lengthen the pulse width (LED_PW) to ensure the light penetrates deeper tissue layers.
Using the standard SparkFun MAX30105 Library (which is fully compatible with the MAX30102 on your MAXREFDES117 board), configure the parameters in your setup() function as follows:#include <Wire.h>
#include "MAX30105.h" // Works perfectly with the MAX30102 chip on the MAXREFDES117 board
MAX30105 particleSensor;
void setup() {
Serial.begin(115200);
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("Sensor not found!");
while (1);
}
// --- HARDWARE OPTIMIZATION FOR DIFFICULT LOCATIONS ---
byte ledBrightness = 0x3F; // High LED current (~12.6mA to 50mA depending on setup) - deeper tissue penetration
byte sampleAverage = 4; // On-chip sample averaging (1, 2, 4, 8, 16, 32) - filters high-frequency noise
byte ledMode = 2; // Mode: 2 = Red + IR (Required for both SpO2 and Heart Rate)
// Increasing the Sample Rate (SPS) improves the precision of the captured PPG wave shape
int sampleRate = 400; // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
// Extending Pulse Width increases the internal ADC resolution (411us = 18-bit)
int pulseWidth = 411; // Options: 69 (15-bit), 118 (16-bit), 215 (17-bit), 411 (18-bit)
// Load custom configurations into the sensor registers
particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, 4096);
}
Golden Rule for Wrists: Set sampleRate = 400 or 800 and pulseWidth = 411. The maximum 18-bit ADC resolution allows the sensor to capture miniscule volumetric changes in the bloodstream that are otherwise dampened by thicker skin.
On-Chip Averaging: Setting sampleAverage = 4 averages the samples directly inside the sensor's hardware FIFO, delivering a smoother signal to your microcontroller.
Eliminating Ambient Light Noise and Filtering Signal Waves
The MAX30102 features built-in hardware Ambient Light Cancellation (ALC). It measures the background light while the LEDs are turned off and subtracts it from the active measurement. However, rapid ambient changes (like 50Hz/60Hz indoor fluorescent lighting or direct sunlight) can still saturate the ADC.
To fix this, you must apply both software filtering and physical isolation:
A. Digital Band-Pass Filter (Removing Muscle Noise and Baseline Drift)
Wrist movement introduces baseline wander (low-frequency drifting), while muscle micro-tremors introduce high-frequency hiss. The code below implements a digital band-pass filter (a combination of High-Pass and Low-Pass) that isolates the AC component, cutting off everything below ~0.5 Hz (30 BPM) and above ~5 Hz (300 BPM).#include <Wire.h>
#include "MAX30105.h"
MAX30105 particleSensor;
// Filter coefficients (Tuned for ~100 Hz effective sampling rate after averaging)
const float alpha_hp = 0.95; // High-Pass filter coefficient (Cuts DC baseline wander)
const float alpha_lp = 0.25; // Low-Pass filter coefficient (Smooths muscle noise)
float dc_offset = 0;
float filtered_signal = 0;
void loop() {
// Read raw Infrared (IR) values - IR provides a more stable heart rate wave than Red
uint32_t raw_ir = particleSensor.getIR();
if (raw_ir > 50000) { // Skin-proximity check (ensures a body part is touching the sensor)
// 1. HIGH-PASS FILTER (Removes DC component and baseline wandering)
dc_offset = (alpha_hp * dc_offset) + ((1.0 - alpha_hp) * raw_ir);
float ac_component = raw_ir - dc_offset;
// 2. LOW-PASS FILTER (Smooths high-frequency muscle tremors)
filtered_signal = (alpha_lp * ac_component) + ((1.0 - alpha_lp) * filtered_signal);
// Output to the Arduino IDE Serial Plotter to visualize a clean PPG wave
Serial.print("Raw_AC:");
Serial.print(ac_component); // For reference
Serial.print(",");
Serial.print("Filtered_AC:");
Serial.println(filtered_signal);
} else {
// Sensor is off the skin
Serial.println("No Signal (Place sensor against skin)");
dc_offset = 0;
filtered_signal = 0;
}
delay(10); // Matches the loop execution to the ~100Hz effective sampling rate
}
B. Physical Optical Isolation
3D Print a Matte Black Enclosure: Your custom casing must include a physical wall separating the LEDs from the photodetector on the MAXREFDES117 PCB. This prevents optical crosstalk (light traveling sideways through the plastic enclosure directly into the sensor without entering the skin).
Use a Black Foam Gasket: Place a small ring of dense, black neoprene foam or rubber around the sensor perimeter. When strapped to the body, it compresses and blocks external light leaks entirely.
Stabilization of Mechanical Contact Pressure
PPG signal response to contact pressure follows a bell-shaped (U-shaped) curve:
- Too Loose: The sensor records air movements, ambient light leaks, and loses contact immediately during movement.
- Too Tight: The sensor compresses the local capillaries, blocking microvascular blood flow entirely. This causes the PPG amplitude to vanish.
To achieve consistent, optimal pressure (optimum transmural pressure), use these mechanical techniques:
- Elastic Hook-and-Loop (Velcro) Band: Avoid stiff, solid plastic watch bands. Use an elastic material that stretches slightly to absorb muscle expansions (e.g., when making a fist) while keeping the sensor tension uniform.
- Spring-Loaded Module Housing: Design an enclosure where the MAXREFDES117 PCB sits on top of a soft miniature spring or silicone sponge cushion inside the strap capsule. This maintains a light, continuous push against the skin, regardless of how tightly the main strap is pulled.
- Double-Sided Medical Adhesive Tape: If testing on the forehead or forearm, using biocompatible, hypoallergenic double-sided medical tape provides the cleanest signal. Cutting a hole for the optics and adhering the sensor directly to the skin completely eliminates motion artifacts.
Non-Blocking STM32 Architecture (C/HAL) and SpO₂ Processing
To calculate SpO₂, you must extract the AC and DC values for both the Red and IR channels simultaneously to compute the Ratio of Ratios (R):

Polling the sensor using delay() fails on STM32 because the sampling intervals must be mathematically uniform. Instead, connect the INT pin of the MAXREFDES117 to an STM32 GPIO External Interrupt (EXTI) pin. When the sensor fills its FIFO, it pulls the INT pin low, triggering the STM32 to fetch data via I2C asynchronously.
Here is the implementation of the SpO₂ extraction algorithm in C for STM32:#include "main.h"
#include <math.h>
#define BUFFER_SIZE 100 // Window size (100 samples = 1 second at 100Hz effective sampling)
// Global filter variables (separated for Red and IR channels)
float dc_red = 0.0f;
float dc_ir = 0.0f;
const float alpha = 0.98f; // IIR low-pass coefficient to isolate the DC baseline
// Sliding buffers for AC wave analysis
float ac_red_buffer[BUFFER_SIZE];
float ac_ir_buffer[BUFFER_SIZE];
uint16_t buffer_index = 0;
extern I2C_HandleTypeDef hi2c1; // Assumes your STM32 HAL I2C setup
/**
* @brief Processes a single raw sample packet (called inside your non-blocking main loop when INT fires)
*/
void Process_PPG_Sample(uint32_t raw_red, uint32_t raw_ir)
{
// 1. Extract the DC component using an IIR Low-Pass Filter
dc_red = (alpha * dc_red) + ((1.0f - alpha) * (float)raw_red);
dc_ir = (alpha * dc_ir) + ((1.0f - alpha) * (float)raw_ir);
// 2. Isolate the AC component by removing the DC offset
float ac_red = (float)raw_red - dc_red;
float ac_ir = (float)raw_ir - dc_ir;
// 3. Store the filtered AC waves into the analysis arrays
ac_red_buffer[buffer_index] = ac_red;
ac_ir_buffer[buffer_index] = ac_ir;
buffer_index++;
// 4. Once the window is full (1 second of data), compute SpO2
if (buffer_index >= BUFFER_SIZE)
{
// Proximity validation: Ensure the sensor is actually resting on tissue
if (dc_red > 50000.0f && dc_ir > 50000.0f)
{
Calculate_SpO2(dc_red, dc_ir);
}
buffer_index = 0; // Reset window index
}
}
/**
* @brief Calculates SpO2 using Peak-to-Peak amplitudes of the collected AC waves
*/
void Calculate_SpO2(float dc_r, float dc_i)
{
float min_red = 999999.0f, max_red = -999999.0f;
float min_ir = 999999.0f, max_ir = -999999.0f;
// Locate the peaks and troughs within the 1-second window
for (int i = 0; i < BUFFER_SIZE; i++)
{
if (ac_red_buffer[i] > max_red) max_red = ac_red_buffer[i];
if (ac_red_buffer[i] < min_red) min_red = ac_red_buffer[i];
if (ac_ir_buffer[i] > max_ir) max_ir = ac_ir_buffer[i];
if (ac_ir_buffer[i] < min_ir) min_ir = ac_ir_buffer[i];
}
// Determine Peak-to-Peak amplitudes (AC component size)
float ac_r_p2p = max_red - min_red;
float ac_i_p2p = max_ir - min_ir;
if (ac_i_p2p > 0 && dc_r > 0)
{
// Compute the Ratio of Ratios (R)
float R = (ac_r_p2p / dc_r) / (ac_i_p2p / dc_i);
// Empirical calibration curve equation: SpO2 = A - B * R
// Generic medical constants for MAX30102 on fingertips: A = 104.0, B = 17.0
float spo2 = 104.0f - (17.0f * R);
// Constrain to realistic physiological limits
if (spo2 > 100.0f) spo2 = 100.0f;
if (spo2 < 60.0f) spo2 = 60.0f;
// Print output to UART or send to an integrated OLED display
printf("Calculated SpO2: %.2f %%\r\n", spo2);
}
}
Calibrating the SpO₂ Curve for Wrist Placement
The default linear equation used above (SpO2 = 104.0 - 17.0 ⋅ R) is calibrated for fingertips, where light easily reflects off dense, shallow capillary beds. Leaving this unchanged for a wrist monitor will yield inaccurate, drifting numbers.
To fix this, developers utilize a second-degree polynomial calibration curve:
Calibration Procedure:
- Attach a certified, medical-grade reference pulse oximeter to the subject's fingertip.
- Mount your STM32-driven MAXREFDES117 device securely to their wrist, outputting the raw, uncalibrated values of R over UART.
- Have the subject safely induce a minor drop in oxygen saturation (e.g., holding their breath for 30–40 seconds until the medical finger oximeter registers a drop down to ~93-94%).
- Log the data pairs: the true SpO2 from the medical device alongside the raw value of R calculated by your STM32 at that exact second.
- Import the log into Excel, MATLAB, or a Python script and apply a polynomial regression fit (Polynomial Fit / Linear Regression). This will generate custom A, B, and C coefficients unique to your custom wrist band assembly and housing pressure.
4. Complete Source Code (src/main.cpp)
Here is the final, stable, and production-ready C++ firmware for PlatformIO:
#include <Arduino.h>
#include <Wire.h>
#include "MAX30105.h"
MAX30105 particleSensor;
// Pulse detection and BPM averaging variables
unsigned long lastBeat = 0;
long lastFilteredIR = 0;
int beatAvg = 0;
const int RATE_SIZE = 8;
byte rates[RATE_SIZE];
byte rateSpot = 0;
// Digital filter variables (DC Removal / High-Pass Filter)
float dcVal = 0;
const float alpha = 0.95; // Smoothing factor for IIR filter
// Startup stabilization (Warm-up phase)
unsigned long fingerTouchTime = 0;
bool isFilterStable = false;
// Transmission interval control
unsigned long lastPrintTime = 0;
const long printInterval = 500; // Send JSON frame every 500 ms
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("--- STM32 PULSE OXIMETER SYSTEM (MAX30102) ---");
Wire.begin();
// Initialize MAX30102 sensor via I2C
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("{\"error\": \"MAX30102 not found! Check wiring.\"}");
while (1);
}
// Sensor configuration:
// LED Brightness = 60, Sample Averaging = 4, Mode = 2 (Red + IR),
// Sample Rate = 400 Hz, Pulse Width = 411 us, ADC Range = 4096
particleSensor.setup(60, 4, 2, 400, 411, 4096);
Serial.println("{\"status\": \"Sensor ready. Place your finger.\"}\n");
}
void loop() {
// Process sensor FIFO buffer in a non-blocking manner
particleSensor.check();
if (particleSensor.available()) {
long rawIR = particleSensor.getIR();
long rawRed = particleSensor.getRed();
particleSensor.nextSample();
// 1. Check finger presence on sensor
if (rawIR < 50000) {
if (millis() - lastPrintTime > 1000) {
Serial.println("{\"status\": \"No finger on sensor\"}");
lastPrintTime = millis();
}
beatAvg = 0;
isFilterStable = false;
dcVal = 0; // Reset filter state when finger is removed
return;
}
// 2. Filter warm-up stabilization phase after finger contact (1.5 seconds)
if (!isFilterStable) {
if (fingerTouchTime == 0) {
fingerTouchTime = millis();
dcVal = rawIR; // Seed the filter state with initial raw reading
}
// Warm up filter state in the background
dcVal = (alpha * dcVal) + ((1.0 - alpha) * (float)rawIR);
if (millis() - fingerTouchTime > 1500) {
isFilterStable = true; // Warm-up complete, ready for pulse detection
}
return;
} else {
fingerTouchTime = 0;
}
// 3. Motion Artifact Detection (sudden amplitude delta monitoring)
static long previousRawIR = rawIR;
long irDelta = abs(rawIR - previousRawIR);
previousRawIR = rawIR;
if (irDelta > 8000) { // Safety threshold for sudden movement/jerk
if (millis() - lastPrintTime > 500) {
Serial.println("{\"status\": \"Motion detected - keep finger still\"}");
lastPrintTime = millis();
}
return; // Skip sample to prevent corrupted BPM readings
}
// 4. Digital High-Pass Filter (DC Removal)
dcVal = (alpha * dcVal) + ((1.0 - alpha) * (float)rawIR);
float filteredIR = (float)rawIR - dcVal;
// 5. Peak Detection & Moving Average BPM Calculation
unsigned long currentTime = millis();
static bool falling = false;
static float peakValue = 0;
if (filteredIR > lastFilteredIR) {
if (falling) falling = false;
if (filteredIR > peakValue) peakValue = filteredIR;
} else if (filteredIR < lastFilteredIR) {
if (!falling && (peakValue - filteredIR > 10.0)) { // Peak threshold limit
falling = true;
long delta = currentTime - lastBeat;
if (delta > 300 && delta < 2000) { // Valid HR pulse interval (30 to 200 BPM)
long beatsPerMinute = 60000 / delta;
if (beatsPerMinute > 45 && beatsPerMinute < 180) { // Plausible HR range
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE;
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++) {
beatAvg += rates[x];
}
beatAvg /= RATE_SIZE;
}
}
lastBeat = currentTime;
peakValue = filteredIR;
}
}
lastFilteredIR = filteredIR;
// 6. Empirical SpO2 Estimation based on Red/IR Ratio
int estimatedSpO2 = 0;
if (rawIR > 100000 && rawRed > 50000) {
float ratio = (float)rawRed / (float)rawIR;
float spo2Calc = 110.0 - (25.0 * ratio);
if (spo2Calc > 95.0 && spo2Calc <= 100.0) estimatedSpO2 = 98;
else if (spo2Calc > 90.0) estimatedSpO2 = 96;
else estimatedSpO2 = 93;
}
// 7. Data streaming via USB CDC in JSON format every 500 ms
if (millis() - lastPrintTime >= printInterval) {
Serial.print("{\"bpm\": ");
Serial.print(beatAvg > 0 ? beatAvg : 0);
Serial.print(", \"spo2\": ");
Serial.print(beatAvg > 0 ? estimatedSpO2 : 0);
Serial.print(", \"filtered_ir\": ");
Serial.print(filteredIR, 2);
Serial.println("}");
lastPrintTime = millis();
}
}
}
5. Output Data Stream Example
Structured JSON formatting allows seamless integration with external dashboard apps or Python visualization scripts:
JSON
{"bpm": 73, "spo2": 98, "filtered_ir": 4425.22}
{"bpm": 73, "spo2": 98, "filtered_ir": 3095.70}
{"status": "Motion detected - keep finger still"}
{"bpm": 73, "spo2": 98, "filtered_ir": 935.17}
6. Impact of Sensor Application Site on PPG Signal Quality and SpO₂ Measurements
Placing the MAXREFDES117 sensor (based on the MAX30102 photoplethysmography (PPG) chip) on different parts of the body has a crucial impact on the accuracy, signal strength, and stability of heart rate and pulse oximetry (SpO2) results. Since the sensor operates on a reflective method (reflection of red and infrared light from blood vessels), the results directly depend on skin thickness, the degree of blood perfusion (tissue perfusion), and the susceptibility of a given site to movement.
Below is an overview of how specific locations affect measurements:
1. Fingertip – Highest Accuracy
- Impact on results: Provides the strongest and most stable PPG signal. Fingertips are densely vascularized (high perfusion), and capillaries are located close to the skin's surface
- Characteristics: An ideal location for calibration and algorithm testing.
- Disadvantages: Highly susceptible to motion artifacts. Any finger twitch drastically falsifies the heart rate and SpO2 readings. Requires remaining motionless and maintaining constant, light pressure (too much pressure blocks blood flow, resulting in a zero reading).
2. Earlobe – Great Alternative
- Impact on results: Yields a very clean pulse signal, comparable in quality to the finger.
- Characteristics: The earlobe has thin skin and no muscle tissue, minimizing interference caused by muscle contractions. It is also less susceptible to whole-body movements than the hand.
- Disadvantages: Harder to attach the sensor (requires a clip). In low temperatures, rapid vasoconstriction occurs here, which can drastically reduce signal amplitude and make SpO2 readings impossible.
3. Forehead – High Clinical Precision
- Impact on results: Studies show that placing the MAX30102 sensor on the forehead achieves over 99% accuracy in SpO2 measurements compared to stationary devices.
- Characteristics: Blood circulation in the forehead area (supratrochlear artery) is exceptionally stable and less prone to weakening during shock or body cooling. This site works great for sleep monitoring systems.
- Disadvantages: Requires thorough cleansing of the skin from sebum and complete blocking of ambient light (the sensor must be covered, e.g., with a headband).
4. Wrist and Forearm – Most Problematic
- Impact on results: The signal is significantly weaker (low amplitude), and heart rate and blood oxygenation results are most susceptible to errors
- Characteristics: Although this is the default location for smartwatches, skin on the wrist is thicker, and arteries and capillaries lie deeper.
- Disadvantages: The standard, simplified algorithm provided by the manufacturer for Arduino/mbed boards (which processes small data packets at a sampling rate of 25–100 samples per second) typically fails to read SpO2 on the wrist. To obtain correct results here, it is necessary to apply advanced digital filters to filter out noise and press the strap very firmly against the skin.
Summary of Physical Parameters of the Application Site:
| Body Part | Signal Strength (Perfusion) | Susceptibility to Interference (Motion) | Suitability for Default MAXREFDES117 Algorithm |
| Fingertip | Very High | High | Ideal (Recommended for testing) |
| Earlobe | High | Low | Very Good |
| Forehead | High | Very Low | Very Good (Requires light isolation) |
| Wrist | Low | Very High | Poor (Requires code modification and noise filtering) |
Main rule: If you are building a prototype using the default source code from Analog Devices / SparkFun libraries, stick to the finger or earlobe. Choosing the wrist will force you to deeply modify the software and filter out noise associated with movement and tissue thickness.
7. Measures Implemented to Mitigate Measurement Issues
1. Optimization of Sensor Operating Parameters and Registers
Default settings provided by basic programming libraries (such as SparkFun or Maxim Integrated) prioritize energy saving, which results in insufficient measurement resolution. To enable light to penetrate deeper tissue layers in diagnostically challenging locations, the chip's register parameters were modified:
-
Increasing optical power and resolution: In the setup configuration section (
setup()), the LED supply current was increased (parameterledBrightnessset to0x3F, corresponding to a range of approx. 12.6 mA to 50 mA) and the pulse duration was extended (pulseWidth = 411, corresponding to 411s). This achieved the maximum 18-bit analog-to-digital converter (ADC) resolution (for comparison, the lowest resolution is 15-bit at 69s). Higher resolution is crucial for the wrist, as it allows the system to capture very small changes in light reflection that are heavily attenuated by thicker skin. -
Raising the sampling rate and hardware filtering: The sampling rate (
sampleRate) was increased to 400Hz (available ranges span from 50H to 3200Hz). A higher SPS significantly improves the precision of PPG waveform shape detection. Additionally, hardware-level sample averaging was enabled (sampleAverage = 4from available options of 1, 2, 4, 8, 16, 32). This smooths the signal and removes minor high-frequency noise before data is transferred to the microcontroller. The sensor was configured in dual mode (ledMode = 2), activating both the red and infrared LEDs, which is necessary for simultaneous heart rate and blood oxygen saturation (SpO2) measurements.
For locations like the wrist, the "golden rule" configuration adopted was sampleRate = 400 or 800 at pulseWidth = 411.
2. Elimination of Ambient Light Interference
Although the MAX30102 chip features a built-in hardware Ambient Light Cancellation (ALC) system—which works by measuring background light when LEDs are turned off and subtracting it from the actual measurement—strong or fluctuating illumination (e.g., sunlight or 50Hz/60Hz fluorescent flicker) can saturate the ADC. This problem was addressed through a dual-track approach:
Software Filtering (High-Pass Filter – HPF)
Both ambient light and slow movement of the sensor across the skin cause baseline wander (drift of the isoelectric line). A digital high-pass filter (IIR / simple exponential filter or Butterworth filter) with a coefficient of alpha = 0.95 (within the 0.90–0.99 range) was implemented in the code. This filter calculates the DC component (dc_offset) and subtracts it from the raw reading (raw_sample), effectively cutting off signal drift and leaving only a clean, pulsating AC component that reflects blood vessel activity.
Physical Optical Isolation
-
Dedicated 3D-printed enclosure with a partition: An enclosure printed from black, matte filament (PLA/PETG) was used, featuring a physical wall (partition) separating the emitting LEDs from the photodetector on the MAXREFDES117 board. This partition makes direct contact with the skin and completely eliminates optical crosstalk (light passing directly from the LEDs to the sensor through the plastic or the top layer of skin, bypassing blood vessels).
-
Foam sealing: A sealing ring made of thin, dense, black neoprene foam (or rubber) was placed around the sensor. When the module is pressed against the skin (e.g., on the forehead or wrist), the foam forms a tight barrier, cutting off external light ingress.
3. Stabilization of Sensor Pressure on the Skin
The proper pressure force of a PPG sensor follows a bell-shaped curve: too little pressure causes the sensor to register air movement, ambient light, and lose tissue contact upon motion, while too much pressure crushes capillaries and stops blood flow, leading to a complete loss of the PPG waveform.
To achieve and maintain optimum transmural pressure, three structural solutions were implemented:
-
Elastic damping strap: The rigid plastic watch strap was replaced with a neoprene or Velcro band featuring an elastic insert. The elasticity of the material compensates for sudden muscle tension (e.g., when clenching a fist), maintaining constant pressure on the skin.
-
Spring-loaded design in the housing: The sensor board was mounted inside the strap capsule on a soft spring or a piece of silicone foam. As a result, the spring generates a constant, gentle pressing force regardless of how tightly the outer strap is pulled.
-
Medical double-sided tape: For tests on the forehead and a cleaned forearm, a dedicated hypoallergenic double-sided medical tape (such as those used for CGM sensors in diabetes care) was applied. After cutting openings for the LEDs and photodetector, the sensor was taped directly to the skin, which almost entirely eliminated motion artifacts without the need for straps.