Arduino version , update and tested on NUCLEO-L073RZ

Dependencies:   mbed

Oximeter and pulse sensor, based on the Beer-Lambert gas absorption law

MAXREFDES117 ( MAX30102 ) from MAXIM, based on MAXIM library for Arduino

adaptation C.Dupaty 07-2017

Tested on STM32 NUCLEO-F411RE and NUCLEO-L073RZ

https://os.mbed.com/media/uploads/cdupaty/max30102.jpg

algorithm/algorithm.cpp

Committer:
MaximGordon
Date:
2016-04-20
Revision:
1:e88f22c6c1b0
Parent:
0:346a7fa07998
Child:
2:560e76e77544

File content as of revision 1:e88f22c6c1b0:

/** \file algorithm.cpp ******************************************************
*
* Project: MAXREFDES117#
* Filename: algorithm.cpp
* Description: This module calculates the heart rate/SpO2 level
*
* Revision History:
*\n 1-18-2016 Rev 01.00 SK Initial release.
*\n
*
* --------------------------------------------------------------------
*
* This code follows the following naming conventions:
*
*\n char              ch_pmod_value
*\n char (array)      s_pmod_s_string[16]
*\n float             f_pmod_value
*\n int32_t           n_pmod_value
*\n int32_t (array)   an_pmod_value[16]
*\n int16_t           w_pmod_value
*\n int16_t (array)   aw_pmod_value[16]
*\n uint16_t          uw_pmod_value
*\n uint16_t (array)  auw_pmod_value[16]
*\n uint8_t           uch_pmod_value
*\n uint8_t (array)   auch_pmod_buffer[16]
*\n uint32_t          un_pmod_value
*\n int32_t *         pn_pmod_value
*
* ------------------------------------------------------------------------- */
/*******************************************************************************
* Copyright (C) 2015 Maxim Integrated Products, Inc., All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Maxim Integrated
* Products, Inc. shall not be used except as stated in the Maxim Integrated
* Products, Inc. Branding Policy.
*
* The mere transfer of this software does not imply any licenses
* of trade secrets, proprietary technology, copyrights, patents,
* trademarks, maskwork rights, or any other form of intellectual
* property whatsoever. Maxim Integrated Products, Inc. retains all
* ownership rights.
*******************************************************************************
*/
#include "algorithm.h"
#include "mbed.h"

void maxim_heart_rate_and_oxygen_saturation(uint32_t *un_ir_buffer ,  int32_t n_ir_buffer_length, uint32_t *un_red_buffer ,   int32_t *n_spo2, int8_t *ch_spo2_valid , 
                              int32_t *n_heart_rate , int8_t  *ch_hr_valid)
