Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Heart Rate and SpO2 Monitoring with MAX30102 and STM32

Tech May 10 3

Sensor Operating Principles

The MAX30102 integrates two LEDs (red at 660nm and infrared at 880nm) and a photodetector to facilitate photoplethysmography (PPG). Blood oxygen saturation (SpO2) is determined by the differential absorption of light by oxygenated hemoglobin (HbO2) and deoxygenated hemoglobin (Hb). Since Hb absorbs red light more efficiently than HbO2, while absorption in the infrared spectrum is similar, the ratio of absorbed light allows for the calculation of SpO2 using the ratio of ratios method.

Heart rate monitoring relies on detecting the periodic expansion of blood vessels during cardiac cycles. As blood volume increases, light absorption increases and reflected intensity decreases. By processing the PPG waveform to identify peaks corresponding to heartbeats, the BPM (beats per minute) can be accurately derived.

Hardware Interface Configuration

The interface requires I2C communication for register configuration and FIFO data retrieval. The following pin mapping corresponds to the STM32F103C8T6 peripherals:

MAX30102 Connections:
VCC -> 3.3V
GND -> GND
SCL -> PB7
SDA -> PB8
INT -> PB9

OLED Display Connections:
VCC -> 3.3V
GND -> GND
SCL -> PC13
SDA -> PC14

Sensor Initialization and Register Setup

The MAX30102 utilizes a 32-sample deep FIFO with 6 bytes per sample (3 bytes for Red, 3 for IR), allowing up to 192 bytes of storage. Configuration involves setting the device to SpO2 mode, adjusting the sample rate, LED pulse widths, and currents.

// Configure Mode: SpO2 Mode (0x03)
i2c_write_byte(MODE_CONFIG, 0x03);

// Configure SpO2 Settings:
// ADC Range: 4096 nA, Sample Rate: 100Hz, Pulse Width: 411us
i2c_write_byte(SPO2_CONFIG, 0x27);

// Configure LED Currents:
// Red (LED1): ~7mA, IR (LED2): ~7mA
i2c_write_byte(LED1_PA, 0x24);
i2c_write_byte(LED2_PA, 0x24);
i2c_write_byte(PILOT_PA, 0x7F);

// FIFO Configuration:
// Sample Avg: 2, Rollover: false, Almost Full: 17
// Effective rate becomes 50Hz due to averaging
i2c_write_byte(FIFO_CONFIG, 0x2F);

Data Acquisition

Reading the FIFO involves retrieving a block of data and parsing the 3-byte values for each channel. The 18-bit values are stored in unsigned 32-bit integers.

void max30102_read_fifo(uint32_t *red_val, uint32_t *ir_val) {
    uint8_t temp_buf[6];
    
    // Read 6 bytes from FIFO Data Register (0x07)
    i2c_read_bytes(REG_FIFO_DATA, temp_buf, 6);
    
    // Combine bytes: 3 bytes per channel
    // MSB is only the lower 2 bits of the first byte (0x03 mask)
    *ir_val = (uint32_t)((temp_buf[0] & 0x03) << 16) | (temp_buf[1] << 8) | temp_buf[2];
    *red_val = (uint32_t)((temp_buf[3] & 0x03) << 16) | (temp_buf[4] << 8) | temp_buf[5];
}

Signal Processing and Algorithm Implementation

1. DC Removal and Mean Calculation

The raw PPG signal contains a large DC component caused by tissue and venous blood. This must be removed to analyze the AC pulsatile component.

float calculate_mean(int32_t *data, int len) {
    float sum = 0;
    for(int i=0; i<len; i++) sum += data[i];
    return sum / len;
}

void subtract_dc(int32_t *signal, float mean, int len) {
    for(int i=0; i<len; i++) {
        signal[i] -= (int32_t)mean;
    }
}

2. Baseline Drift Correction

Linear regression is applied to estimate and remove baseline drift caused by motion artifacts or breathing.

