Adding an SD card module to a MAX30102 Project

I've been experimenting with a MAX30102 module connected to an Arduino Nano and I'm using an OLED display to show BPM and SpO2 outputs. Having tried several examples of code from sensor libraries and YouTube tutorials, I found the tutorial by Bill Kolicoski (tastethecode.com) to work really well, the only change being my use of a MAX30102, where his sensor is a MAX30105. Checking the output against a commercial "finger clamp" unit I find both pulse and O2 output values to be acceptable. I want to add a datalogging function using a SD card module (SPI device), however, when I add "#include <SD.h>" to my sketch it compiles but then I get an error message in the serial monitor ("SSD1306 allocation failed") and the sketch won't run.

It seems there's a conflict between the SPI device and the two I2C devices. I'd appreciate some guidance on how to get the SD card module to work in this project.

Here's my code:

#include <MAX3010x.h>
#include <Adafruit_SSD1306.h>
#include "filters.h"
#include <Wire.h>
#include <SPI.h>
#include <SD.h>

#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)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Sensor (adjust to your sensor type)
MAX30105 sensor;
const auto kSamplingRate = sensor.SAMPLING_RATE_400SPS;
const float kSamplingFrequency = 400.0;

const int chipSelect = 10;

// Finger Detection Threshold and Cooldown
const unsigned long kFingerThreshold = 10000;
const unsigned int kFingerCooldownMs = 500;

// Edge Detection Threshold (decrease for MAX30100)
const float kEdgeThreshold = -2000.0;

// Filters
const float kLowPassCutoff = 5.0;
const float kHighPassCutoff = 0.5;

// Averaging
const bool kEnableAveraging = true;
const int kAveragingSamples = 5;
const int kSampleThreshold = 5;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  SPI.begin();

  // while (!Serial);

  // Serial.print("Initializing SD card...");

  // if (!SD.begin(chipSelect)) {
  //   Serial.println("initialization failed. Things to check:");
  //   Serial.println("1. is a card inserted?");
  //   Serial.println("2. is your wiring correct?");
  //   Serial.println("3. did you change the chipSelect pin to match your shield or module?");
  //   Serial.println("Note: press reset button on the board and reopen this Serial Monitor after fixing your issue!");
  //   while (true);
  //}

  //Serial.println("initialization done.");
 
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 allocation failed"));
    while (1);
  }

  if(sensor.begin() && sensor.setSamplingRate(kSamplingRate)) { 
    Serial.println("Sensor initialized");
  }
  else {
    Serial.println("Sensor not found");  
    while(1);
  }

  display.clearDisplay();
  initDrawScreen(); 
}

// Filter Instances
LowPassFilter low_pass_filter_red(kLowPassCutoff, kSamplingFrequency);
LowPassFilter low_pass_filter_ir(kLowPassCutoff, kSamplingFrequency);
HighPassFilter high_pass_filter(kHighPassCutoff, kSamplingFrequency);
Differentiator differentiator(kSamplingFrequency);
MovingAverageFilter<kAveragingSamples> averager_bpm;
MovingAverageFilter<kAveragingSamples> averager_r;
MovingAverageFilter<kAveragingSamples> averager_spo2;

// Statistic for pulse oximetry
MinMaxAvgStatistic stat_red;
MinMaxAvgStatistic stat_ir;

// R value to SpO2 calibration factors
// See https://www.maximintegrated.com/en/design/technical-documents/app-notes/6/6845.html
float kSpO2_A = 1.5958422;
float kSpO2_B = -34.6596622;
float kSpO2_C = 112.6898759;

// Timestamp of the last heartbeat
long last_heartbeat = 0;

// Timestamp for finger detection
long finger_timestamp = 0;
bool finger_detected = false;

// Last diff to detect zero crossing
float last_diff = NAN;
bool crossed = false;
long crossed_time = 0;

