A Challenge To The Viewer: MAX30102

Published by Dan on

Tackle this coding challenge: Getting a MAX30102 module to display on a SSD1306 I2C OLED using a RP2040 chip based module.

I usually make videos about how to use a sensor or module board with a microcontroller and typically I do this in advance, learn to use it, create a PCB for easy assembly and then demo the use via a YouTube video – all that came crashing down when I tried to pair a RP2040-Zero from Waveshare with a MAX 30102 pulse and O2 sat board.

It seemed so simple, power, ground and I2C interface and I was off to the races but so far – neither the example code nor about 40 attempts at vibe-coding was able to get it to work, across 3 sensor boards and a few microcontrollers, including a plane-Jane Arduino Uno. Even to the point of recreating people’s projects from their YouTube videos.

So here is the challenge for you:

Using the same hardware – a Pi Pico or derivative, a MAX30102 board and a SSD1306 OLED – have it detect your finger, and spit out an accurate heart rate and SPO2 and then, for the ultra mode, have it work on my hardware on my pcb.

Here are the links to what you will need – you do NOT have to purchase a PCB for this challenge, I will test it on mine here – however, ordering PCBs from my sponsor is an easy way to support the channel, also these links are affiliate links which support me when you buy stuff with them with no additional cost to you.

You’re going to need my pinouts as well:

DevicePinNameRP2040 PIN
MAX1I2C Serial DataGP6
MAX2I2C Serial ClockGP7
MAX3INTGP0
MAX4IRDGP1
MAX5RDGP2
MAX6GROUNDGND
MAX7POWER5V (USB)
OLED1GROUNDGND
OLED2POWER3V3
OLED3I2C Serial ClockGP5
OLED4I2C Serial DataGP4

Some notes:

  • I did add 4.7k ohm pull-up resistors between GP6 and 3.3V and GP7 and 3.3V
    • I cannot pull them up to 5V because the RP2040 is not 5V tolerant
  • I could not get as far as I did powering the MAX30102 from 3.3V
    • Whether it was internal or external, for some reason it only works on 5V – I am assuming that the on-board regulator doesn’t work on 3.3V
  • The jumper pads to swap between 1.8V and 3.3V logic are left bare
    • The I2C does run at 3.3V in this configuration

Here is the code that Gemini Pro came up with

Before you ask – I have tried the built-in examples from Sparkfun in the Arduino library and I have also fed it to Claude, GPT and Gemini to try and get it to work – no matter what, it isn’t working with the Pi Pico nor the Uno R3.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "MAX30105.h"        // SparkFun library for MAX3010x sensors
#include "spo2_algorithm.h"  // SpO2 calculation algorithm

// == DISPLAY (OLED) SETUP ==========================================
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1    // Reset pin # (or -1 if sharing Arduino reset pin)
#define OLED_I2C_ADDR 0x3C // Common I2C address for SSD1306

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// == SENSOR (MAX30102) SETUP =======================================
MAX30105 particleSensor;
#define MAX_INT_PIN 2 // GP2
volatile bool samplesAvailable = false;

void onFifoAlmostFull() {
  samplesAvailable = true;
}

// == DATA BUFFERS & VARIABLES ======================================
// ** FIX 1: Renamed BUFFER_SIZE to SAMPLE_BUFFER_SIZE to avoid conflict **
// The spo2_algorithm.h library expects 100 samples (4 seconds at 25Hz)
const int SAMPLE_BUFFER_SIZE = 100; 

uint32_t irBuffer[SAMPLE_BUFFER_SIZE];  // IR LED sensor data
uint32_t redBuffer[SAMPLE_BUFFER_SIZE]; // Red LED sensor data

int32_t bufferLength; // Number of samples in buffer
int32_t spo2;         // Calculated SpO2 value
int8_t validSpo2;     // Flag for valid SpO2
int32_t heartRate;    // Calculated heart rate value
int8_t validHeartRate; // Flag for valid heart rate

long lastDisplayTime = 0;

