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-21
Revision:
2:560e76e77544
Parent:
1:e88f22c6c1b0
Child:
3:7c0fb55eb3ff

File content as of revision 2:560e76e77544:

/** \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 un_ir_mean ,un_only_once ;
        int32_t k ,n_i_ratio_count;
        int32_t i,s ,m, n_exact_ir_valley_locs_count ,n_middle_idx;
        int32_t n_th1, n_npks,n_c_min;      
        int32_t n_ir_valley_locs[15] ;
        int32_t n_exact_ir_valley_locs[15] ;
        int32_t n_dx_peak_locs[15] ;
        int32_t n_peak_interval_sum;
        
        int32_t n_y_ac, n_x_ac;
        int32_t n_spo2_calc; 
        int32_t n_y_dc_max, n_x_dc_max; 
        int32_t n_y_dc_max_idx, n_x_dc_max_idx; 
        int32_t n_ratio[5],n_ratio_average; 
        int32_t n_nume,  n_denom ;
        // remove DC of ir signal    
        un_ir_mean =0; 
        for (k=0 ; k<n_ir_buffer_length ; k++ ) un_ir_mean += un_ir_buffer[k] ;
        un_ir_mean =un_ir_mean/n_ir_buffer_length ;
        for (k=0 ; k<n_ir_buffer_length ; k++ )  n_x[k] =  un_ir_buffer[k] - un_ir_mean ; 
        
        // 4 pt Moving Average
        for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
          n_denom= ( n_x[k]+n_x[k+1]+ n_x[k+2]+ n_x[k+3]);
          n_x[k]=  n_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 
        }
    
     
        n_th1=0; // threshold calculation
        for ( k=0 ; k<BUFFER_SIZE-HAMMING_SIZE ;k++){
                n_th1 += ((n_dx[k]>0)? n_dx[k] : ((int32_t)0-n_dx[k])) ;
        }
        n_th1= n_th1/ ( BUFFER_SIZE-HAMMING_SIZE);
        // peak location is acutally index for sharpest location of raw signal since we flipped the signal         
        maxim_find_peaks( n_dx_peak_locs, &n_npks, n_dx, BUFFER_SIZE-HAMMING_SIZE, n_th1, 8, 5 );//peak_height, peak_distance, max_num_peaks 

        n_peak_interval_sum =0;
                if (n_npks>=2){
                      for (k=1; k<n_npks; k++)
                             n_peak_interval_sum += (n_dx_peak_locs[k] -n_dx_peak_locs[k -1] ) ;
                      n_peak_interval_sum =n_peak_interval_sum/(n_npks-1);
                     *n_heart_rate =(int32_t)( 6000/ n_peak_interval_sum );// 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++)
            n_ir_valley_locs[k]= n_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 n_ir_valley_locs
        n_exact_ir_valley_locs_count =0; 
        for ( k=0 ; k<n_npks ;k++){
            un_only_once =1;
            m=n_ir_valley_locs[k];
            n_c_min= 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]<n_c_min){
                        if (un_only_once >0){
                           un_only_once =0;
                       } 
                       n_c_min= n_x[i] ;
                       n_exact_ir_valley_locs[k]=i;
                   }
                if (un_only_once ==0)   n_exact_ir_valley_locs_count ++ ;
            }
        }
        if (n_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 n_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
        n_ratio_average =0; 
        n_i_ratio_count = 0; 
        
        for(k=0; k< 5; k++) n_ratio[k]=0;
        for (k=0; k< n_exact_ir_valley_locs_count; k++){
             if (n_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< n_exact_ir_valley_locs_count-1; k++){
              n_y_dc_max= -16777216 ; 
              n_x_dc_max= - 16777216; 
           //   printf("range=%d: %d\n ", n_exact_ir_valley_locs[k], n_exact_ir_valley_locs[k+1]);
              if (n_exact_ir_valley_locs[k+1]-n_exact_ir_valley_locs[k] >10){
                    for (i=n_exact_ir_valley_locs[k]; i< n_exact_ir_valley_locs[k+1]; i++){
                
                          if (n_x[i]> n_x_dc_max) {n_x_dc_max =n_x[i];n_x_dc_max_idx =i; }
                          if (n_y[i]> n_y_dc_max) {n_y_dc_max =n_y[i];n_y_dc_max_idx=i;}
                      }
                    n_y_ac= (n_y[n_exact_ir_valley_locs[k+1]] - n_y[n_exact_ir_valley_locs[k] ] )*(n_y_dc_max_idx -n_exact_ir_valley_locs[k]); //red
                    n_y_ac=  n_y[n_exact_ir_valley_locs[k]] + n_y_ac/ (n_exact_ir_valley_locs[k+1] - n_exact_ir_valley_locs[k])  ; 
            
            
                    n_y_ac=  n_y[n_y_dc_max_idx] - n_y_ac;    // subracting linear DC compoenents from raw 
                    n_x_ac= (n_x[n_exact_ir_valley_locs[k+1]] - n_x[n_exact_ir_valley_locs[k] ] )*(n_x_dc_max_idx -n_exact_ir_valley_locs[k]); // ir
                    n_x_ac=  n_x[n_exact_ir_valley_locs[k]] + n_x_ac/ (n_exact_ir_valley_locs[k+1] - n_exact_ir_valley_locs[k]); 
                    n_x_ac=  n_x[n_y_dc_max_idx] - n_x_ac;      // subracting linear DC compoenents from raw 
                    n_nume=( n_y_ac *n_x_dc_max)>>7 ; //prepare X100 to preserve floating value
                    n_denom= ( n_x_ac *n_y_dc_max)>>7;
                    if (n_denom>0  && n_i_ratio_count <5 &&  n_nume != 0)
                    {   
                         n_ratio[n_i_ratio_count]= (n_nume*100)/n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ;
                         n_i_ratio_count++;
                    }
              }
            
    //  prlongf("n_ratio[%d]= %d n_exact_ir_valley_locs[k] =%d , n_exact_ir_valley_locs[%d] =%d \n",k, n_ratio[k] ,n_exact_ir_valley_locs[k] ,k+1,  n_exact_ir_valley_locs[k+1]  ) ;
    //  prlongf("n_nume= %d ,n_denom= %d  n_y_ac = %d, n_x_dc_max = %d, n_x_ac= %d, n_y_dc_max = %d\n",n_nume, n_denom,  n_y_ac ,n_x_dc_max ,n_x_ac ,n_y_dc_max );

        }

        maxim_sort_ascend(n_ratio, n_i_ratio_count);
        n_middle_idx= n_i_ratio_count/2;

        if (n_middle_idx >1)
          n_ratio_average =( n_ratio[n_middle_idx-1] +n_ratio[n_middle_idx])/2; // use median
        else
           n_ratio_average = n_ratio[n_middle_idx ];

        if( n_ratio_average>2 && n_ratio_average <184){
                   n_spo2_calc= uch_spo2_table[n_ratio_average] ;
            *n_spo2 = n_spo2_calc ;
            *ch_spo2_valid  = 1;//  float_SPO2 =  -45.060*n_ratio_average* n_ratio_average/10000 + 30.354 *n_ratio_average/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, n_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
            n_width = 1;
            while (i+n_width < n_size && n_x[i] == n_x[i+n_width])    // find flat peaks
                n_width++;
            if (n_x[i] > n_x[i+n_width] && (*n_npks) < 15 ){                            // find right edge of peaks
                n_locs[(*n_npks)++] = i;        
              // for flat peaks, peak location is left edge
                i += n_width+1;
            }
            else
                i += n_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, n_old_npks, n_dist;
    
    /* Order peaks from large to small */
    maxim_sort_indices_descend( n_x, n_locs, *n_npks );

    for ( i = -1; i < *n_npks; i++ ){
        n_old_npks = *n_npks;
        *n_npks = i+1;
        for ( j = i+1; j < n_old_npks; j++ ){
            n_dist =  n_locs[j] - ( i == -1 ? -1 : n_locs[i] ); // lag-zero peak of autocorr is at index -1
            if ( n_dist > n_min_distance || n_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, n_temp;
    for (i = 1; i < n_size; i++) {
        n_temp = n_x[i];
        for (j = i; j > 0 && n_temp < n_x[j-1]; j--)
            n_x[j] = n_x[j-1];
        n_x[j] = n_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, n_temp;
    for (i = 1; i < n_size; i++) {
        n_temp = n_indx[i];
        for (j = i; j > 0 && n_x[n_temp] > n_x[n_indx[j-1]]; j--)
            n_indx[j] = n_indx[j-1];
        n_indx[j] = n_temp;
    }
}