/**
* \brief        Calculate the heart rate and SpO2 level
* \par          Details
*               By detecting  peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the ratio for the SPO2 is computed.
*               Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow.
*               Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each ratio.
*
* \param[in]    *un_ir_buffer           - IR sensor data buffer
* \param[in]    n_ir_buffer_length      - IR sensor data buffer length
* \param[in]    *un_red_buffer          - Red sensor data buffer
* \param[out]    *n_spo2                - Calculated SpO2 value
* \param[out]    *ch_spo2_valid         - 1 if the calculated SpO2 value is valid
* \param[out]    *n_heart_rate          - Calculated heart rate value
* \param[out]    *ch_hr_valid           - 1 if the calculated heart rate value is valid
*
* \retval       None
*/
{


        uint32_t irMean ,onlyOnce ;
        int32_t k ,iRatioCount;
        int32_t i,s ,m, exact_ir_valley_locs_count ,middleIdx;
        int32_t th1, n_npks,cMin;      
        int32_t ir_valley_locs[15] ;
        int32_t exact_ir_valley_locs[15] ;
        int32_t dx_peak_locs[15] ;
        int32_t  peakintervalSum;
        
        int32_t yAC, xAC;
        int32_t spo2calc; 
        int32_t yDCmax, xDCmax; 
        int32_t yDCmaxIdx, xDCmaxIdx; 
        int32_t ratio[5],ratioAverage; 
        int32_t nume,  denom ;
        // remove DC of ir signal    
        irMean =0; 
        for (k=0 ; k<n_ir_buffer_length ; k++ ) irMean += un_ir_buffer[k] ;
        irMean =irMean/n_ir_buffer_length ;
        for (k=0 ; k<n_ir_buffer_length ; k++ )  n_x[k] =  un_ir_buffer[k] - irMean ; 
        
        // 4 pt Moving Average
        for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
          denom= ( n_x[k]+n_x[k+1]+ n_x[k+2]+ n_x[k+3]);
          n_x[k]=  denom/(int32_t)4; 
        }

        // get difference of smoothed IR signal
        
       for( k=0; k<BUFFER_SIZE-MA4_SIZE-1;  k++)
           n_dx[k]= (n_x[k+1]- n_x[k]);

       // 2-pt Moving Average to n_dx
       for(k=0; k< BUFFER_SIZE-MA4_SIZE-2; k++){
          n_dx[k] =  ( n_dx[k]+n_dx[k+1])/2 ;
        }
        
       // hamming window
       // flip wave form so that we can detect valley with peak detector
        for ( i=0 ; i<BUFFER_SIZE-HAMMING_SIZE-MA4_SIZE-2 ;i++){
            s= 0;
            for( k=i; k<i+ HAMMING_SIZE ;k++){
                s -= n_dx[k] *uw_hamm[k-i] ; 
                         }
            n_dx[i]= s/ (int32_t)1146; // divide by sum of uw_hamm 
        }
    
     
        th1=0; // threshold calculation
        for ( k=0 ; k<BUFFER_SIZE-HAMMING_SIZE ;k++){
                th1 += ((n_dx[k]>0)? n_dx[k] : ((int32_t)0-n_dx[k])) ;
        }
        th1= th1/ ( BUFFER_SIZE-HAMMING_SIZE);
        // peak location is acutally index for sharpest location of raw signal since we flipped the signal         
        maxim_find_peaks( dx_peak_locs, &n_npks, n_dx, BUFFER_SIZE-HAMMING_SIZE, th1, 8, 5 );//peak_height, peak_distance, max_num_peaks 

        peakintervalSum =0;
                if (n_npks>=2){
                      for (k=1; k<n_npks; k++)
                             peakintervalSum += (dx_peak_locs[k] -dx_peak_locs[k -1] ) ;
                      peakintervalSum =peakintervalSum/(n_npks-1);
                     *n_heart_rate =(int32_t)( 6000/ peakintervalSum );// beats per minutes
                      //prlongf(">>>  *n_heart_rate= %d \n", *n_heart_rate) ;
                      *ch_hr_valid  = 1;
                }
                else  {
                      *n_heart_rate = -999;
                      *ch_hr_valid  = 0;
                }
                
        for ( k=0 ; k<n_npks ;k++)
            ir_valley_locs[k]= dx_peak_locs[k] +HAMMING_SIZE /2; 


          // raw value : RED(=y) and IR(=X)
          // we need to assess DC and AC value of ir and red PPG. 
            for (k=0 ; k<n_ir_buffer_length ; k++ )  {
                n_x[k] =  un_ir_buffer[k] ; 
                n_y[k] =  un_red_buffer[k] ; 
            }

        // find precise min near ir_valley_locs
        exact_ir_valley_locs_count =0; 
        for ( k=0 ; k<n_npks ;k++){
            onlyOnce =1;
            m=ir_valley_locs[k];
            cMin= 16777216;//2^24;
            if (m+5 <  BUFFER_SIZE-HAMMING_SIZE  && m-5 >0){
               
                for(i= m-5;i<m+5; i++)
                   if (n_x[i]<cMin){
                        if (onlyOnce >0){
                           onlyOnce =0;
                       } 
                       cMin= n_x[i] ;
                       exact_ir_valley_locs[k]=i;
                   }
                if (onlyOnce ==0)   exact_ir_valley_locs_count ++ ;
            }
        }
        if (exact_ir_valley_locs_count <2 ){
           *n_spo2 =  -999 ; // do not use SPO2 since signal ratio is out of range
           *ch_spo2_valid  = 0; 
           return;
       }
        // 4 pt MA
        for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
         n_x[k]=( n_x[k]+n_x[k+1]+ n_x[k+2]+ n_x[k+3])/(int32_t)4;
         n_y[k]=( n_y[k]+n_y[k+1]+ n_y[k+2]+ n_y[k+3])/(int32_t)4;
          
        }

        //using exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration ratio
        //finding AC/DC maximum of raw ir * red between two valley locations
        ratioAverage =0; 
        iRatioCount = 0; 
        
        for(k=0; k< 5; k++) ratio[k]=0;
        for (k=0; k< exact_ir_valley_locs_count; k++){
             if (exact_ir_valley_locs[k] > BUFFER_SIZE ){             
                  *n_spo2 =  -999 ; // do not use SPO2 since valley loc is out of range
                  *ch_spo2_valid  = 0; 
                   return;
             }
         }
        // find max between two valley locations 
        // and use ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2 
    
        for (k=0; k< exact_ir_valley_locs_count-1; k++){
              yDCmax= -16777216 ; 
              xDCmax= - 16777216; 
           //   printf("range=%d: %d\n ", exact_ir_valley_locs[k], exact_ir_valley_locs[k+1]);
              if (exact_ir_valley_locs[k+1]-exact_ir_valley_locs[k] >10){
                    for (i=exact_ir_valley_locs[k]; i< exact_ir_valley_locs[k+1]; i++){
                
                          if (n_x[i]> xDCmax) {xDCmax =n_x[i];xDCmaxIdx =i; }
                          if (n_y[i]> yDCmax) {yDCmax =n_y[i];yDCmaxIdx=i;}
                      }
                    yAC= (n_y[exact_ir_valley_locs[k+1]] - n_y[exact_ir_valley_locs[k] ] )*(yDCmaxIdx -exact_ir_valley_locs[k]); //red
                    yAC=  n_y[exact_ir_valley_locs[k]] + yAC/ (exact_ir_valley_locs[k+1] - exact_ir_valley_locs[k])  ; 
            
            
                    yAC=  n_y[yDCmaxIdx] - yAC;    // subracting linear DC compoenents from raw 
                    xAC= (n_x[exact_ir_valley_locs[k+1]] - n_x[exact_ir_valley_locs[k] ] )*(xDCmaxIdx -exact_ir_valley_locs[k]); // ir
                    xAC=  n_x[exact_ir_valley_locs[k]] + xAC/ (exact_ir_valley_locs[k+1] - exact_ir_valley_locs[k]); 
                    xAC=  n_x[yDCmaxIdx] - xAC;      // subracting linear DC compoenents from raw 
                    nume=( yAC *xDCmax)>>7 ; //prepare X100 to preserve floating value
                    denom= ( xAC *yDCmax)>>7;
                    if (denom>0  && iRatioCount <5 &&  nume != 0)
                    {   
                         ratio[iRatioCount]= (nume*100)/denom ; //formular is ( yAC *xDCmax) / ( xAC *yDCmax) ;
                         iRatioCount++;
                    }
              }
            
    //  prlongf("ratio[%d]= %d exact_ir_valley_locs[k] =%d , exact_ir_valley_locs[%d] =%d \n",k, ratio[k] ,exact_ir_valley_locs[k] ,k+1,  exact_ir_valley_locs[k+1]  ) ;
    //  prlongf("nume= %d ,denom= %d  yAC = %d, xDCmax = %d, xAC= %d, yDCmax = %d\n",nume, denom,  yAC ,xDCmax ,xAC ,yDCmax );

        }

        maxim_sort_ascend(ratio, iRatioCount);
        middleIdx= iRatioCount/2;

        if (middleIdx >1)
          ratioAverage =( ratio[middleIdx-1] +ratio[middleIdx])/2; // use median
        else
           ratioAverage = ratio[middleIdx ];

        if( ratioAverage>2 && ratioAverage <184){
                   spo2calc= uch_spo2_table[ratioAverage] ;
            *n_spo2 = spo2calc ;
            *ch_spo2_valid  = 1;//  float_SPO2 =  -45.060*ratioAverage* ratioAverage/10000 + 30.354 *ratioAverage/100 + 94.845 ;  // for comparison with table
        }
        else{
            *n_spo2 =  -999 ; // do not use SPO2 since signal ratio is out of range
            *ch_spo2_valid  = 0; 
        }
            

}