void loop() {
  auto sample = sensor.readSample(1000);
  float current_value_red = sample.red;
  float current_value_ir = sample.ir;
  
  // Detect Finger using raw sensor value
  if(sample.red > kFingerThreshold) {
    if(millis() - finger_timestamp > kFingerCooldownMs) {
      finger_detected = true;
    }
  }
  else {
    // Reset values if the finger is removed
    differentiator.reset();
    averager_bpm.reset();
    averager_r.reset();
    averager_spo2.reset();
    low_pass_filter_red.reset();
    low_pass_filter_ir.reset();
    high_pass_filter.reset();
    stat_red.reset();
    stat_ir.reset();
    
    finger_detected = false;
    finger_timestamp = millis();
  }

  if(finger_detected) {
    displayMeasuredValues(false, 0, 0);
    current_value_red = low_pass_filter_red.process(current_value_red);
    current_value_ir = low_pass_filter_ir.process(current_value_ir);

    // Statistics for pulse oximetry
    stat_red.process(current_value_red);
    stat_ir.process(current_value_ir);

    // Heart beat detection using value for red LED
    float current_value = high_pass_filter.process(current_value_red);
    float current_diff = differentiator.process(current_value);

    // Valid values?
    if(!isnan(current_diff) && !isnan(last_diff)) {
      
      // Detect Heartbeat - Zero-Crossing
      if(last_diff > 0 && current_diff < 0) {
        crossed = true;
        crossed_time = millis();
      }
      
      if(current_diff > 0) {
        crossed = false;
      }
  
      // Detect Heartbeat - Falling Edge Threshold
      if(crossed && current_diff < kEdgeThreshold) {
        if(last_heartbeat != 0 && crossed_time - last_heartbeat > 300) {
          // Show Results
          int bpm = 60000/(crossed_time - last_heartbeat);
          float rred = (stat_red.maximum()-stat_red.minimum())/stat_red.average();
          float rir = (stat_ir.maximum()-stat_ir.minimum())/stat_ir.average();
          float r = rred/rir;
          float spo2 = kSpO2_A * r * r + kSpO2_B * r + kSpO2_C;
          
          if(bpm > 50 && bpm < 250) {
            // Average?
            if(kEnableAveraging) {
              int average_bpm = averager_bpm.process(bpm);
              int average_r = averager_r.process(r);
              int average_spo2 = averager_spo2.process(spo2);
  
              // Show if enough samples have been collected
              if(averager_bpm.count() >= kSampleThreshold) {
                Serial.print("Time (ms): ");
                Serial.println(millis());
                Serial.print("Heart Rate (avg, bpm): ");
                Serial.println(average_bpm);
                Serial.print("R-Value (avg): ");
                Serial.println(average_r);  
                Serial.print("SpO2 (avg, %): ");
                Serial.println(average_spo2);
                displayMeasuredValues(false, average_bpm, average_spo2);
              }
            }
            else {
              Serial.print("Time (ms): ");
              Serial.println(millis());
              Serial.print("Heart Rate (current, bpm): ");
              Serial.println(bpm);  
              Serial.print("R-Value (current): ");
              Serial.println(r);
              Serial.print("SpO2 (current, %): ");
              Serial.println(spo2);   
              displayMeasuredValues(false, bpm, spo2);
            }
          }

          // Reset statistic
          stat_red.reset();
          stat_ir.reset();
        }
  
        crossed = false;
        last_heartbeat = crossed_time;
      }
    }

    last_diff = current_diff;
  } 
  else {
    displayMeasuredValues(true, 0, 0);
  }
}

void initDrawScreen(void) {
  display.clearDisplay();

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  display.println(F("Taste The Code"));
  display.println(F(""));
  display.setCursor(5, display.getCursorY());
  display.setTextSize(2);
  display.println(F("BPM  %SpO2"));
  display.display();
}
bool display_reset = true;
void displayMeasuredValues(bool no_finger, int32_t beatAvg, int32_t spo2) {
  display.setCursor(5,35);
  display.setTextColor(WHITE, BLACK);
  if(no_finger) {
    display.setTextSize(2);
    display.println(F("NO Finger            "));
    display_reset = true;
    display.display();
  } else if(beatAvg < 30 && display_reset) {
    display.setTextSize(2);
    display.println(F("Pls.  Wait             "));
    display_reset = false;
    display.display();
  } 
  else if(beatAvg >= 30) {
    display.setTextSize(2);
    display.println(F("             "));
    display.setCursor(5,35);
    display.setTextSize(3);
    display.print(beatAvg);
    display.print(F(" "));
    if(spo2 >= 20 && spo2 <= 100) {
      display.print(spo2);
    } 
    else {
      display.print(F("--"));
    }
    display.println(F("    "));
    display.display();
  }
}

Your Nano has 2K of RAM. The Adafruit_SSD1306 library allocates 1K of RAM as a screen buffer, but it doesn't do it until runtime. So that 1K doesn't show up in your compilation stats. I'll bet the RAM usage you're seeing at compilation time is over 1K (or even just close to it). display.begin(...) tries to find 1K to allocate, fails, and it's game over.

You've got a few string literals in print statements that you could wrap in the F() macro to see if that pulls you back from the edge. But it's a long shot.

Thanks for the quick reply.

The last 2 lines from the compilation output reads:
Sketch uses 24532 bytes (79%) of program storage space. Maximum is 30720 bytes.
Global variables use 1425 bytes (69%) of dynamic memory, leaving 623 bytes for local variables. Maximum is 2048 bytes.

I guess I'd be better off using an ESP8266?

And there you go.

8266, Nano Every, Mega, Teensy, Pico... anything with more RAM and that is supported by the libraries you want to use.

OR... since you're only outputting text to the SSD1306, use a different library that doesn't use a page buffer. SSD1306Ascii is one that comes to mind.

Thank you very much. I'll give the library you suggested a try in the first instance.

The simplest approach for me was to switch the Nano out for a Mega 2560 and hey presto! I've now added to the code and have the project writing to an SD card at prescribed intervals. Thanks again for your help.