float get_slope(int32_t *y, int n) {
    float sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0;
    for (int i = 0; i < n; i++) {
        sum_x += i;
        sum_y += y[i];
        sum_xy += i * y[i];
        sum_x2 += i * i;
    }
    return (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
}

void correct_baseline(int32_t *signal, int len) {
    float slope = get_slope(signal, len);
    float mean = calculate_mean(signal, len);
    for (int i = 0; i < len; i++) {
        signal[i] -= (slope * (i - len/2));
    }
}

3. RMS and Correlation Calculation

The Root Mean Square (RMS) of the AC components is calculated along with the Pearson correlation coefficient to ensure signal validity.

float compute_rms(int32_t *data, int len) {
    float sum_sq = 0;
    for(int i=0; i<len; i++) sum_sq += data[i] * data[i];
    return sqrt(sum_sq / len);
}

float calculate_pearson(int32_t *x, int32_t *y, int len) {
    float sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0, sum_y2 = 0;
    for(int i=0; i<len; i++) {
        sum_x += x[i];
        sum_y += y[i];
        sum_xy += x[i] * y[i];
        sum_x2 += x[i] * x[i];
        sum_y2 += y[i] * y[i];
    }
    return (len * sum_xy - sum_x * sum_y) / sqrt((len * sum_x2 - sum_x * sum_x) * (len * sum_y2 - sum_y * sum_y));
}

4. SpO2 Calculation

The ratio of the AC component to the DC component (normalized RMS) is calculated for both Red and IR channels. The SpO2 is derived from the ratio of these ratios (R).

void calculate_spo2(int32_t *red_raw, int32_t *ir_raw, int len, int8_t *spo2) {
    float red_mean = calculate_mean(red_raw, len);
    float ir_mean = calculate_mean(ir_raw, len);
    
    // Subtract DC for RMS calc
    int32_t red_ac[len], ir_ac[len];
    memcpy(red_ac, red_raw, len*sizeof(int32_t));
    memcpy(ir_ac, ir_raw, len*sizeof(int32_t));
    subtract_dc(red_ac, red_mean, len);
    subtract_dc(ir_ac, ir_mean, len);
    
    float red_rms = compute_rms(red_ac, len);
    float ir_rms = compute_rms(ir_ac, len);
    
    // Calculate AC/DC Ratio
    float r_red = red_rms / red_mean;
    float r_ir = ir_rms / ir_mean;
    
    float ratio = r_red / r_ir; // Ratio of Ratios (R)
    
    // Calibration Curve for SpO2
    if (ratio > 0.02 && ratio < 1.84) {
        *spo2 = (int8_t)((-45.060 * ratio + 30.354) * ratio + 94.845);
    } else {
        *spo2 = -1; // Invalid Value
    }
}

5. Heart Rate Detection via Autocorrelation

Periodicity in the IR signal is detected using autocorrelation to find the peak interval, which is then converted to BPM.

int detect_heart_rate(int32_t *ir_signal, int len) {
    // Find periodicity using autocorrelation
    // Check lags corresponding to 30BPM to 250BPM
    int min_lag = len / 5; // approx 12 seconds lag check window logic placeholder
    int max_lag = len / 1.2; 
    
    float max_corr = -1;
    int best_lag = 0;
    
    for (int lag = min_lag; lag < max_lag; lag++) {
        float sum = 0;
        for (int i = 0; i < len - lag; i++) {
            sum += ir_signal[i] * ir_signal[i + lag];
        }
        if (sum > max_corr) {
            max_corr = sum;
            best_lag = lag;
        }
    }
    
    if (best_lag == 0) return 0;
    
    // Convert samples to time based on sampling rate (e.g., 50Hz)
    // Rate = SampleFreq * 60 / Lag
    return (50 * 60) / best_lag;
}

Troubleshooting Common Issues

No Data Read: Verify I2C pull-up resistors are present on SCL/SDA lines. Check that the logic levels on the I2C bus match the sensor's voltage (3.3V). Use a logic analyzer to confirm the ACK bit from the sensor.

Noisy or Inaccurate Readings: Ambient light is a significant interferent. Ensure the sensor is pressed firmly against the finger or use an opaque cover to block external light. Software-wise, implement a median filter or moving average to smooth out transient spikes in the FIFO data.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.