// ==================================================================
//                            SETUP
// ==================================================================
void setup() {
  Serial.begin(115200);

  // --- 1. Initialize OLED Display (on I2C0) ---
  Wire.setSDA(4); // Set I2C0 SDA to GP4
  Wire.setSCL(5); // Set I2C0 SCL to GP5
  Wire.begin();   // Start I2C0

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
    Serial.println(F("SSD1306 allocation failed"));
    while (true); 
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("OLED Initialized (I2C0)");
  display.display();
  delay(1000);

  // --- 2. Initialize MAX30102 Sensor (on I2C1) ---
  Wire1.setSDA(6); // Set I2C1 SDA to GP6
  Wire1.setSCL(7); // Set I2C1 SCL to GP7
  Wire1.begin();   // Start I2C1

  display.println("Initializing MAX30102 (I2C1)...");
  display.display();

  if (!particleSensor.begin(Wire1, I2C_SPEED_FAST)) { // Use Wire1
    Serial.println("MAX30102 was not found. Please check wiring.");
    display.println("MAX30102 NOT FOUND!");
    display.display();
    while (true);
  }

  // --- 3. Configure Sensor for SpO2 ---
  // ** FIX 2: Changed sample rate from 50Hz to 25Hz **
  // This matches the 25Hz (FreqS) hardcoded in spo2_algorithm.h
  // Settings: (Red LED 68mA, IR LED 68mA, ADC 4096, Sample Rate 25Hz)
  byte ledBrightness = 68;
  byte sampleAverage = 4;
  byte ledMode = 2; // 2 = Red/IR
  int sampleRate = 25; // <-- THIS IS THE FIX
  int pulseWidth = 411;
  int adcRange = 4096;
  
  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); 
  
  // Configure for "FIFO Almost Full" interrupt
  particleSensor.enableAFULL();
  particleSensor.setFIFOAlmostFull(2); 
  particleSensor.clearFIFO(); 

  // --- 4. Attach Interrupt ---
  pinMode(MAX_INT_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(MAX_INT_PIN), onFifoAlmostFull, FALLING);

  display.println("Sensor Ready.");
  display.println("Place finger on sensor.");
  display.display();
  delay(1000);
}

// ==================================================================
//                            LOOP
// ==================================================================
void loop() {

  // --- 1. Check for new samples (using interrupt flag) ---
  if (samplesAvailable) {
    samplesAvailable = false; 
    particleSensor.check();   

    while (particleSensor.available()) {
      // ** FIX 1: Use new variable name **
      if (bufferLength < SAMPLE_BUFFER_SIZE) {
        irBuffer[bufferLength] = particleSensor.getIR();
        redBuffer[bufferLength] = particleSensor.getRed();
        bufferLength++;
      }
      particleSensor.nextSample(); 
    }
  }

  // --- 2. Calculate SpO2 and Heart Rate when buffer is full ---
  // ** FIX 1: Use new variable name **
  if (bufferLength == SAMPLE_BUFFER_SIZE) { 
    // Call the algorithm
    maxim_heart_rate_and_oxygen_saturation(
        irBuffer, SAMPLE_BUFFER_SIZE, redBuffer, // <-- Here
        &spo2, &validSpo2, 
        &heartRate, &validHeartRate);

    // Slide the buffer (keep last 25 samples)
    for (int i = 0; i < 75; i++) {
      irBuffer[i] = irBuffer[i + 25];
      redBuffer[i] = redBuffer[i + 25];
    }
    bufferLength = 75; 
  }

  // --- 3. Update the Display (every 500ms) ---
  if (millis() - lastDisplayTime > 500) {
    lastDisplayTime = millis();
    updateDisplay();
  }
}

// ==================================================================
//                        HELPER FUNCTIONS
// ==================================================================

void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0, 10);

  // Adjust 50000 based on your sensor's readings
  if (particleSensor.getIR() < 50000) {
    display.setTextSize(1);
    display.setCursor(15, 20);
    display.println("Please Place");
    display.setCursor(15, 35);
    display.println("Finger on Sensor");
    heartRate = 0;
    spo2 = 0;
  } else {
    // Display Heart Rate
    display.print("HR ");
    if (validHeartRate) {
      display.print(heartRate);
    } else {
      display.print("--");
    }
    display.println(" bpm");

    // Display SpO2
    display.print("SpO2 ");
    if (validSpo2) {
      display.print(spo2);
    } else {
      display.print("--");
    }
    display.println(" %");
  }

  display.display();
}