void maxim_find_peaks( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num )
/**
* \brief        Find peaks
* \par          Details
*               Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE
*
* \retval       None
*/
{
    maxim_peaks_above_min_height( n_locs, n_npks, n_x, n_size, n_min_height );
    maxim_remove_close_peaks( n_locs, n_npks, n_x, n_min_distance );
    *n_npks = min( *n_npks, n_max_num );
}

void maxim_peaks_above_min_height( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_size, int32_t n_min_height )
/**
* \brief        Find peaks above n_min_height
* \par          Details
*               Find all peaks above MIN_HEIGHT
*
* \retval       None
*/
{
    int32_t i = 1, width;
    *n_npks = 0;
    
    while (i < n_size-1){
        if (n_x[i] > n_min_height && n_x[i] > n_x[i-1]){            // find left edge of potential peaks
            width = 1;
            while (i+width < n_size && n_x[i] == n_x[i+width])    // find flat peaks
                width++;
            if (n_x[i] > n_x[i+width] && (*n_npks) < 15 ){                            // find right edge of peaks
                n_locs[(*n_npks)++] = i;        
              // for flat peaks, peak location is left edge
                i += width+1;
            }
            else
                i += width;
        }
        else
            i++;
    }
}


void maxim_remove_close_peaks( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_min_distance )
/**
* \brief        Remove peaks
* \par          Details
*               Remove peaks separated by less than MIN_DISTANCE
*
* \retval       None
*/
{
    
    int32_t i, j, old_npks, dist;
    
    /* Order peaks from large to small */
    maxim_sort_indices_descend( n_x, n_locs, *n_npks );

    for ( i = -1; i < *n_npks; i++ ){
        old_npks = *n_npks;
        *n_npks = i+1;
        for ( j = i+1; j < old_npks; j++ ){
            dist =  n_locs[j] - ( i == -1 ? -1 : n_locs[i] ); // lag-zero peak of autocorr is at index -1
            if ( dist > n_min_distance || dist < -n_min_distance )
                n_locs[(*n_npks)++] = n_locs[j];
        }
    }

    // Resort indices longo ascending order
    maxim_sort_ascend( n_locs, *n_npks );
}

void maxim_sort_ascend(int32_t  *n_x, int32_t n_size) 
/**
* \brief        Sort array
* \par          Details
*               Sort array in ascending order (insertion sort algorithm)
*
* \retval       None
*/
{
    int32_t i, j, temp;
    for (i = 1; i < n_size; i++) {
        temp = n_x[i];
        for (j = i; j > 0 && temp < n_x[j-1]; j--)
            n_x[j] = n_x[j-1];
        n_x[j] = temp;
    }
}

void maxim_sort_indices_descend(  int32_t  *n_x, int32_t *n_indx, int32_t n_size)
/**
* \brief        Sort indices
* \par          Details
*               Sort indices according to descending order (insertion sort algorithm)
*
* \retval       None
*/ 
{
    int32_t i, j, temp;
    for (i = 1; i < n_size; i++) {
        temp = n_indx[i];
        for (j = i; j > 0 && n_x[temp] > n_x[n_indx[j-1]]; j--)
            n_indx[j] = n_indx[j-1];
        n_indx[j] = temp;
    }
}